From 0fdf1f88ec31eb44f3deb66a74cd56152fb2fcce Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Fri, 1 Nov 2024 11:23:08 +0000 Subject: [PATCH 1/4] Split the service into a separate file from init --- custom_components/battery_notes/__init__.py | 237 +----------------- custom_components/battery_notes/services.py | 252 ++++++++++++++++++++ 2 files changed, 255 insertions(+), 234 deletions(-) create mode 100644 custom_components/battery_notes/services.py diff --git a/custom_components/battery_notes/__init__.py b/custom_components/battery_notes/__init__.py index 4b885a00..4aaa385c 100644 --- a/custom_components/battery_notes/__init__.py +++ b/custom_components/battery_notes/__init__.py @@ -1,4 +1,4 @@ -"""Custom integration to integrate BatteryNotes with Home Assistant. +"""Battery Notes integration for Home Assistant. For more details about this integration, please refer to https://github.com/andrew-codechimp/ha-battery-notes @@ -9,7 +9,6 @@ import logging import re from dataclasses import dataclass, field -from datetime import datetime import voluptuous as vol from awesomeversion.awesomeversion import AwesomeVersion @@ -17,27 +16,13 @@ from homeassistant.const import __version__ as HA_VERSION # noqa: N812 from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import device_registry as dr from homeassistant.helpers import entity_registry as er from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util from .config_flow import CONFIG_VERSION from .const import ( - ATTR_BATTERY_LAST_REPORTED, - ATTR_BATTERY_LAST_REPORTED_DAYS, - ATTR_BATTERY_LAST_REPORTED_LEVEL, - ATTR_BATTERY_LEVEL, - ATTR_BATTERY_LOW, - ATTR_BATTERY_QUANTITY, - ATTR_BATTERY_THRESHOLD_REMINDER, - ATTR_BATTERY_TYPE, - ATTR_BATTERY_TYPE_AND_QUANTITY, - ATTR_DEVICE_ID, - ATTR_DEVICE_NAME, - ATTR_PREVIOUS_BATTERY_LEVEL, ATTR_REMOVE, - ATTR_SOURCE_ENTITY_ID, CONF_BATTERY_INCREASE_THRESHOLD, CONF_BATTERY_QUANTITY, CONF_BATTERY_TYPE, @@ -55,24 +40,15 @@ DEFAULT_BATTERY_LOW_THRESHOLD, DOMAIN, DOMAIN_CONFIG, - EVENT_BATTERY_NOT_REPORTED, - EVENT_BATTERY_REPLACED, - EVENT_BATTERY_THRESHOLD, MIN_HA_VERSION, PLATFORMS, - SERVICE_BATTERY_REPLACED, - SERVICE_BATTERY_REPLACED_SCHEMA, - SERVICE_CHECK_BATTERY_LAST_REPORTED, - SERVICE_CHECK_BATTERY_LAST_REPORTED_SCHEMA, - SERVICE_CHECK_BATTERY_LOW, - SERVICE_DATA_DATE_TIME_REPLACED, - SERVICE_DATA_DAYS_LAST_REPORTED, ) from .device import BatteryNotesDevice from .discovery import DiscoveryManager from .library_updater import ( LibraryUpdater, ) +from .services import setup_services from .store import ( async_get_registry, ) @@ -158,7 +134,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: _LOGGER.debug("Auto discovery disabled") # Register custom services - register_services(hass) + setup_services(hass) return True @@ -267,210 +243,3 @@ async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry): async def async_update_options(hass: HomeAssistant, entry: ConfigEntry) -> None: """Update options.""" await hass.config_entries.async_reload(entry.entry_id) - - -@callback -def register_services(hass: HomeAssistant): - """Register services used by battery notes component.""" - - async def handle_battery_replaced(call): - """Handle the service call.""" - device_id = call.data.get(ATTR_DEVICE_ID, "") - source_entity_id = call.data.get(ATTR_SOURCE_ENTITY_ID, "") - datetime_replaced_entry = call.data.get(SERVICE_DATA_DATE_TIME_REPLACED) - - if datetime_replaced_entry: - datetime_replaced = dt_util.as_utc(datetime_replaced_entry).replace( - tzinfo=None - ) - else: - datetime_replaced = datetime.utcnow() - - entity_registry = er.async_get(hass) - device_registry = dr.async_get(hass) - - if source_entity_id: - source_entity_entry = entity_registry.async_get(source_entity_id) - if not source_entity_entry: - _LOGGER.error( - "Entity %s not found", - source_entity_id, - ) - return - - # entity_id is the associated entity, now need to find the config entry for battery notes - for config_entry in hass.config_entries.async_entries(DOMAIN): - if config_entry.data.get("source_entity_id") == source_entity_id: - config_entry_id = config_entry.entry_id - - coordinator = ( - hass.data[DOMAIN][DATA].devices[config_entry_id].coordinator - ) - - coordinator.last_replaced =datetime_replaced - await coordinator.async_request_refresh() - - _LOGGER.debug( - "Entity %s battery replaced on %s", - source_entity_id, - str(datetime_replaced), - ) - - hass.bus.async_fire( - EVENT_BATTERY_REPLACED, - { - ATTR_DEVICE_ID: coordinator.device_id or "", - ATTR_SOURCE_ENTITY_ID: coordinator.source_entity_id - or "", - ATTR_DEVICE_NAME: coordinator.device_name, - ATTR_BATTERY_TYPE_AND_QUANTITY: coordinator.battery_type_and_quantity, - ATTR_BATTERY_TYPE: coordinator.battery_type, - ATTR_BATTERY_QUANTITY: coordinator.battery_quantity, - }, - ) - - _LOGGER.debug( - "Raised event battery replaced %s", - coordinator.device_id, - ) - - return - - _LOGGER.error("Entity %s not configured in Battery Notes", source_entity_id) - - else: - device_entry = device_registry.async_get(device_id) - if not device_entry: - _LOGGER.error( - "Device %s not found", - device_id, - ) - return - - for entry_id in device_entry.config_entries: - if ( - entry := hass.config_entries.async_get_entry(entry_id) - ) and entry.domain == DOMAIN: - coordinator = ( - hass.data[DOMAIN][DATA].devices[entry.entry_id].coordinator - ) - - coordinator.last_replaced =datetime_replaced - - await coordinator.async_request_refresh() - - _LOGGER.debug( - "Device %s battery replaced on %s", - device_id, - str(datetime_replaced), - ) - - hass.bus.async_fire( - EVENT_BATTERY_REPLACED, - { - ATTR_DEVICE_ID: coordinator.device_id or "", - ATTR_SOURCE_ENTITY_ID: coordinator.source_entity_id - or "", - ATTR_DEVICE_NAME: coordinator.device_name, - ATTR_BATTERY_TYPE_AND_QUANTITY: coordinator.battery_type_and_quantity, - ATTR_BATTERY_TYPE: coordinator.battery_type, - ATTR_BATTERY_QUANTITY: coordinator.battery_quantity, - }, - ) - - _LOGGER.debug( - "Raised event battery replaced %s", - coordinator.device_id, - ) - - # Found and dealt with, exit - return - - _LOGGER.error( - "Device %s not configured in Battery Notes", - device_id, - ) - - - async def handle_battery_last_reported(call): - """Handle the service call.""" - days_last_reported = call.data.get(SERVICE_DATA_DAYS_LAST_REPORTED) - - device: BatteryNotesDevice - for device in hass.data[DOMAIN][DATA].devices.values(): - if device.coordinator.wrapped_battery and device.coordinator.last_reported: - time_since_lastreported = ( - datetime.fromisoformat(str(datetime.utcnow()) + "+00:00") - - device.coordinator.last_reported - ) - - if time_since_lastreported.days > days_last_reported: - hass.bus.async_fire( - EVENT_BATTERY_NOT_REPORTED, - { - ATTR_DEVICE_ID: device.coordinator.device_id or "", - ATTR_SOURCE_ENTITY_ID: device.coordinator.source_entity_id - or "", - ATTR_DEVICE_NAME: device.coordinator.device_name, - ATTR_BATTERY_TYPE_AND_QUANTITY: device.coordinator.battery_type_and_quantity, - ATTR_BATTERY_TYPE: device.coordinator.battery_type, - ATTR_BATTERY_QUANTITY: device.coordinator.battery_quantity, - ATTR_BATTERY_LAST_REPORTED: device.coordinator.last_reported, - ATTR_BATTERY_LAST_REPORTED_DAYS: time_since_lastreported.days, - ATTR_BATTERY_LAST_REPORTED_LEVEL: device.coordinator.last_reported_level, - }, - ) - - _LOGGER.debug( - "Raised event device %s not reported since %s", - device.coordinator.device_id, - str(device.coordinator.last_reported), - ) - - async def handle_battery_low(call): - """Handle the service call.""" - - device: BatteryNotesDevice - for device in hass.data[DOMAIN][DATA].devices.values(): - if device.coordinator.battery_low is True: - hass.bus.async_fire( - EVENT_BATTERY_THRESHOLD, - { - ATTR_DEVICE_ID: device.coordinator.device_id or "", - ATTR_DEVICE_NAME: device.coordinator.device_name, - ATTR_SOURCE_ENTITY_ID: device.coordinator.source_entity_id - or "", - ATTR_BATTERY_LOW: device.coordinator.battery_low, - ATTR_BATTERY_TYPE_AND_QUANTITY: device.coordinator.battery_type_and_quantity, - ATTR_BATTERY_TYPE: device.coordinator.battery_type, - ATTR_BATTERY_QUANTITY: device.coordinator.battery_quantity, - ATTR_BATTERY_LEVEL: device.coordinator.rounded_battery_level, - ATTR_PREVIOUS_BATTERY_LEVEL: device.coordinator.rounded_previous_battery_level, - ATTR_BATTERY_THRESHOLD_REMINDER: True, - }, - ) - - _LOGGER.debug( - "Raised event device %s battery low", - device.coordinator.device_id, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_BATTERY_REPLACED, - handle_battery_replaced, - schema=SERVICE_BATTERY_REPLACED_SCHEMA, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_CHECK_BATTERY_LAST_REPORTED, - handle_battery_last_reported, - schema=SERVICE_CHECK_BATTERY_LAST_REPORTED_SCHEMA, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_CHECK_BATTERY_LOW, - handle_battery_low, - ) diff --git a/custom_components/battery_notes/services.py b/custom_components/battery_notes/services.py new file mode 100644 index 00000000..91b72b3e --- /dev/null +++ b/custom_components/battery_notes/services.py @@ -0,0 +1,252 @@ +"""Define services for the Battery Notes integration.""" + +import logging +from datetime import datetime + +from homeassistant.core import ( + HomeAssistant, + ServiceCall, + ServiceResponse, +) +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util + +from .const import ( + ATTR_BATTERY_LAST_REPORTED, + ATTR_BATTERY_LAST_REPORTED_DAYS, + ATTR_BATTERY_LAST_REPORTED_LEVEL, + ATTR_BATTERY_LEVEL, + ATTR_BATTERY_LOW, + ATTR_BATTERY_QUANTITY, + ATTR_BATTERY_THRESHOLD_REMINDER, + ATTR_BATTERY_TYPE, + ATTR_BATTERY_TYPE_AND_QUANTITY, + ATTR_DEVICE_ID, + ATTR_DEVICE_NAME, + ATTR_PREVIOUS_BATTERY_LEVEL, + ATTR_SOURCE_ENTITY_ID, + DATA, + DOMAIN, + EVENT_BATTERY_NOT_REPORTED, + EVENT_BATTERY_REPLACED, + EVENT_BATTERY_THRESHOLD, + SERVICE_BATTERY_REPLACED, + SERVICE_BATTERY_REPLACED_SCHEMA, + SERVICE_CHECK_BATTERY_LAST_REPORTED, + SERVICE_CHECK_BATTERY_LAST_REPORTED_SCHEMA, + SERVICE_CHECK_BATTERY_LOW, + SERVICE_DATA_DATE_TIME_REPLACED, + SERVICE_DATA_DAYS_LAST_REPORTED, +) +from .device import BatteryNotesDevice + +_LOGGER = logging.getLogger(__name__) + +def setup_services(hass: HomeAssistant) -> None: + """Set up the services used by battery notes component.""" + + async def handle_battery_replaced(call: ServiceCall) -> ServiceResponse: + """Handle the service call.""" + device_id = call.data.get(ATTR_DEVICE_ID, "") + source_entity_id = call.data.get(ATTR_SOURCE_ENTITY_ID, "") + datetime_replaced_entry = call.data.get(SERVICE_DATA_DATE_TIME_REPLACED) + + if datetime_replaced_entry: + datetime_replaced = dt_util.as_utc(datetime_replaced_entry).replace( + tzinfo=None + ) + else: + datetime_replaced = datetime.utcnow() + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + if source_entity_id: + source_entity_entry = entity_registry.async_get(source_entity_id) + if not source_entity_entry: + _LOGGER.error( + "Entity %s not found", + source_entity_id, + ) + return None + + # entity_id is the associated entity, now need to find the config entry for battery notes + for config_entry in hass.config_entries.async_entries(DOMAIN): + if config_entry.data.get("source_entity_id") == source_entity_id: + config_entry_id = config_entry.entry_id + + coordinator = ( + hass.data[DOMAIN][DATA].devices[config_entry_id].coordinator + ) + + coordinator.last_replaced =datetime_replaced + await coordinator.async_request_refresh() + + _LOGGER.debug( + "Entity %s battery replaced on %s", + source_entity_id, + str(datetime_replaced), + ) + + hass.bus.async_fire( + EVENT_BATTERY_REPLACED, + { + ATTR_DEVICE_ID: coordinator.device_id or "", + ATTR_SOURCE_ENTITY_ID: coordinator.source_entity_id + or "", + ATTR_DEVICE_NAME: coordinator.device_name, + ATTR_BATTERY_TYPE_AND_QUANTITY: coordinator.battery_type_and_quantity, + ATTR_BATTERY_TYPE: coordinator.battery_type, + ATTR_BATTERY_QUANTITY: coordinator.battery_quantity, + }, + ) + + _LOGGER.debug( + "Raised event battery replaced %s", + coordinator.device_id, + ) + + return None + + _LOGGER.error("Entity %s not configured in Battery Notes", source_entity_id) + + else: + device_entry = device_registry.async_get(device_id) + if not device_entry: + _LOGGER.error( + "Device %s not found", + device_id, + ) + return None + + for entry_id in device_entry.config_entries: + if ( + entry := hass.config_entries.async_get_entry(entry_id) + ) and entry.domain == DOMAIN: + coordinator = ( + hass.data[DOMAIN][DATA].devices[entry.entry_id].coordinator + ) + + coordinator.last_replaced =datetime_replaced + + await coordinator.async_request_refresh() + + _LOGGER.debug( + "Device %s battery replaced on %s", + device_id, + str(datetime_replaced), + ) + + hass.bus.async_fire( + EVENT_BATTERY_REPLACED, + { + ATTR_DEVICE_ID: coordinator.device_id or "", + ATTR_SOURCE_ENTITY_ID: coordinator.source_entity_id + or "", + ATTR_DEVICE_NAME: coordinator.device_name, + ATTR_BATTERY_TYPE_AND_QUANTITY: coordinator.battery_type_and_quantity, + ATTR_BATTERY_TYPE: coordinator.battery_type, + ATTR_BATTERY_QUANTITY: coordinator.battery_quantity, + }, + ) + + _LOGGER.debug( + "Raised event battery replaced %s", + coordinator.device_id, + ) + + # Found and dealt with, exit + return None + + _LOGGER.error( + "Device %s not configured in Battery Notes", + device_id, + ) + return None + + + async def handle_battery_last_reported(call: ServiceCall) -> ServiceResponse: + """Handle the service call.""" + days_last_reported = call.data.get(SERVICE_DATA_DAYS_LAST_REPORTED) + + device: BatteryNotesDevice + for device in hass.data[DOMAIN][DATA].devices.values(): + if device.coordinator.wrapped_battery and device.coordinator.last_reported: + time_since_lastreported = ( + datetime.fromisoformat(str(datetime.utcnow()) + "+00:00") + - device.coordinator.last_reported + ) + + if time_since_lastreported.days > days_last_reported: + hass.bus.async_fire( + EVENT_BATTERY_NOT_REPORTED, + { + ATTR_DEVICE_ID: device.coordinator.device_id or "", + ATTR_SOURCE_ENTITY_ID: device.coordinator.source_entity_id + or "", + ATTR_DEVICE_NAME: device.coordinator.device_name, + ATTR_BATTERY_TYPE_AND_QUANTITY: device.coordinator.battery_type_and_quantity, + ATTR_BATTERY_TYPE: device.coordinator.battery_type, + ATTR_BATTERY_QUANTITY: device.coordinator.battery_quantity, + ATTR_BATTERY_LAST_REPORTED: device.coordinator.last_reported, + ATTR_BATTERY_LAST_REPORTED_DAYS: time_since_lastreported.days, + ATTR_BATTERY_LAST_REPORTED_LEVEL: device.coordinator.last_reported_level, + }, + ) + + _LOGGER.debug( + "Raised event device %s not reported since %s", + device.coordinator.device_id, + str(device.coordinator.last_reported), + ) + return None + + async def handle_battery_low(call: ServiceCall) -> ServiceResponse: + """Handle the service call.""" + + device: BatteryNotesDevice + for device in hass.data[DOMAIN][DATA].devices.values(): + if device.coordinator.battery_low is True: + hass.bus.async_fire( + EVENT_BATTERY_THRESHOLD, + { + ATTR_DEVICE_ID: device.coordinator.device_id or "", + ATTR_DEVICE_NAME: device.coordinator.device_name, + ATTR_SOURCE_ENTITY_ID: device.coordinator.source_entity_id + or "", + ATTR_BATTERY_LOW: device.coordinator.battery_low, + ATTR_BATTERY_TYPE_AND_QUANTITY: device.coordinator.battery_type_and_quantity, + ATTR_BATTERY_TYPE: device.coordinator.battery_type, + ATTR_BATTERY_QUANTITY: device.coordinator.battery_quantity, + ATTR_BATTERY_LEVEL: device.coordinator.rounded_battery_level, + ATTR_PREVIOUS_BATTERY_LEVEL: device.coordinator.rounded_previous_battery_level, + ATTR_BATTERY_THRESHOLD_REMINDER: True, + }, + ) + + _LOGGER.debug( + "Raised event device %s battery low", + device.coordinator.device_id, + ) + return None + + hass.services.async_register( + DOMAIN, + SERVICE_BATTERY_REPLACED, + handle_battery_replaced, + schema=SERVICE_BATTERY_REPLACED_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_CHECK_BATTERY_LAST_REPORTED, + handle_battery_last_reported, + schema=SERVICE_CHECK_BATTERY_LAST_REPORTED_SCHEMA, + ) + + hass.services.async_register( + DOMAIN, + SERVICE_CHECK_BATTERY_LOW, + handle_battery_low, + ) From 41a554d8e0ccf2f47fce99bf4e453bd1c4049308 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 2 Nov 2024 06:46:54 +0000 Subject: [PATCH 2/4] Update device: H5126 by Govee (#2282) --- custom_components/battery_notes/data/library.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/custom_components/battery_notes/data/library.json b/custom_components/battery_notes/data/library.json index 8dffcdc2..254ddf11 100644 --- a/custom_components/battery_notes/data/library.json +++ b/custom_components/battery_notes/data/library.json @@ -6769,6 +6769,11 @@ "model": "ZSE70", "battery_type": "CR123A", "battery_quantity": 2 + }, + { + "manufacturer": "Govee", + "model": "H5126", + "battery_type": "CR2032" } ] } \ No newline at end of file From b9f795a8a0ee3526dcd5321355c43958c88b97ef Mon Sep 17 00:00:00 2001 From: andrew-codechimp Date: Sat, 2 Nov 2024 06:47:13 +0000 Subject: [PATCH 3/4] Apply automatic changes --- custom_components/battery_notes/data/library.json | 10 +++++----- library.md | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/custom_components/battery_notes/data/library.json b/custom_components/battery_notes/data/library.json index 254ddf11..e438f073 100644 --- a/custom_components/battery_notes/data/library.json +++ b/custom_components/battery_notes/data/library.json @@ -2444,6 +2444,11 @@ "model": "H5123", "battery_type": "CR1632" }, + { + "manufacturer": "Govee", + "model": "H5126", + "battery_type": "CR2032" + }, { "manufacturer": "Govee", "model": "H5177", @@ -6769,11 +6774,6 @@ "model": "ZSE70", "battery_type": "CR123A", "battery_quantity": 2 - }, - { - "manufacturer": "Govee", - "model": "H5126", - "battery_type": "CR2032" } ] } \ No newline at end of file diff --git a/library.md b/library.md index 4bd0d970..d556e2ef 100644 --- a/library.md +++ b/library.md @@ -1,4 +1,4 @@ -## 1241 Devices in library +## 1242 Devices in library This file is auto generated, do not modify @@ -450,6 +450,7 @@ Request new devices to be added to the library [here](https://github.com/andrew- |Govee |H5121 |CR2450 | | | |Govee |H5122 |CR2032 | | | |Govee |H5123 |CR1632 | | | +|Govee |H5126 |CR2032 | | | |Govee |H5177 |3× AAA | | | |Govee |H5179 |3× AA | | | |GoveeLife |H5105 |CR2450 | | | From d073bef1f17a94791f740c27ecb38a75ab2a09ad Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Sat, 2 Nov 2024 09:54:04 +0000 Subject: [PATCH 4/4] Fix last_replaced dates #2280 --- custom_components/battery_notes/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/battery_notes/device.py b/custom_components/battery_notes/device.py index bdc127c1..70540bb6 100644 --- a/custom_components/battery_notes/device.py +++ b/custom_components/battery_notes/device.py @@ -193,13 +193,13 @@ async def async_setup(self) -> bool: if entity.device_id: device_entry = device_registry.async_get(entity.device_id) - if device_entry.created_at.year > 1970: + if device_entry and device_entry.created_at.year > 1970: last_replaced = device_entry.created_at.strftime( "%Y-%m-%dT%H:%M:%S:%f" ) else: entity = entity_registry.async_get(source_entity_id) - if entity.created_at.year > 1970: + if entity and entity.created_at.year > 1970: last_replaced = entity.created_at.strftime("%Y-%m-%dT%H:%M:%S:%f") _LOGGER.debug(