diff --git a/custom_components/battery_notes/__init__.py b/custom_components/battery_notes/__init__.py index d54d4ba8e..94e9302eb 100644 --- a/custom_components/battery_notes/__init__.py +++ b/custom_components/battery_notes/__init__.py @@ -52,9 +52,20 @@ SERVICE_BATTERY_REPLACED, SERVICE_BATTERY_REPLACED_SCHEMA, SERVICE_DATA_DATE_TIME_REPLACED, + SERVICE_CHECK_BATTERY_LAST_REPORTED, + SERVICE_DATA_DAYS_LAST_REPORTED, + SERVICE_CHECK_BATTERY_LAST_REPORTED_SCHEMA, + EVENT_BATTERY_NOT_REPORTED, DATA_STORE, ATTR_REMOVE, ATTR_DEVICE_ID, + ATTR_DEVICE_NAME, + ATTR_BATTERY_TYPE_AND_QUANTITY, + ATTR_BATTERY_TYPE, + ATTR_BATTERY_QUANTITY, + ATTR_BATTERY_LAST_REPORTED, + ATTR_BATTERY_LAST_REPORTED_DAYS, + ATTR_BATTERY_LAST_REPORTED_LEVEL, CONF_BATTERY_TYPE, CONF_BATTERY_QUANTITY, ) @@ -311,9 +322,48 @@ async def handle_battery_replaced(call): 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.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, + 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), + ) + + 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, + ) diff --git a/custom_components/battery_notes/binary_sensor.py b/custom_components/battery_notes/binary_sensor.py index b2f25c724..b15e4a42b 100644 --- a/custom_components/battery_notes/binary_sensor.py +++ b/custom_components/battery_notes/binary_sensor.py @@ -1,20 +1,41 @@ """Binary Sensor platform for battery_notes.""" from __future__ import annotations +from collections.abc import Callable from dataclasses import dataclass +from typing import Any import logging 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.core import ( + HomeAssistant, + callback, + Event, +) +from homeassistant.exceptions import TemplateError +from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.start import async_at_start from homeassistant.helpers import ( config_validation as cv, device_registry as dr, entity_registry as er, ) +from homeassistant.helpers.event import ( + EventStateChangedData, + TrackTemplate, + TrackTemplateResult, + async_track_template_result, +) + +from homeassistant.helpers import template +from homeassistant.helpers.template import ( + Template, + TemplateStateFromEntityId, +) from homeassistant.components.binary_sensor import ( PLATFORM_SCHEMA, BinarySensorEntity, @@ -29,6 +50,7 @@ async_track_entity_registry_updated_event, ) from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.typing import EventType from homeassistant.const import ( CONF_NAME, @@ -47,6 +69,7 @@ from .common import isfloat +from .device import BatteryNotesDevice from .coordinator import BatteryNotesCoordinator from .entity import ( @@ -122,7 +145,7 @@ async def async_registry_updated(event: Event) -> None: device_id, remove_config_entry_id=config_entry.entry_id ) - coordinator = hass.data[DOMAIN][DATA].devices[config_entry.entry_id].coordinator + coordinator: BatteryNotesCoordinator = hass.data[DOMAIN][DATA].devices[config_entry.entry_id].coordinator config_entry.async_on_unload( async_track_entity_registry_updated_event( @@ -144,9 +167,22 @@ async def async_registry_updated(event: Event) -> None: device_class=BinarySensorDeviceClass.BATTERY, ) - device = hass.data[DOMAIN][DATA].devices[config_entry.entry_id] + device: BatteryNotesDevice = hass.data[DOMAIN][DATA].devices[config_entry.entry_id] + + if coordinator.battery_low_template is not None: + async_add_entities( + [ + BatteryNotesBatteryLowTemplateSensor( + hass, + coordinator, + description, + f"{config_entry.entry_id}{description.unique_id_suffix}", + coordinator.battery_low_template, + ) + ] + ) - if device.wrapped_battery is not None: + elif device.wrapped_battery is not None: async_add_entities( [ BatteryNotesBatteryLowSensor( @@ -166,6 +202,284 @@ async def async_setup_platform( await async_setup_reload_service(hass, DOMAIN, PLATFORMS) +class _TemplateAttribute: + """Attribute value linked to template result.""" + + def __init__( + self, + entity: Entity, + attribute: str, + template: Template, + validator: Callable[[Any], Any] | None = None, + on_update: Callable[[Any], None] | None = None, + none_on_template_error: bool | None = False, + ) -> None: + """Template attribute.""" + self._entity = entity + self._attribute = attribute + self.template = template + self.validator = validator + self.on_update = on_update + self.async_update = None + self.none_on_template_error = none_on_template_error + + @callback + def async_setup(self) -> None: + """Config update path for the attribute.""" + if self.on_update: + return + + if not hasattr(self._entity, self._attribute): + raise AttributeError(f"Attribute '{self._attribute}' does not exist.") + + self.on_update = self._default_update + + @callback + def _default_update(self, result: str | TemplateError) -> None: + attr_result = None if isinstance(result, TemplateError) else result + setattr(self._entity, self._attribute, attr_result) + + @callback + def handle_result( + self, + event: EventType[EventStateChangedData] | None, + template: Template, + last_result: str | None | TemplateError, + result: str | TemplateError, + ) -> None: + """Handle a template result event callback.""" + if isinstance(result, TemplateError): + _LOGGER.error( + ( + "TemplateError('%s') " + "while processing template '%s' " + "for attribute '%s' in entity '%s'" + ), + result, + self.template, + self._attribute, + self._entity.entity_id, + ) + if self.none_on_template_error: + self._default_update(result) + else: + assert self.on_update + self.on_update(result) + return + + if not self.validator: + assert self.on_update + self.on_update(result) + return + + try: + validated = self.validator(result) + except vol.Invalid as ex: + _LOGGER.error( + ( + "Error validating template result '%s' " + "from template '%s' " + "for attribute '%s' in entity %s " + "validation message '%s'" + ), + result, + self.template, + self._attribute, + self._entity.entity_id, + ex.msg, + ) + assert self.on_update + self.on_update(None) + return + + assert self.on_update + self.on_update(validated) + return + +class BatteryNotesBatteryLowTemplateSensor(BinarySensorEntity, CoordinatorEntity[BatteryNotesCoordinator]): + """Represents a low battery threshold binary sensor.""" + + _attr_should_poll = False + + def __init__( + self, + hass: HomeAssistant, + coordinator: BatteryNotesCoordinator, + description: BatteryNotesBinarySensorEntityDescription, + unique_id: str, + template: str, + ) -> None: + """Create a low battery binary sensor.""" + + device_registry = dr.async_get(hass) + + self.coordinator = coordinator + self.entity_description = description + self._attr_unique_id = unique_id + self._attr_has_entity_name = True + self._template_attrs: dict[Template, list[_TemplateAttribute]] = {} + + super().__init__(coordinator=coordinator) + + if coordinator.device_id and ( + device_entry := device_registry.async_get(coordinator.device_id) + ): + self._attr_device_info = DeviceInfo( + connections=device_entry.connections, + identifiers=device_entry.identifiers, + ) + + self.entity_id = f"binary_sensor.{coordinator.device_name.lower()}_{description.key}" + + self._template = template + self._state: bool | None = None + + async def async_added_to_hass(self) -> None: + """Handle added to Hass.""" + + await super().async_added_to_hass() + + self._async_setup_templates() + + async_at_start(self.hass, self._async_template_startup) + + def add_template_attribute( + self, + attribute: str, + template: Template, + validator: Callable[[Any], Any] | None = None, + on_update: Callable[[Any], None] | None = None, + none_on_template_error: bool = False, + ) -> None: + """Call in the constructor to add a template linked to a attribute. + + Parameters + ---------- + attribute + The name of the attribute to link to. This attribute must exist + unless a custom on_update method is supplied. + template + The template to calculate. + validator + Validator function to parse the result and ensure it's valid. + on_update + Called to store the template result rather than storing it + the supplied attribute. Passed the result of the validator, or None + if the template or validator resulted in an error. + none_on_template_error + If True, the attribute will be set to None if the template errors. + + """ + assert self.hass is not None, "hass cannot be None" + template.hass = self.hass + template_attribute = _TemplateAttribute( + self, attribute, template, validator, on_update, none_on_template_error + ) + self._template_attrs.setdefault(template, []) + self._template_attrs[template].append(template_attribute) + + @callback + def _async_setup_templates(self) -> None: + """Set up templates.""" + self.add_template_attribute("_state", Template(self._template), None, self._update_state) + + @callback + def _async_template_startup( + self, + _hass: HomeAssistant | None, + log_fn: Callable[[int, str], None] | None = None, + ) -> None: + template_var_tups: list[TrackTemplate] = [] + has_availability_template = False + + variables = {"this": TemplateStateFromEntityId(self.hass, self.entity_id)} + + for loop_template, attributes in self._template_attrs.items(): + template_var_tup = TrackTemplate(loop_template, variables) + is_availability_template = False + for attribute in attributes: + # pylint: disable-next=protected-access + if attribute._attribute == "_attr_available": + has_availability_template = True + is_availability_template = True + attribute.async_setup() + # Insert the availability template first in the list + if is_availability_template: + template_var_tups.insert(0, template_var_tup) + else: + template_var_tups.append(template_var_tup) + + result_info = async_track_template_result( + self.hass, + template_var_tups, + self._handle_results, + log_fn=log_fn, + has_super_template=has_availability_template, + ) + self.async_on_remove(result_info.async_remove) + self._template_result_info = result_info + result_info.async_refresh() + + @callback + def _handle_results( + self, + event: EventType[EventStateChangedData] | None, + updates: list[TrackTemplateResult], + ) -> None: + """Call back the results to the attributes.""" + if event: + self.async_set_context(event.context) + + entity_id = event and event.data["entity_id"] + + if entity_id and entity_id == self.entity_id: + self._self_ref_update_count += 1 + else: + self._self_ref_update_count = 0 + + if self._self_ref_update_count > len(self._template_attrs): + for update in updates: + _LOGGER.warning( + ( + "Template loop detected while processing event: %s, skipping" + " template render for Template[%s]" + ), + event, + update.template.template, + ) + return + + for update in updates: + for template_attr in self._template_attrs[update.template]: + template_attr.handle_result( + event, update.template, update.last_result, update.result + ) + + self.async_write_ha_state() + return + + + @callback + def _update_state(self, result): + + state = ( + None + if isinstance(result, TemplateError) + else template.result_as_boolean(result) + ) + + if state == self._state: + return + + self._state = state + self.coordinator.battery_low_template_state = state + _LOGGER.debug("%s binary sensor battery_low set to: %s via template", self.entity_id, state) + + + @property + def is_on(self) -> bool | None: + """Return true if sensor is on.""" + return self._state class BatteryNotesBatteryLowSensor(BinarySensorEntity, CoordinatorEntity[BatteryNotesCoordinator]): """Represents a low battery threshold binary sensor.""" diff --git a/custom_components/battery_notes/config_flow.py b/custom_components/battery_notes/config_flow.py index 044d76ab8..81b4448b9 100644 --- a/custom_components/battery_notes/config_flow.py +++ b/custom_components/battery_notes/config_flow.py @@ -37,6 +37,7 @@ DATA_LIBRARY_UPDATER, DOMAIN_CONFIG, CONF_SHOW_ALL_DEVICES, + CONF_BATTERY_LOW_TEMPLATE, ) _LOGGER = logging.getLogger(__name__) @@ -50,7 +51,7 @@ ), vol.Optional(CONF_NAME): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT), - ), + ) } ) @@ -72,7 +73,7 @@ ), vol.Optional(CONF_NAME): selector.TextSelector( selector.TextSelectorConfig(type=selector.TextSelectorType.TEXT), - ), + ) } ) @@ -228,6 +229,7 @@ async def async_step_battery(self, user_input: dict[str, Any] | None = None): min=0, max=99, mode=selector.NumberSelectorMode.BOX ), ), + vol.Optional(CONF_BATTERY_LOW_TEMPLATE): selector.TemplateSelector() } ), errors=errors, @@ -245,6 +247,7 @@ def __init__(self, config_entry: ConfigEntry) -> None: self.name: str = self.current_config.get(CONF_NAME) self.battery_type: str = self.current_config.get(CONF_BATTERY_TYPE) self.battery_quantity: int = self.current_config.get(CONF_BATTERY_QUANTITY) + self.battery_low_template: str = self.current_config.get(CONF_BATTERY_LOW_TEMPLATE) async def async_step_init( self, @@ -328,6 +331,7 @@ def build_options_schema(self) -> vol.Schema: min=0, max=99, mode=selector.NumberSelectorMode.BOX ), ), + vol.Optional(CONF_BATTERY_LOW_TEMPLATE): selector.TemplateSelector() } ) diff --git a/custom_components/battery_notes/const.py b/custom_components/battery_notes/const.py index 78cdc51d5..e68e799fd 100644 --- a/custom_components/battery_notes/const.py +++ b/custom_components/battery_notes/const.py @@ -45,6 +45,7 @@ CONF_BATTERY_INCREASE_THRESHOLD = "battery_increase_threshold" CONF_HIDE_BATTERY = "hide_battery" CONF_ROUND_BATTERY = "round_battery" +CONF_BATTERY_LOW_TEMPLATE = "battery_low_template" DATA_CONFIGURED_ENTITIES = "configured_entities" DATA_DISCOVERED_ENTITIES = "discovered_entities" @@ -59,8 +60,12 @@ SERVICE_BATTERY_REPLACED = "set_battery_replaced" SERVICE_DATA_DATE_TIME_REPLACED = "datetime_replaced" +SERVICE_CHECK_BATTERY_LAST_REPORTED = "check_battery_last_reported" +SERVICE_DATA_DAYS_LAST_REPORTED = "days_last_reported" + EVENT_BATTERY_THRESHOLD = "battery_notes_battery_threshold" EVENT_BATTERY_INCREASED = "battery_notes_battery_increased" +EVENT_BATTERY_NOT_REPORTED = "battery_notes_battery_not_reported" ATTR_DEVICE_ID = "device_id" ATTR_REMOVE = "remove" @@ -73,6 +78,7 @@ ATTR_DEVICE_NAME = "device_name" ATTR_BATTERY_LEVEL = "battery_level" ATTR_BATTERY_LAST_REPORTED = "battery_last_reported" +ATTR_BATTERY_LAST_REPORTED_DAYS = "battery_last_reported_days" ATTR_BATTERY_LAST_REPORTED_LEVEL = "battery_last_reported_level" ATTR_PREVIOUS_BATTERY_LEVEL = "previous_battery_level" @@ -83,6 +89,12 @@ } ) +SERVICE_CHECK_BATTERY_LAST_REPORTED_SCHEMA = vol.Schema( + { + vol.Required(SERVICE_DATA_DAYS_LAST_REPORTED): cv.positive_int, + } +) + PLATFORMS: Final = [ Platform.BUTTON, Platform.SENSOR, diff --git a/custom_components/battery_notes/coordinator.py b/custom_components/battery_notes/coordinator.py index 6988510ad..3579ad1e4 100644 --- a/custom_components/battery_notes/coordinator.py +++ b/custom_components/battery_notes/coordinator.py @@ -53,12 +53,15 @@ class BatteryNotesCoordinator(DataUpdateCoordinator): battery_type: str battery_quantity: int battery_low_threshold: int + battery_low_template: str wrapped_battery: RegistryEntry _current_battery_level: str = None enable_replaced: bool = True _round_battery: bool = False _previous_battery_low: bool = None _previous_battery_level: str = None + _battery_low_template_state: bool = False + _previous_battery_low_template_state: bool = None def __init__( self, hass, store: BatteryNotesStorage, wrapped_battery: RegistryEntry @@ -74,6 +77,50 @@ def __init__( super().__init__(hass, _LOGGER, name=DOMAIN) + @property + def battery_low_template_state(self): + """Get the current battery low status from a templated device.""" + return self._battery_low_template_state + + @battery_low_template_state.setter + def battery_low_template_state(self, value): + """Set the current battery low status from a templated device and fire events if valid.""" + self._battery_low_template_state = value + if self._previous_battery_low_template_state is not None and self.battery_low_template: + self.hass.bus.async_fire( + EVENT_BATTERY_THRESHOLD, + { + ATTR_DEVICE_ID: self.device_id, + ATTR_DEVICE_NAME: self.device_name, + ATTR_BATTERY_LOW: self.battery_low, + ATTR_BATTERY_TYPE_AND_QUANTITY: self.battery_type_and_quantity, + ATTR_BATTERY_TYPE: self.battery_type, + ATTR_BATTERY_QUANTITY: self.battery_quantity, + }, + ) + + _LOGGER.debug("battery_threshold event fired Low: %s via template", self.battery_low) + + if ( + self._previous_battery_low_template_state + and not self._battery_low_template_state + ): + self.hass.bus.async_fire( + EVENT_BATTERY_INCREASED, + { + ATTR_DEVICE_ID: self.device_id, + ATTR_DEVICE_NAME: self.device_name, + ATTR_BATTERY_LOW: self.battery_low, + ATTR_BATTERY_TYPE_AND_QUANTITY: self.battery_type_and_quantity, + ATTR_BATTERY_TYPE: self.battery_type, + ATTR_BATTERY_QUANTITY: self.battery_quantity, + }, + ) + + _LOGGER.debug("battery_increased event fired via template") + + self._previous_battery_low_template_state = value + @property def current_battery_level(self): """Get the current battery level.""" @@ -84,7 +131,7 @@ def current_battery_level(self, value): """Set the current battery level and fire events if valid.""" self._current_battery_level = value - if self._previous_battery_level is not None: + if self._previous_battery_level is not None and self.battery_low_template is None: # Battery low event if self.battery_low != self._previous_battery_low: self.hass.bus.async_fire( @@ -204,10 +251,13 @@ def last_reported_level(self, value): @property def battery_low(self) -> bool: """Check if battery low against threshold.""" - if isfloat(self.current_battery_level): - return bool( - float(self.current_battery_level) < self.battery_low_threshold - ) + if self.battery_low_template: + return self.battery_low_template_state + else: + if isfloat(self.current_battery_level): + return bool( + float(self.current_battery_level) < self.battery_low_threshold + ) return False diff --git a/custom_components/battery_notes/device.py b/custom_components/battery_notes/device.py index d7d0ae688..ffaffd3fd 100644 --- a/custom_components/battery_notes/device.py +++ b/custom_components/battery_notes/device.py @@ -30,6 +30,7 @@ CONF_BATTERY_QUANTITY, CONF_BATTERY_LOW_THRESHOLD, CONF_DEFAULT_BATTERY_LOW_THRESHOLD, + CONF_BATTERY_LOW_TEMPLATE, DEFAULT_BATTERY_LOW_THRESHOLD, ) @@ -134,6 +135,8 @@ async def async_setup(self) -> bool: CONF_DEFAULT_BATTERY_LOW_THRESHOLD, DEFAULT_BATTERY_LOW_THRESHOLD ) + self.coordinator.battery_low_template = config.data.get(CONF_BATTERY_LOW_TEMPLATE) + if self.wrapped_battery: _LOGGER.debug( "%s low threshold set at %d", diff --git a/custom_components/battery_notes/icons.json b/custom_components/battery_notes/icons.json index 204d1f52b..12e03b769 100644 --- a/custom_components/battery_notes/icons.json +++ b/custom_components/battery_notes/icons.json @@ -1,5 +1,6 @@ { "services": { - "set_battery_replaced": "mdi:battery-sync" + "set_battery_replaced": "mdi:battery-sync", + "check_battery_reported": "mdi:battery-unknown" } } \ No newline at end of file diff --git a/custom_components/battery_notes/services.yaml b/custom_components/battery_notes/services.yaml index 063edfdc7..46a6044e2 100644 --- a/custom_components/battery_notes/services.yaml +++ b/custom_components/battery_notes/services.yaml @@ -1,11 +1,10 @@ - set_battery_replaced: name: Set battery replaced description: "Set the battery last replaced." fields: device_id: name: Device - description: Device that has had it's battery replaced. + description: Device that has had its battery replaced. required: true selector: device: @@ -17,3 +16,16 @@ set_battery_replaced: required: false selector: datetime: +check_battery_last_reported: + name: Check battery reported + description: "Raise events for devices that haven't reported their battery level." + fields: + days_last_reported: + name: Days + description: Number of days since a device last reported its battery level. + required: true + selector: + number: + min: 1 + max: 100 + mode: box diff --git a/custom_components/battery_notes/translations/da.json b/custom_components/battery_notes/translations/da.json index 3a3dc179b..7e65b38f3 100644 --- a/custom_components/battery_notes/translations/da.json +++ b/custom_components/battery_notes/translations/da.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Hvis du har brug for hjælp til konfigurationen, så kig her: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Hvis du har brug for hjælp til konfigurationen, så kig her: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "device_id": "Enhed", "name": "Navn" @@ -34,7 +34,7 @@ "options": { "step": { "init": { - "description": "Hvis du har brug for hjælp til konfigurationen, så kig her: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Hvis du har brug for hjælp til konfigurationen, så kig her: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "name": "Navn", "battery_type": "Batteri type", diff --git a/custom_components/battery_notes/translations/de.json b/custom_components/battery_notes/translations/de.json index 45c9ed97f..36a26c5c3 100644 --- a/custom_components/battery_notes/translations/de.json +++ b/custom_components/battery_notes/translations/de.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Hilfe zur Konfiguration findest du unter: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Hilfe zur Konfiguration findest du unter: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "device_id": "Gerät", "name": "Name" @@ -34,7 +34,7 @@ "options": { "step": { "init": { - "description": "Hilfe zur Konfiguration findest du unter: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Hilfe zur Konfiguration findest du unter: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "name": "Name", "battery_type": "Batterieart", diff --git a/custom_components/battery_notes/translations/en.json b/custom_components/battery_notes/translations/en.json index 474ab97d8..cd0666346 100644 --- a/custom_components/battery_notes/translations/en.json +++ b/custom_components/battery_notes/translations/en.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "If you need help with the configuration have a look here: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "If you need help with the configuration have a look here: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "device_id": "Device", "name": "Name" @@ -15,10 +15,12 @@ "data": { "battery_type": "Battery type", "battery_quantity": "Battery quantity", - "battery_low_threshold": "Battery low threshold" + "battery_low_threshold": "Battery low threshold", + "battery_low_template": "Battery low template" }, "data_description": { - "battery_low_threshold": "0 will use the global default threshold" + "battery_low_threshold": "0 will use the global default threshold", + "battery_low_template": "Template to determine a battery is low, should return true if low\nOnly needed for non-standard battery levels" } } }, @@ -32,16 +34,18 @@ "options": { "step": { "init": { - "description": "If you need help with the configuration have a look here: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "If you need help with the configuration have a look here: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "name": "Name", "battery_type": "Battery type", "battery_quantity": "Battery quantity", - "battery_low_threshold": "Battery low threshold" + "battery_low_threshold": "Battery low threshold", + "battery_low_template": "Battery low template" }, "data_description": { "name": "Leaving blank will take the name from the source device", - "battery_low_threshold": "0 will use the global default threshold" + "battery_low_threshold": "0 will use the global default threshold", + "battery_low_template": "Template to determine a battery is low, should return true if low\nOnly needed for non-standard battery levels" } } }, @@ -125,6 +129,16 @@ } }, "name": "Set battery replaced" + }, + "check_battery_last_reported": { + "description": "Raise events for devices that haven't reported their battery level.", + "fields": { + "days_last_reported": { + "description": "Number of days since a device last reported its battery level.", + "name": "Days" + } + }, + "name": "Check battery last reported" } } -} +} \ No newline at end of file diff --git a/custom_components/battery_notes/translations/fi.json b/custom_components/battery_notes/translations/fi.json index 154fff57d..4c73f0bd0 100644 --- a/custom_components/battery_notes/translations/fi.json +++ b/custom_components/battery_notes/translations/fi.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Jos tarvitset apua asetuksissa, katso täältä: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Jos tarvitset apua asetuksissa, katso täältä: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "device_id": "Laite", "name": "Nimi" @@ -34,7 +34,7 @@ "options": { "step": { "init": { - "description": "Jos tarvitset apua asetuksissa, katso täältä: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Jos tarvitset apua asetuksissa, katso täältä: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "name": "Nimi", "battery_type": "Akun tyyppi", diff --git a/custom_components/battery_notes/translations/fr.json b/custom_components/battery_notes/translations/fr.json index 8e221512f..3192bb139 100644 --- a/custom_components/battery_notes/translations/fr.json +++ b/custom_components/battery_notes/translations/fr.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "En cas de demande d'aide, aller à: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "En cas de demande d'aide, aller à: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "device_id": "Entité", "name": "Nom" @@ -34,7 +34,7 @@ "options": { "step": { "init": { - "description": "En cas de demande d'aide, aller à: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "En cas de demande d'aide, aller à: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "name": "Nom", "battery_type": "Type de batterie", diff --git a/custom_components/battery_notes/translations/hu.json b/custom_components/battery_notes/translations/hu.json index ddd865574..9c687c299 100644 --- a/custom_components/battery_notes/translations/hu.json +++ b/custom_components/battery_notes/translations/hu.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Ha segítségre van szükséged a konfigurációhoz: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Ha segítségre van szükséged a konfigurációhoz: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "device_id": "Eszköz", "name": "Név" @@ -34,7 +34,7 @@ "options": { "step": { "init": { - "description": "Ha segítségre van szükséged a konfigurációhoz: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Ha segítségre van szükséged a konfigurációhoz: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "name": "Név", "battery_type": "Elem típus", diff --git a/custom_components/battery_notes/translations/pl.json b/custom_components/battery_notes/translations/pl.json index 5391bb879..c102943d9 100644 --- a/custom_components/battery_notes/translations/pl.json +++ b/custom_components/battery_notes/translations/pl.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Jeśli potrzebujesz pomocy w konfiguracji, zajrzyj tutaj: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Jeśli potrzebujesz pomocy w konfiguracji, zajrzyj tutaj: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "device_id": "Urządzenie", "name": "Nazwa" @@ -34,7 +34,7 @@ "options": { "step": { "init": { - "description": "Jeśli potrzebujesz pomocy w konfiguracji, zajrzyj tutaj: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Jeśli potrzebujesz pomocy w konfiguracji, zajrzyj tutaj: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "name": "Nazwa", "battery_type": "Typ baterii", diff --git a/custom_components/battery_notes/translations/ru.json b/custom_components/battery_notes/translations/ru.json index 593707bf1..b18760b4d 100644 --- a/custom_components/battery_notes/translations/ru.json +++ b/custom_components/battery_notes/translations/ru.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Если вам нужна помощь с настройкой, посмотрите здесь: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Если вам нужна помощь с настройкой, посмотрите здесь: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "device_id": "Устройство", "name": "Название" @@ -34,7 +34,7 @@ "options": { "step": { "init": { - "description": "Если вам нужна помощь с настройкой, посмотрите здесь: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Если вам нужна помощь с настройкой, посмотрите здесь: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "name": "Название", "battery_type": "Тип батареи", diff --git a/custom_components/battery_notes/translations/sk.json b/custom_components/battery_notes/translations/sk.json index a64437c80..72cbf9788 100644 --- a/custom_components/battery_notes/translations/sk.json +++ b/custom_components/battery_notes/translations/sk.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Ak potrebujete pomoc s konfiguráciou, pozrite sa sem: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Ak potrebujete pomoc s konfiguráciou, pozrite sa sem: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "device_id": "Zariadenie", "name": "Názov" @@ -34,7 +34,7 @@ "options": { "step": { "init": { - "description": "Ak potrebujete pomoc s konfiguráciou, pozrite sa sem: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "Ak potrebujete pomoc s konfiguráciou, pozrite sa sem: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "name": "Názov", "battery_type": "Typ batérie", diff --git a/custom_components/battery_notes/translations/ur.json b/custom_components/battery_notes/translations/ur.json index 36dc08101..89029621e 100644 --- a/custom_components/battery_notes/translations/ur.json +++ b/custom_components/battery_notes/translations/ur.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "اگر آپ کو ترتیب میں مدد کی ضرورت ہو تو یہاں ایک نظر ڈالیں: https://github.com/andrew-codechimp/ha-battery-notes", + "description": "اگر آپ کو ترتیب میں مدد کی ضرورت ہو تو یہاں ایک نظر ڈالیں: https://andrew-codechimp.github.io/HA-Battery-Notes/", "data": { "device_id": "آلہ", "name": "نام" diff --git a/docs/blueprints/battery_notes_battery_not_reported.yaml b/docs/blueprints/battery_notes_battery_not_reported.yaml new file mode 100644 index 000000000..f9ad978b3 --- /dev/null +++ b/docs/blueprints/battery_notes_battery_not_reported.yaml @@ -0,0 +1,53 @@ +blueprint: + name: Battery Notes - Battery Not Reported + description: Actions to perform when the battery not reported event is fired + author: andrew-codechimp + source_url: https://raw.githubusercontent.com/andrew-codechimp/HA-Battery-Notes/main/docs/blueprints/battery_notes_battery_not_reported.yaml + domain: automation + + input: + not_reported_notification: + name: Battery Not Reported Notification + description: Create a persistent notification when the battery is not reported. + default: True + selector: + boolean: + user_actions: + name: User Actions + description: User actions to run on battery not reported. + default: [] + selector: + action: + +variables: + not_reported_notification: !input not_reported_notification + +trigger: + - platform: event + event_type: battery_notes_battery_not_reported + alias: Battery not reported + +condition: [] + +action: + - if: + - condition: template + value_template: "{{ not_reported_notification }}" + then: + - service: persistent_notification.create + data: + title: | + {{ trigger.event.data.device_name }} Battery Not Reported + message: > + The device has not reported its battery level for {{ + trigger.event.data.battery_last_reported_days }} days {{ '\n' + -}} Its last reported level was {{ + trigger.event.data.battery_last_reported_level }}% {{ '\n' -}} You need + {{ trigger.event.data.battery_quantity }}× {{ + trigger.event.data.battery_type }} + - alias: "Run user actions" + choose: [] + default: !input 'user_actions' + +mode: queued +max: 30 diff --git a/docs/community.md b/docs/community.md index f2a280e56..d29b9fd40 100644 --- a/docs/community.md +++ b/docs/community.md @@ -89,6 +89,50 @@ action: mode: queued ``` +### Check Battery Last Reported Daily +Call the check battery last reported service every day to raise events for those not reported in the last two days. +To be used in conjunction with a Battery Not Reported automation. + +```yaml +alias: Daily Battery Not Reported Check +description: Check whether a battery has reported +trigger: + - platform: time + at: "09:00:00" +condition: [] +action: + - service: battery_notes.check_battery_last_reported + data: + days_last_reported: 2 +mode: single +``` + +### Battery Not Reported +Respond to events raised by the check_battery_last_reported service and create notifications. + +```yaml +alias: Battery Not Reported +description: Battery not reported +trigger: + - platform: event + event_type: battery_notes_battery_not_reported +condition: [] +action: + - service: persistent_notification.create + data: + title: | + {{ trigger.event.data.device_name }} Battery Not Reported + message: > + The device has not reported its battery level for {{ + trigger.event.data.battery_last_reported_days }} days {{ '\n' + -}} Its last reported level was {{ + trigger.event.data.battery_last_reported_level }}% {{ '\n' -}} You need + {{ trigger.event.data.battery_quantity }}× {{ + trigger.event.data.battery_type }} +mode: queued +max: 30 +``` + ## Automation Tips To call the battery replaced service from an entity trigger you will need the device_id, here's an easy way to get this @@ -117,5 +161,12 @@ It is extended from the example Battery Low Notification automation yaml above f This blueprint will automatically update the battery replaced sensor and custom actions to be performed when the battery increases. It is extended from the example Battery Replaced automation yaml above for those who'd prefer an easy way to get started. +### Battery Not Reported +[Install blueprint](https://my.home-assistant.io/redirect/blueprint_import/?blueprint_url=https%3A%2F%2Fraw.githubusercontent.com%2Fandrew-codechimp%2FHA-Battery-Notes%2Fmain%2Fdocs%2Fblueprints%2Fbattery_notes_battery_not_reported.yaml) | [Source](./blueprints/battery_notes_battery_not_reported.yaml) + +This blueprint will allow notifications to be raised and/or custom actions to be performed when the battery not reported event is fired. +It is extended from the example Battery Not Reported automation yaml above for those who'd prefer an easy way to get started. +You will want to trigger the check_battery_not_reported service via an automation to raise events, see Check Battery Last Reported Daily above. + ## Contributing If you want to contribute then [fork the repository](https://github.com/andrew-codechimp/HA-Battery-Notes), edit this page which is in the docs folder and submit a pull request. diff --git a/docs/events.md b/docs/events.md index 20d0505fd..a448beae1 100644 --- a/docs/events.md +++ b/docs/events.md @@ -17,8 +17,8 @@ You can use this to send notifications in your preferred method. An example aut | `battery_type_and_quantity` | `string` | Battery type & quantity. | | `battery_type` | `string` | Battery type. | | `battery_quantity` | `int` | Battery quantity. | -| `battery_level` | `int` | Battery level % of the device. | -| `previous_battery_level` | `int` | Previous battery level % of the device. | +| `battery_level` | `float` | Battery level % of the device. | +| `previous_battery_level` | `float` | Previous battery level % of the device. | ### Automation Example @@ -85,8 +85,8 @@ An example automation below shows how to update the battery_replaced. | `battery_type_and_quantity` | `string` | Battery type & quantity. | | `battery_type` | `string` | Battery type. | | `battery_quantity` | `int` | Battery quantity. | -| `battery_level` | `int` | Current battery level % of the device. | -| `previous_battery_level` | `int` | Previous battery level % of the device. | +| `battery_level` | `float` | Current battery level % of the device. | +| `previous_battery_level` | `float` | Previous battery level % of the device. | ### Automation Example @@ -104,4 +104,49 @@ action: data: device_id: "{{ trigger.event.data.device_id }}" mode: queued +``` + +## Battery Not Reported +`battery_notes_battery_not_reported` + +This is fired from the [check_battery_last_reported](./services/check_battery_last_reported) service call for each device that has not reported its battery level for the number of days specified in the service call. + +The service can raise multiple events quickly so when using with an automation it's important to use the `mode: queued` to handle these. + +| Attribute | Type | Description | +|-----------|------|-------------| +| `device_id` | `string` | The device id of the device. | +| `device_name` | `string` | The device name. | +| `battery_type_and_quantity` | `string` | Battery type & quantity. | +| `battery_type` | `string` | Battery type. | +| `battery_quantity` | `int` | Battery quantity. | +| `battery_last_reported` | `datetime` | The datetime the battery was last reported. | +| `battery_last_reported_days` | `int` | The number of days since the battery was last reported. | +| `battery_last_reported_level` | `float` | The level of the battery when it was last reported. | + +### Automation Example + +See others in the [community contributions](./community.md) + +```yaml +alias: Battery Not Reported +description: Battery not reported +trigger: + - platform: event + event_type: battery_notes_battery_not_reported +condition: [] +action: + - service: persistent_notification.create + data: + title: | + {{ trigger.event.data.device_name }} Battery Not Reported + message: > + The device has not reported its battery level for {{ + trigger.event.data.battery_last_reported_days }} days {{ '\n' + -}} Its last reported level was {{ + trigger.event.data.battery_last_reported_level }}% {{ '\n' -}} You need + {{ trigger.event.data.battery_quantity }}× {{ + trigger.event.data.battery_type }} +mode: queued +max: 30 ``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index dc33467bd..a2b82d157 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,6 +34,21 @@ Once you have [installed the integration](https://github.com/andrew-codechimp/HA The library is updated automatically with new devices approximately every 24 hours from starting Home Assistant, if you have added a device to the library using [this form](https://github.com/andrew-codechimp/HA-Battery-Notes/issues/new?template=new_device_request.yml&title=[Device]%3A+) then this will take about a day to be discovered once it's approved and added. +## Battery Low Template +This is for advanced use where a device does not have a typical battery percentage (or it is innacurate) but still provides an indication of the level, such as a string, boolean or voltage. +You can specify a template that must return true when the battery is deemed low. + +Example templates +``` +{{ states('sensor.mysensor_battery_low') }} +{{ states('sensor.mysensor_battery_level') == "Low" }} +{{ states('sensor.mysensor_battery_voltage') | float(5) < 1 }} +``` + +!!! info + + If a template is specified then the battery percentage will be ignored when evaluating threshold and increased events. + ## Community Contributions diff --git a/docs/services.md b/docs/services.md index aa1ec57a4..62cd9de4a 100644 --- a/docs/services.md +++ b/docs/services.md @@ -10,3 +10,17 @@ See how to use this service in the [community contributions](./community.md) | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------- | | `data.device_id` | `no` | The device id that you want to change the battery replaced date for. | | `data.datetime_replaced` | `yes` | The optional datetime that you want to set the battery replaced to, if omitted the current date/time will be used. | + +## battery_notes.check_battery_last_reported + +For raising events for devices that haven't reported their battery level. + +The service will raise a seperate [battery_not_reported](./events/battery_not_reported) event for each device where its last reported date is older than the number of days specified. + +You can use this service call to schedule checks on batteries that is convenient to you, e.g. when you wake up, once a week etc. + +See how to use this service in the [community contributions](./community.md) + +| Parameter | Optional | Description | +| ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------- | +| `data.days` | `no` | The number of days since a device last reported its battery level. |