Skip to content

Commit 0726151

Browse files
andrew-codechimpdependabot[bot]P-v-Daskpatrickwbmos
authored
Battery Replaced (#147)
* 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>
1 parent 855d265 commit 0726151

File tree

17 files changed

+770
-27
lines changed

17 files changed

+770
-27
lines changed

.devcontainer.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
"editor.tabSize": 4,
2525
"python.pythonPath": "/usr/bin/python3",
2626
"python.analysis.autoSearchPaths": false,
27-
"python.linting.pylintEnabled": true,
28-
"python.linting.enabled": true,
2927
"python.formatting.provider": "black",
3028
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
3129
"editor.formatOnPaste": false,

README.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ Integration to add battery notes to a device, with automatic discovery via a gro
1919

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

22-
Platform | Description
23-
-- | --
24-
`sensor` | Show battery type.
22+
Platform | Name | Description
23+
-- | -- | --
24+
`sensor` | Battery Type | Show battery type.
25+
`sensor` | Battery last replaced | Date & Time the battery was last replaced.
26+
`button` | Battery replaced | Update Battery last replaced to now.
27+
`service` | Set battery replaced | Update Battery last replaced to now.
2528

2629
## Installation
2730

custom_components/battery_notes/__init__.py

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from __future__ import annotations
77

88
import logging
9+
from datetime import datetime
910

1011
import homeassistant.helpers.config_validation as cv
1112
import voluptuous as vol
@@ -15,14 +16,18 @@
1516
from homeassistant.core import HomeAssistant, callback
1617
from homeassistant.const import __version__ as HA_VERSION # noqa: N812
1718
from homeassistant.helpers.aiohttp_client import async_get_clientsession
18-
1919
from homeassistant.helpers.typing import ConfigType
20+
from homeassistant.helpers import device_registry as dr
2021

2122
from .discovery import DiscoveryManager
2223
from .library_coordinator import BatteryNotesLibraryUpdateCoordinator
2324
from .library_updater import (
2425
LibraryUpdaterClient,
2526
)
27+
from .coordinator import BatteryNotesCoordinator
28+
from .store import (
29+
async_get_registry,
30+
)
2631

2732
from .const import (
2833
DOMAIN,
@@ -32,6 +37,10 @@
3237
CONF_LIBRARY,
3338
DATA_UPDATE_COORDINATOR,
3439
CONF_SHOW_ALL_DEVICES,
40+
SERVICE_BATTERY_REPLACED,
41+
SERVICE_BATTERY_REPLACED_SCHEMA,
42+
DATA_COORDINATOR,
43+
ATTR_REMOVE,
3544
)
3645

3746
MIN_HA_VERSION = "2023.7"
@@ -53,6 +62,7 @@
5362
extra=vol.ALLOW_EXTRA,
5463
)
5564

65+
ATTR_SERVICE_DEVICE_ID = "device_id"
5666

5767
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
5868
"""Integration setup."""
@@ -75,12 +85,17 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
7585
DOMAIN_CONFIG: domain_config,
7686
}
7787

