Skip to content

Commit

Permalink
Battery Replaced (#147)
Browse files Browse the repository at this point in the history
* An initial button

* Bump ruff from 0.0.287 to 0.0.288

Bumps [ruff](https://github.com/astral-sh/ruff) from 0.0.287 to 0.0.288.
- [Release notes](https://github.com/astral-sh/ruff/releases)
- [Changelog](https://github.com/astral-sh/ruff/blob/main/BREAKING_CHANGES.md)
- [Commits](astral-sh/ruff@v0.0.287...v0.0.288)

---
updated-dependencies:
- dependency-name: ruff
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update README.md

* Made the button appear

* Start of multiple sensors

* Remove unused const

* Improving sensors

* Add entity translations

* Tidy up entity naming

* Formatting

* Update minimum requirements

* Work on battery changed

* Got a datetime for last changed

* Work on last changed sensor

* Trying things

* Work on last changed time

* Work on last changed

* Manual merge from main refactor

* WIP

* WIP

* Update library.json (#48)

* Update library.json

IKEA - TRADFRI motion sensor (E1525/E1745) - Edit to 2X CR2032

SONOFF - Temperature and humidity sensor (SNZB-02) - Edit to CR2450

IKEA - TRADFRI ON/OFF switch (E1743) - Added Configuration

Saswell - Thermostatic radiator valve (SEA801-Zigbee/SEA802-Zigbee) - Added Configuration

Siterwell - Radiator valve with thermostat (GS361A-H04) - Added Configuration

* Update library.json

Add comma

---------

Co-authored-by: Andrew Jackson <andrew@codechimp.org>

* Add 2 Devices (#50)

* Add 2 Devices

These are how two of my Aqara Zigbee devices showed up.

* Update library.json

Remove quantity where just 1

---------

Co-authored-by: Andrew Jackson <andrew@codechimp.org>

* Update manifest.json

Version bump

* Add discovery screenshot

* Update README.md

* Update discovery screenshot

* WIP

* WIP

* remove duplicate entries for LUMI devices (#121)

* Update library.json (#122)

Change HEIMAN smoke detectors, the batteries are replaceable.

* Update library.json (#123)

* Fix Philips Hue Dimmer switch v2 manufacturer name

* Add Roborock S7

* Update library.json

* Update library.json

---------

Co-authored-by: Andrew Jackson <andrew@codechimp.org>

* Create translation to Hungarian (#125)

* Added device details (Xiaomi SRTS-A01, Sonoff TRVZB) (#127)

* Added "SONOFF TRVZB"

* Added "Xiaomi SRTS-A01"

* Fixed formatting

* Update library.json

---------

Co-authored-by: Andrew Jackson <andrew@codechimp.org>

* Update library.json (#128)

Add Ecolink TILT-ZWAVE2.5-ECO tilt sensor

* Add Drayton Wiser iTRV & RoomStat (#129)

* Updated readme and additions to library

* Update README.md

* Add multiple devices to library (#130)

* Add multiple devices to library

* Update library.json

---------

Co-authored-by: Andrew Jackson <andrew@codechimp.org>

* Update README.md

* Update validate.yml

Ignore readme changes

* WIP

* WIP

* Create battery_changed service

* Change the service to use the last changed entity

* WIP

* WIP

* WIP

* Change back to device id

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* Button press update store

* Docs

* Sensor update on button & service

* WIP

* Tidy up storage on device remove

* Check device exists on service call

* Lint fixes

* Update HA version to test service translation

* Add placeholders for new translation requirements

* Tidy up code

* Add new fields to Danish translation

* Remove date from service

* Rename to battery replaced

* Update service description

* Update library

* Fix lint issues & version bump

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: P-v-D <88889180+P-v-D@users.noreply.github.com>
Co-authored-by: Patrick <4002194+askpatrickw@users.noreply.github.com>
Co-authored-by: Wil T <wil.thieme@protonmail.com>
Co-authored-by: Edouard Kleinhans <edouard@kleinhans.info>
Co-authored-by: Jan Čermák <info@jan-cermak.cz>
Co-authored-by: bekesizl <41523450+bekesizl@users.noreply.github.com>
Co-authored-by: Janek <6506725+jkrgr0@users.noreply.github.com>
Co-authored-by: Christian Allred <13487734+cgallred@users.noreply.github.com>
Co-authored-by: Thomas Dietrich <Thomas@Nurzen.de>
  • Loading branch information
11 people authored Dec 26, 2023
1 parent 855d265 commit 0726151
Show file tree
Hide file tree
Showing 17 changed files with 770 additions and 27 deletions.
2 changes: 0 additions & 2 deletions .devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
"editor.tabSize": 4,
"python.pythonPath": "/usr/bin/python3",
"python.analysis.autoSearchPaths": false,
"python.linting.pylintEnabled": true,
"python.linting.enabled": true,
"python.formatting.provider": "black",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"editor.formatOnPaste": false,
Expand Down
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@ Integration to add battery notes to a device, with automatic discovery via a gro

**This integration will set up the following platforms.**

Platform | Description
-- | --
`sensor` | Show battery type.
Platform | Name | Description
-- | -- | --
`sensor` | Battery Type | Show battery type.
`sensor` | Battery last replaced | Date & Time the battery was last replaced.
`button` | Battery replaced | Update Battery last replaced to now.
`service` | Set battery replaced | Update Battery last replaced to now.

## Installation

Expand Down
83 changes: 77 additions & 6 deletions custom_components/battery_notes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from __future__ import annotations

import logging
from datetime import datetime

import homeassistant.helpers.config_validation as cv
import voluptuous as vol
Expand All @@ -15,14 +16,18 @@
from homeassistant.core import HomeAssistant, callback
from homeassistant.const import __version__ as HA_VERSION # noqa: N812
from homeassistant.helpers.aiohttp_client import async_get_clientsession

from homeassistant.helpers.typing import ConfigType
from homeassistant.helpers import device_registry as dr

from .discovery import DiscoveryManager
from .library_coordinator import BatteryNotesLibraryUpdateCoordinator
from .library_updater import (
LibraryUpdaterClient,
)
from .coordinator import BatteryNotesCoordinator
from .store import (
async_get_registry,
)

from .const import (
DOMAIN,
Expand All @@ -32,6 +37,10 @@
CONF_LIBRARY,
DATA_UPDATE_COORDINATOR,
CONF_SHOW_ALL_DEVICES,
SERVICE_BATTERY_REPLACED,
SERVICE_BATTERY_REPLACED_SCHEMA,
DATA_COORDINATOR,
ATTR_REMOVE,
)

MIN_HA_VERSION = "2023.7"
Expand All @@ -53,6 +62,7 @@
extra=vol.ALLOW_EXTRA,
)

ATTR_SERVICE_DEVICE_ID = "device_id"

async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Integration setup."""
Expand All @@ -75,12 +85,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
DOMAIN_CONFIG: domain_config,
}

coordinator = BatteryNotesLibraryUpdateCoordinator(
store = await async_get_registry(hass)

coordinator = BatteryNotesCoordinator(hass, store)
hass.data[DOMAIN][DATA_COORDINATOR] = coordinator

library_coordinator = BatteryNotesLibraryUpdateCoordinator(
hass=hass,
client=LibraryUpdaterClient(session=async_get_clientsession(hass)),
)

hass.data[DOMAIN][DATA_UPDATE_COORDINATOR] = coordinator
hass.data[DOMAIN][DATA_UPDATE_COORDINATOR] = library_coordinator

await coordinator.async_refresh()

Expand All @@ -100,9 +115,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

entry.async_on_unload(entry.add_update_listener(async_update_options))

# Register custom services
register_services(hass)

return True


async def async_remove_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Device removed, tidy up store."""

if "device_id" not in config_entry.data:
return

device_id = config_entry.data["device_id"]

coordinator = hass.data[DOMAIN][DATA_COORDINATOR]
data = {ATTR_REMOVE: True}

coordinator.async_update_device_config(device_id=device_id, data=data)

_LOGGER.debug("Removed Device %s", device_id)


@callback
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update options."""
Expand All @@ -114,6 +148,43 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)


async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
"""Update listener, called when the config entry options are changed."""
await hass.config_entries.async_reload(entry.entry_id)
@callback
def register_services(hass):
"""Register services used by battery notes component."""

async def handle_battery_replaced(call):
"""Handle the service call."""
device_id = call.data.get(ATTR_SERVICE_DEVICE_ID, "")

device_registry = dr.async_get(hass)

device_entry = device_registry.async_get(device_id)
if not device_entry:
return

for entry_id in device_entry.config_entries:
if (
entry := hass.config_entries.async_get_entry(entry_id)
) and entry.domain == DOMAIN:
date_replaced = datetime.utcnow()

coordinator = hass.data[DOMAIN][DATA_COORDINATOR]
device_entry = {"battery_last_replaced": date_replaced}

coordinator.async_update_device_config(
device_id=device_id, data=device_entry
)

await coordinator._async_update_data()
await coordinator.async_request_refresh()

_LOGGER.debug(
"Device %s battery replaced on %s", device_id, str(date_replaced)
)

hass.services.async_register(
DOMAIN,
SERVICE_BATTERY_REPLACED,
handle_battery_replaced,
schema=SERVICE_BATTERY_REPLACED_SCHEMA,
)
229 changes: 229 additions & 0 deletions custom_components/battery_notes/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
"""Button platform for battery_notes."""
from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime

import voluptuous as vol

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ENTITY_ID
from homeassistant.core import HomeAssistant, callback, Event
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers import (
config_validation as cv,
device_registry as dr,
entity_registry as er,
)
from homeassistant.components.button import (
PLATFORM_SCHEMA,
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
from homeassistant.helpers.event import (
async_track_entity_registry_updated_event,
)

from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.typing import (
ConfigType,
)

from homeassistant.const import (
CONF_NAME,
CONF_UNIQUE_ID,
CONF_DEVICE_ID,
)

from . import PLATFORMS

from .const import (
DOMAIN,
DATA_COORDINATOR,
)

from .entity import (
BatteryNotesEntityDescription,
)


@dataclass
class BatteryNotesButtonEntityDescription(
BatteryNotesEntityDescription,
ButtonEntityDescription,
):
"""Describes Battery Notes button entity."""

unique_id_suffix: str


ENTITY_DESCRIPTIONS: tuple[BatteryNotesButtonEntityDescription, ...] = (
BatteryNotesButtonEntityDescription(
unique_id_suffix="_battery_replaced_button",
key="battery_replaced",
translation_key="battery_replaced",
icon="mdi:battery-sync",
entity_category=EntityCategory.DIAGNOSTIC,
),
)

PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_DEVICE_ID): cv.string}
)


@callback
def async_add_to_device(hass: HomeAssistant, entry: ConfigEntry) -> str | None:
"""Add our config entry to the device."""
device_registry = dr.async_get(hass)

device_id = entry.data.get(CONF_DEVICE_ID)
device_registry.async_update_device(device_id, add_config_entry_id=entry.entry_id)

return device_id


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Initialize Battery Type config entry."""
entity_registry = er.async_get(hass)
device_registry = dr.async_get(hass)

device_id = config_entry.data.get(CONF_DEVICE_ID)

async def async_registry_updated(event: Event) -> None:
"""Handle entity registry update."""
data = event.data
if data["action"] == "remove":
await hass.config_entries.async_remove(config_entry.entry_id)

if data["action"] != "update":
return

if "entity_id" in data["changes"]:
# Entity_id changed, reload the config entry
await hass.config_entries.async_reload(config_entry.entry_id)

if device_id and "device_id" in data["changes"]:
# If the tracked battery note is no longer in the device, remove our config entry
# from the device
if (
not (entity_entry := entity_registry.async_get(data[CONF_ENTITY_ID]))
or not device_registry.async_get(device_id)
or entity_entry.device_id == device_id
):
# No need to do any cleanup
return

device_registry.async_update_device(
device_id, remove_config_entry_id=config_entry.entry_id
)

config_entry.async_on_unload(
async_track_entity_registry_updated_event(
hass, config_entry.entry_id, async_registry_updated
)
)

device_id = async_add_to_device(hass, config_entry)

async_add_entities(
BatteryNotesButton(
hass,
description,
f"{config_entry.entry_id}{description.unique_id_suffix}",
device_id,
)
for description in ENTITY_DESCRIPTIONS
)


async def async_setup_platform(
hass: HomeAssistant,
config: ConfigType,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the battery type button."""
device_id: str = config[CONF_DEVICE_ID]

await async_setup_reload_service(hass, DOMAIN, PLATFORMS)

async_add_entities(
BatteryNotesButton(
hass,
description,
f"{config.get(CONF_UNIQUE_ID)}{description.unique_id_suffix}",
device_id,
)
for description in ENTITY_DESCRIPTIONS
)


class BatteryNotesButton(ButtonEntity):
"""Represents a battery replaced button."""

_attr_should_poll = False

entity_description: BatteryNotesButtonEntityDescription

def __init__(
self,
hass: HomeAssistant,
description: BatteryNotesButtonEntityDescription,
unique_id: str,
device_id: str,
) -> None:
"""Create a battery replaced button."""
device_registry = dr.async_get(hass)

self.entity_description = description
self._attr_unique_id = unique_id
self._attr_has_entity_name = True
self._device_id = device_id

self._device_id = device_id
if device_id and (device := device_registry.async_get(device_id)):
self._attr_device_info = DeviceInfo(
connections=device.connections,
identifiers=device.identifiers,
)

async def async_added_to_hass(self) -> None:
"""Handle added to Hass."""
# Update entity options
registry = er.async_get(self.hass)
if registry.async_get(self.entity_id) is not None:
registry.async_update_entity_options(
self.entity_id,
DOMAIN,
{"entity_id": self._attr_unique_id},
)

async def update_battery_last_replaced(self):
"""Handle sensor state changes."""

# device_id = self._device_id

# device_entry = {
# "battery_last_replaced" : datetime.utcnow()
# }

# coordinator = self.hass.data[DOMAIN][DATA_COORDINATOR]
# coordinator.async_update_device_config(device_id = device_id, data = device_entry)

self.async_write_ha_state()

async def async_press(self) -> None:
"""Press the button."""
device_id = self._device_id

device_entry = {"battery_last_replaced": datetime.utcnow()}

coordinator = self.hass.data[DOMAIN][DATA_COORDINATOR]
coordinator.async_update_device_config(device_id=device_id, data=device_entry)
await coordinator._async_update_data()
await coordinator.async_request_refresh()
Loading

0 comments on commit 0726151

Please sign in to comment.