78-
coordinator = BatteryNotesLibraryUpdateCoordinator(
88+
store = await async_get_registry(hass)
89+
90+
coordinator = BatteryNotesCoordinator(hass, store)
91+
hass.data[DOMAIN][DATA_COORDINATOR] = coordinator
92+
93+
library_coordinator = BatteryNotesLibraryUpdateCoordinator(
7994
hass=hass,
8095
client=LibraryUpdaterClient(session=async_get_clientsession(hass)),
8196
)
8297

83-
hass.data[DOMAIN][DATA_UPDATE_COORDINATOR] = coordinator
98+
hass.data[DOMAIN][DATA_UPDATE_COORDINATOR] = library_coordinator
8499

85100
await coordinator.async_refresh()
86101

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

101116
entry.async_on_unload(entry.add_update_listener(async_update_options))
102117

118+
# Register custom services
119+
register_services(hass)
120+
103121
return True
104122

105123

124+
async def async_remove_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
125+
"""Device removed, tidy up store."""
126+
127+
if "device_id" not in config_entry.data:
128+
return
129+
130+
device_id = config_entry.data["device_id"]
131+
132+
coordinator = hass.data[DOMAIN][DATA_COORDINATOR]
133+
data = {ATTR_REMOVE: True}
134+
135+
coordinator.async_update_device_config(device_id=device_id, data=data)
136+
137+
_LOGGER.debug("Removed Device %s", device_id)
138+
139+
106140
@callback
107141
async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None:
108142
"""Update options."""
@@ -114,6 +148,43 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
114148
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
115149

116150

117-
async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
118-
"""Update listener, called when the config entry options are changed."""
119-
await hass.config_entries.async_reload(entry.entry_id)
151+
@callback
152+
def register_services(hass):
153+
"""Register services used by battery notes component."""
154+
155+
async def handle_battery_replaced(call):
156+
"""Handle the service call."""
157+
device_id = call.data.get(ATTR_SERVICE_DEVICE_ID, "")
158+
159+
device_registry = dr.async_get(hass)
160+
161+
device_entry = device_registry.async_get(device_id)
162+
if not device_entry:
163+
return
164+
165+
for entry_id in device_entry.config_entries:
166+
if (
167+
entry := hass.config_entries.async_get_entry(entry_id)
168+
) and entry.domain == DOMAIN:
169+
date_replaced = datetime.utcnow()
170+
171+
coordinator = hass.data[DOMAIN][DATA_COORDINATOR]
172+
device_entry = {"battery_last_replaced": date_replaced}
173+
174+
coordinator.async_update_device_config(
175+
device_id=device_id, data=device_entry
176+
)
177+
178+
await coordinator._async_update_data()
179+
await coordinator.async_request_refresh()
180+
181+
_LOGGER.debug(
182+
"Device %s battery replaced on %s", device_id, str(date_replaced)
183+
)
184+
185+
hass.services.async_register(
186+
DOMAIN,
187+
SERVICE_BATTERY_REPLACED,
188+
handle_battery_replaced,
189+
schema=SERVICE_BATTERY_REPLACED_SCHEMA,
190+
)
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
"""Button platform for battery_notes."""
2+
from __future__ import annotations
3+
4+
from dataclasses import dataclass
5+
from datetime import datetime
6+
7+
import voluptuous as vol
8+
9+
from homeassistant.config_entries import ConfigEntry
10+
from homeassistant.const import CONF_ENTITY_ID
11+
from homeassistant.core import HomeAssistant, callback, Event
12+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
13+
from homeassistant.helpers import (
14+
config_validation as cv,
15+
device_registry as dr,
16+
entity_registry as er,
17+
)
18+
from homeassistant.components.button import (
19+
PLATFORM_SCHEMA,
20+
ButtonEntity,
21+
ButtonEntityDescription,
22+
)
23+
from homeassistant.helpers.entity import DeviceInfo, EntityCategory
24+
from homeassistant.helpers.event import (
25+
async_track_entity_registry_updated_event,
26+
)
27+
28+
from homeassistant.helpers.reload import async_setup_reload_service
29+
from homeassistant.helpers.typing import (
30+
ConfigType,
31+
)
32+
33+
from homeassistant.const import (
34+
CONF_NAME,
35+
CONF_UNIQUE_ID,
36+
CONF_DEVICE_ID,
37+
)
38+
39+
from . import PLATFORMS
40+
41+
from .const import (
42+
DOMAIN,
43+
DATA_COORDINATOR,
44+
)
45+
46+
from .entity import (
47+
BatteryNotesEntityDescription,
48+
)
49+
50+
51+
@dataclass
52+
class BatteryNotesButtonEntityDescription(
53+
BatteryNotesEntityDescription,
54+
ButtonEntityDescription,
55+
):
56+
"""Describes Battery Notes button entity."""
57+
58+
unique_id_suffix: str
59+
60+
61+
ENTITY_DESCRIPTIONS: tuple[BatteryNotesButtonEntityDescription, ...] = (
62+
BatteryNotesButtonEntityDescription(
63+
unique_id_suffix="_battery_replaced_button",
64+
key="battery_replaced",
65+
translation_key="battery_replaced",
66+
icon="mdi:battery-sync",
67+
entity_category=EntityCategory.DIAGNOSTIC,
68+
),
69+
)
70+
71+
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
72+
{vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_DEVICE_ID): cv.string}
73+
)
74+
75+
76+
@callback
77+
def async_add_to_device(hass: HomeAssistant, entry: ConfigEntry) -> str | None:
78+
"""Add our config entry to the device."""
79+
device_registry = dr.async_get(hass)
80+
81+
device_id = entry.data.get(CONF_DEVICE_ID)
82+
device_registry.async_update_device(device_id, add_config_entry_id=entry.entry_id)
83+
84+
return device_id
85+
86+
87+
async def async_setup_entry(
88+
hass: HomeAssistant,
89+
config_entry: ConfigEntry,
90+
async_add_entities: AddEntitiesCallback,
91+
) -> None:
92+
"""Initialize Battery Type config entry."""
93+
entity_registry = er.async_get(hass)
94+
device_registry = dr.async_get(hass)
95+
96+
device_id = config_entry.data.get(CONF_DEVICE_ID)
97+
98+
async def async_registry_updated(event: Event) -> None:
99+
"""Handle entity registry update."""
100+
data = event.data
101+
if data["action"] == "remove":
102+
await hass.config_entries.async_remove(config_entry.entry_id)
103+
104+
if data["action"] != "update":
105+
return
106+
107+
if "entity_id" in data["changes"]:
108+
# Entity_id changed, reload the config entry
109+
await hass.config_entries.async_reload(config_entry.entry_id)
110+
111+
if device_id and "device_id" in data["changes"]:
112+
# If the tracked battery note is no longer in the device, remove our config entry
113+
# from the device
114+
if (
115+
not (entity_entry := entity_registry.async_get(data[CONF_ENTITY_ID]))
116+
or not device_registry.async_get(device_id)
117+
or entity_entry.device_id == device_id
118+
):
119+
# No need to do any cleanup
120+
return
121+
122+
device_registry.async_update_device(
123+
device_id, remove_config_entry_id=config_entry.entry_id
124+
)
125+
126+
config_entry.async_on_unload(
127+
async_track_entity_registry_updated_event(
128+
hass, config_entry.entry_id, async_registry_updated
129+
)
130+
)
131+
132+
device_id = async_add_to_device(hass, config_entry)
133+
134+
async_add_entities(
135+
BatteryNotesButton(
136+
hass,
137+
description,
138+
f"{config_entry.entry_id}{description.unique_id_suffix}",
139+
device_id,
140+
)
141+
for description in ENTITY_DESCRIPTIONS
142+
)
143+
144+
145+
async def async_setup_platform(
146+
hass: HomeAssistant,
147+
config: ConfigType,
148+
async_add_entities: AddEntitiesCallback,
149+
) -> None:
150+
"""Set up the battery type button."""
151+
device_id: str = config[CONF_DEVICE_ID]
152+
153+
await async_setup_reload_service(hass, DOMAIN, PLATFORMS)
154+
155+
async_add_entities(
156+
BatteryNotesButton(
157+
hass,
158+
description,
159+
f"{config.get(CONF_UNIQUE_ID)}{description.unique_id_suffix}",
160+
device_id,
161+
)
162+
for description in ENTITY_DESCRIPTIONS
163+
)
164+
165+
166+
class BatteryNotesButton(ButtonEntity):
167+
"""Represents a battery replaced button."""
168+
169+
_attr_should_poll = False
170+
171+
entity_description: BatteryNotesButtonEntityDescription
172+
173+
def __init__(
174+
self,
175+
hass: HomeAssistant,
176+
description: BatteryNotesButtonEntityDescription,
177+
unique_id: str,
178+
device_id: str,
179+
) -> None:
180+
"""Create a battery replaced button."""
181+
device_registry = dr.async_get(hass)
182+
183+
self.entity_description = description
184+
self._attr_unique_id = unique_id
185+
self._attr_has_entity_name = True
186+
self._device_id = device_id
187+
188+
self._device_id = device_id
189+
if device_id and (device := device_registry.async_get(device_id)):
190+
self._attr_device_info = DeviceInfo(
191+
connections=device.connections,
192+
identifiers=device.identifiers,
193+
)
194+
195+
async def async_added_to_hass(self) -> None:
196+
"""Handle added to Hass."""
197+
# Update entity options
198+
registry = er.async_get(self.hass)
199+
if registry.async_get(self.entity_id) is not None:
200+
registry.async_update_entity_options(
201+
self.entity_id,
202+
DOMAIN,
203+
{"entity_id": self._attr_unique_id},
204+
)
205+
206+
async def update_battery_last_replaced(self):
207+
"""Handle sensor state changes."""
208+
209+
# device_id = self._device_id
210+
211+
# device_entry = {
212+
# "battery_last_replaced" : datetime.utcnow()
213+
# }
214+
215+
# coordinator = self.hass.data[DOMAIN][DATA_COORDINATOR]
216+
# coordinator.async_update_device_config(device_id = device_id, data = device_entry)
217+
218+
self.async_write_ha_state()
219+
220+
async def async_press(self) -> None:
221+
"""Press the button."""
222+
device_id = self._device_id
223+
224+
device_entry = {"battery_last_replaced": datetime.utcnow()}
225+
226+
coordinator = self.hass.data[DOMAIN][DATA_COORDINATOR]
227+
coordinator.async_update_device_config(device_id=device_id, data=device_entry)
228+
await coordinator._async_update_data()
229+
await coordinator.async_request_refresh()

0 commit comments

Comments
 (0)