From c9cf7871409d09d9e5f465d38e4c224a32a8c956 Mon Sep 17 00:00:00 2001 From: Andrew Jackson Date: Sat, 3 Aug 2024 13:52:26 +0100 Subject: [PATCH] Library improvements (#1931) * Add optional model id on library * WIP * New device form * Apply automatic changes * WIP * WIP * WIP * WIP * WIP * WIP * WIP * Docs * WIP * Fixes * WIP * matching docs * Library sorting * Apply automatic changes * Remove test * Docs * Docs * Add condition inputs to blueprints * Bump min HA version to 2024.5 * Add icon translations * Add manual library message * Update device: Vibration_sensor_TS0210 by Tuya (#1849) * Apply automatic changes * Update device: Water_leak_sensor_IH_K665 by Aubess (#1851) * Apply automatic changes * Update device: Hue_secure_contact_sensor_SOC001 by Signify_Netherlands_B_V (#1853) Co-authored-by: Mariusthvdb <33354141+Mariusthvdb@users.noreply.github.com> * Apply automatic changes * Update device: Smart_button_ZG_101ZL by Loginovo (#1857) * Apply automatic changes * Update device: MJYD02YL by Xiaomi (#1855) * Apply automatic changes * Plant Sensor SGS01 via TuyaBLE integration (#1862) * Apply automatic changes * Update device: BRZ1 by Springs_Window_Fashions (#1865) * Apply automatic changes * Update device: BRZ1 by Springs_Window_Fashions (#1867) * Apply automatic changes * Update device: CSZ1 by Springs_Window_Fashions (#1869) * Apply automatic changes * Update device: VCZ1 by Springs_Window_Fashions (#1873) * Apply automatic changes * Update device: MCZ1 by Springs_Window_Fashions (#1871) * Apply automatic changes * Update device: 914C by Kwikset (#1875) * Apply automatic changes * Update device: 916 by Kwikset (#1877) * Apply automatic changes * Update device: 2844_222_0x10_0x16 by SmartLabs_Inc (#1879) * Apply automatic changes * Update device: 2845_222_0x10_0x11 by SmartLabs_Inc (#1883) * Apply automatic changes * Add Moes thermostat radiator valve variants (#1884) * Apply automatic changes * Update device: 2842_222_0x10_0x01 by SmartLabs_Inc (#1886) Co-authored-by: andrew-codechimp <1849731+andrew-codechimp@users.noreply.github.com> * Apply automatic changes * Bump softprops/action-gh-release from 2.0.6 to 2.0.8 (#1887) * Update crowdin.yml * New Crowdin translations by GitHub Action (#1888) Co-authored-by: Crowdin Bot * Update device: T8160 by Eufy_Security (#1891) * Apply automatic changes * Update device: T8210C by Eufy_Security (#1893) * Apply automatic changes * Update device: T8113_V by Eufy_Security (#1895) * Apply automatic changes * Update device: AS008 by Aqara (#1898) * Apply automatic changes * Update device: ZEN37_800LR by Zooz (#1901) * Apply automatic changes * Update library.json Change ZSE42 to manual * Apply automatic changes * Update device: Yale by YRL256 (#1903) * Apply automatic changes * Update device: M3_2_9 by OpenEpaperLink (#1905) * Apply automatic changes * Update device: SNZB_05P by Sonoff (#1907) * Apply automatic changes * Device: Shelly H&T (gen1) (#1908) * Apply automatic changes * Correct Yale YRL256 (#1909) The manufacturer and model were swapped * Apply automatic changes * Fix crowdin.yml * New Crowdin translations by GitHub Action (#1911) Co-authored-by: Crowdin Bot * Update device: Hinge_PIN_Door_Sensor_500S by GE (#1916) * Apply automatic changes * Update device: Q_Sensor by Zooz (#1920) * Apply automatic changes * Update device: Q_Sensor by GE (#1918) * Apply automatic changes * Update device: FRITZ_DECT_302 by AMV_FritzBox (#1922) * Apply automatic changes * Update device: FRITZ_DECT_350 by AMV_FritzBox (#1924) * Apply automatic changes * Update device: FRITZ_DECT_440 by AMV_FritzBox (#1926) * Apply automatic changes * Update library.json * Apply automatic changes * Device: Eaton - Ellipse ECO 650 (#1927) Added new device Eaton - Ellipse ECO 650 * Apply automatic changes * Update device: 99120_021 by Kwikset (#1929) Co-authored-by: CobraDunn <65230622+CobraDunn@users.noreply.github.com> * Apply automatic changes * New Crowdin translations by GitHub Action (#1930) * Apply automatic changes --------- Co-authored-by: andrew-codechimp Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Mariusthvdb <33354141+Mariusthvdb@users.noreply.github.com> Co-authored-by: Piotr Szulc Co-authored-by: mbuett <13905705+mbuett@users.noreply.github.com> Co-authored-by: andrew-codechimp <1849731+andrew-codechimp@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Crowdin Bot Co-authored-by: Rohan Kapoor Co-authored-by: Jeffrey Co-authored-by: CobraDunn <65230622+CobraDunn@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/new_device_request.yml | 27 ++- .github/scripts/library_doc/generate_file.py | 24 +- .github/workflows/json_librarian.yml | 2 +- .github/workflows/new_device.yml | 27 ++- .../battery_notes/binary_sensor.py | 1 - custom_components/battery_notes/button.py | 1 - custom_components/battery_notes/common.py | 9 + .../battery_notes/config_flow.py | 41 +++- custom_components/battery_notes/const.py | 6 +- custom_components/battery_notes/discovery.py | 6 +- custom_components/battery_notes/icons.json | 20 ++ custom_components/battery_notes/library.py | 229 ++++++++++++------ .../battery_notes/library_updater.py | 3 +- custom_components/battery_notes/schema.json | 15 +- custom_components/battery_notes/sensor.py | 2 - .../battery_notes/translations/ca.json | 4 + .../battery_notes/translations/da.json | 4 + .../battery_notes/translations/de.json | 4 + .../battery_notes/translations/el.json | 4 + .../battery_notes/translations/en.json | 4 + .../battery_notes/translations/es-ES.json | 4 + .../battery_notes/translations/fi.json | 4 + .../battery_notes/translations/fr.json | 4 + .../battery_notes/translations/hu.json | 4 + .../battery_notes/translations/it.json | 4 + .../battery_notes/translations/lt.json | 4 + .../battery_notes/translations/nl.json | 4 + .../battery_notes/translations/pl.json | 4 + .../battery_notes/translations/pt.json | 6 +- .../battery_notes/translations/ru.json | 4 + .../battery_notes/translations/sk.json | 4 + .../battery_notes/translations/sr-Latn.json | 4 + .../battery_notes/translations/sv-SE.json | 4 + .../battery_notes/translations/ur-IN.json | 4 + .../battery_notes_battery_not_reported.yaml | 10 + .../battery_notes_battery_replaced.yaml | 12 +- .../battery_notes_battery_threshold.yaml | 12 +- docs/library.md | 13 +- hacs.json | 2 +- requirements.txt | 2 +- 40 files changed, 420 insertions(+), 122 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/new_device_request.yml b/.github/ISSUE_TEMPLATE/new_device_request.yml index ec2062847..3705d5e95 100644 --- a/.github/ISSUE_TEMPLATE/new_device_request.yml +++ b/.github/ISSUE_TEMPLATE/new_device_request.yml @@ -9,6 +9,7 @@ body: The battery library is a JSON document at [custom_components/battery_notes/data/library.json](https://github.com/andrew-codechimp/HA-Battery-Notes/blob/main/custom_components/battery_notes/data/library.json) To contribute, submit your device details via this form and the relevant code changes will be proposed on your behalf. Note: The title above is not used and can be just a friendly description of the device. Manufacturer and model should be exactly what is displayed on the Device screen within Home Assistant. + If your device has a Model ID or HW Version then these must be included. To see your devices, click here: [![Open your Home Assistant instance and show your devices.](https://my.home-assistant.io/badges/devices.svg)](https://my.home-assistant.io/redirect/devices/) @@ -18,7 +19,7 @@ body: attributes: label: Manufacturer description: The manufacturer should be exactly what is displayed on the Devices screen within Home Assistant. - placeholder: ex. eWeLink + placeholder: ex. Philips validations: required: true @@ -27,12 +28,30 @@ body: attributes: label: Model description: The model should be exactly what is displayed on the Devices screen within Home Assistant. - placeholder: ex. DS01 + placeholder: ex. Hue dimmer switch validations: required: true - type: input - id: battery-type + id: model_id + attributes: + label: Model ID + description: If the device has a Model ID in the Devices screen within Home Assistant it must be included. + placeholder: ex. 324131092621 + validations: + required: false + + - type: input + id: hw_version + attributes: + label: HW Version + description: If the device has a Hardware version shown in the Devices screen within Home Assistant it must be included. + placeholder: ex. V7.2 + validations: + required: false + + - type: input + id: battery_type attributes: label: Battery Type description: When specifying battery types please use the Most Common naming for general batteries and the IEC naming for battery cells according to [Wikipedia](https://en.wikipedia.org/wiki/List_of_battery_sizes). @@ -41,7 +60,7 @@ body: required: true - type: input - id: battery-quantity + id: battery_quantity attributes: label: Battery Quantity description: The battery_quantity attribute is numeric (no letters or special characters). diff --git a/.github/scripts/library_doc/generate_file.py b/.github/scripts/library_doc/generate_file.py index 8e979b79b..4975ee64e 100644 --- a/.github/scripts/library_doc/generate_file.py +++ b/.github/scripts/library_doc/generate_file.py @@ -11,8 +11,9 @@ def generate_device_list(): """Generate static file containing the device library.""" # Load the existing JSON library file - with open("custom_components/battery_notes/data/library.json", - encoding="UTF-8") as f: + with open( + "custom_components/battery_notes/data/library.json", encoding="UTF-8" + ) as f: devices_json = json.loads(f.read()) devices = devices_json.get("devices") @@ -26,6 +27,8 @@ def generate_device_list(): headers = [ "Manufacturer", "Model", + "Model ID", + "Hardware", "Battery Type", ] @@ -37,14 +40,20 @@ def generate_device_list(): else: battery_type_qty = device["battery_type"] - if "hw_version" in device: - model = f"{device['model']} ({device['hw_version']})" - else: - model = device['model'] + model = device["model"] + model_match_method = device.get("model_match_method", "") + if model_match_method == "startswith": + model = rf"{model}\*" + if model_match_method == "endswith": + model = rf"\*{model}" + if model_match_method == "contains": + model = rf"\*{model}\*" row = [ - device['manufacturer'], + device["manufacturer"], model, + device.get("model_id", ""), + device.get("hw_version", ""), battery_type_qty, ] rows.append(row) @@ -59,4 +68,5 @@ def generate_device_list(): md_file.write("".join(toc_links) + tables_output) md_file.close() + generate_device_list() diff --git a/.github/workflows/json_librarian.yml b/.github/workflows/json_librarian.yml index 609557186..6d2d86455 100644 --- a/.github/workflows/json_librarian.yml +++ b/.github/workflows/json_librarian.yml @@ -37,7 +37,7 @@ jobs: devices = devices_json.get("devices") # Sort the devices by manufacturer and model - devices.sort(key=lambda k: (k["manufacturer"].lower(), k["model"].lower(), k.get("hw_version", "").lower())) + devices.sort(key=lambda k: (k["manufacturer"].lower(), k.get("model_match_method", "").lower(), k["model"].lower(), k.get("model_id", "").lower(), k.get("hw_version", "").lower())) with open("custom_components/battery_notes/data/library.json", "w", encoding="UTF-8") as f: f.write(json.dumps(devices_json, indent=4)) diff --git a/.github/workflows/new_device.yml b/.github/workflows/new_device.yml index 88f7e4621..48188d7c4 100644 --- a/.github/workflows/new_device.yml +++ b/.github/workflows/new_device.yml @@ -43,16 +43,20 @@ jobs: # Remove the "battery_quantity" key from the device dictionary if it's 1 new_device = ${{ steps.device-data.outputs.json }} # Convert battery_quantity field to a numeric - numeric_quantity = int(new_device["battery_quantity"]) + numeric_quantity = int(new_device["battery_quantity"]) del new_device["battery_quantity"] # Add numeric "battery_quantity" key if it's more than 1 if numeric_quantity > 1: new_device["battery_quantity"] = numeric_quantity + if new_device.get("model_id", "MISSING").strip() == "": + del new_device["model_id"] + if new_device.get("hw_version", "MISSING").strip() == "": + del new_device["hw_version"] # Check for duplicates and replace old entry with new one duplicate_found = False for i, device in enumerate(devices): - if device["manufacturer"] == new_device["manufacturer"] and device["model"] == new_device["model"]: + if device["manufacturer"] == new_device["manufacturer"] and device["model"] == new_device["model"] and device.get("model_id", "") == new_device.get("model_id", "") and device.get("hw_version", "") == new_device.get("hw_version", ""): devices[i] = new_device duplicate_found = True break @@ -62,15 +66,17 @@ jobs: devices.append(new_device) # Save manufacturer and model for later use - set_output("mm", "_".join(re.findall(r"\w+",f"{new_device['manufacturer']}{new_device['model']})".lower()))) + set_output("branch", "_".join(re.findall(r"\w+",f"{new_device['manufacturer']}{new_device['model']}{new_device.get('model_id', '')}{new_device.get('hw_version', '')})".lower()))) set_output("manufacturer", "_".join(re.findall(r"\w+",f"{new_device['manufacturer']})"))) set_output("model", "_".join(re.findall(r"\w+",f"{new_device['model']})"))) + set_output("model_id", "_".join(re.findall(r"\w+",f"{new_device.get('model_id', '')})"))) + set_output("hw_version", "_".join(re.findall(r"\w+",f"{new_device.get('hw_version', '')})"))) set_output("bqt", f"{numeric_quantity}x {new_device['battery_type']}") if duplicate_found: set_output("mode", "updates") else: - set_output("mode", "adds") + set_output("mode", "adds") with open("custom_components/battery_notes/data/library.json", "w") as f: f.write(json.dumps(devices_json, indent=4)) @@ -96,10 +102,17 @@ jobs: with: commit-message: "Update device: ${{ steps.update-json.outputs.model }} by ${{ steps.update-json.outputs.manufacturer }}" title: "Device: ${{ steps.update-json.outputs.manufacturer }} - ${{ steps.update-json.outputs.model }}" - body: "This pull request ${{ steps.update-json.outputs.mode }} the device information for ${{ steps.update-json.outputs.model }} by ${{ steps.update-json.outputs.manufacturer }} with ${{ steps.update-json.outputs.bqt }}\nIt closes issue #${{ github.event.issue.number }}" - branch: "device-${{ steps.update-json.outputs.mm }}" + body: | + This pull request ${{ steps.update-json.outputs.mode }} the device information for: + Manufacturer: ${{ steps.update-json.outputs.manufacturer }} + Model: ${{ steps.update-json.outputs.model }} + Model ID: ${{ steps.update-json.outputs.model_id }} + Hardware: ${{ steps.update-json.outputs.hw_version }} + Battery: ${{ steps.update-json.outputs.bqt }} + It closes issue #${{ github.event.issue.number }} + branch: "device-${{ steps.update-json.outputs.branch }}" - name: Close Issue run: gh issue close --comment "Thanks for the contribution. We're auto-closing this issue. If it's a new device, a pull request will be created that will be reviewed and merged." ${{github.event.issue.number}} env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/custom_components/battery_notes/binary_sensor.py b/custom_components/battery_notes/binary_sensor.py index d94e12a64..b5cab3672 100644 --- a/custom_components/battery_notes/binary_sensor.py +++ b/custom_components/battery_notes/binary_sensor.py @@ -173,7 +173,6 @@ async def async_registry_updated(event: Event) -> None: unique_id_suffix="_battery_low", key="_battery_plus_low", translation_key="battery_low", - icon="mdi:battery-alert", entity_category=EntityCategory.DIAGNOSTIC, device_class=BinarySensorDeviceClass.BATTERY, ) diff --git a/custom_components/battery_notes/button.py b/custom_components/battery_notes/button.py index 8e33b2d8c..522c8aacb 100644 --- a/custom_components/battery_notes/button.py +++ b/custom_components/battery_notes/button.py @@ -147,7 +147,6 @@ async def async_registry_updated(event: Event) -> None: unique_id_suffix="_battery_replaced_button", key="battery_replaced", translation_key="battery_replaced", - icon="mdi:battery-sync", entity_category=EntityCategory.DIAGNOSTIC, entity_registry_enabled_default=enable_replaced, ) diff --git a/custom_components/battery_notes/common.py b/custom_components/battery_notes/common.py index 309064b1d..f2374224b 100644 --- a/custom_components/battery_notes/common.py +++ b/custom_components/battery_notes/common.py @@ -1,5 +1,7 @@ """Common functions for battery_notes.""" +from homeassistant.helpers.device_registry import DeviceEntry + def validate_is_float(num): """Validate value is a float.""" @@ -10,3 +12,10 @@ def validate_is_float(num): except ValueError: return False return False + +def get_device_model_id(device_entry: DeviceEntry) -> str | None: + """Get the device model if available.""" + if hasattr(device_entry, "model_id"): + return device_entry.model_id + else: + return None diff --git a/custom_components/battery_notes/config_flow.py b/custom_components/battery_notes/config_flow.py index f9b6ce85b..8de06e053 100644 --- a/custom_components/battery_notes/config_flow.py +++ b/custom_components/battery_notes/config_flow.py @@ -23,6 +23,7 @@ from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import dt as dt_util +from .common import get_device_model_id from .const import ( CONF_BATTERY_LOW_TEMPLATE, CONF_BATTERY_LOW_THRESHOLD, @@ -31,6 +32,7 @@ CONF_DEVICE_NAME, CONF_MANUFACTURER, CONF_MODEL, + CONF_MODEL_ID, CONF_SHOW_ALL_DEVICES, CONF_SOURCE_ENTITY_ID, DATA_LIBRARY_UPDATER, @@ -131,6 +133,7 @@ async def async_step_integration_discovery( "name": discovery_info[CONF_DEVICE_NAME], "manufacturer": discovery_info[CONF_MANUFACTURER], "model": discovery_info[CONF_MODEL], + "model_id": discovery_info[CONF_MODEL_ID], } return await self.async_step_device(discovery_info) @@ -149,6 +152,8 @@ async def async_step_device( ) -> config_entries.FlowResult: """Handle a flow for a device or discovery.""" errors: dict[str, str] = {} + device_battery_details = None + if user_input is not None: self.data = user_input @@ -167,14 +172,15 @@ async def async_step_device( device_entry = device_registry.async_get(device_id) _LOGGER.debug( - "Looking up device %s %s %s", + "Looking up device %s %s %s %s", device_entry.manufacturer, device_entry.model, + get_device_model_id(device_entry) or "", device_entry.hw_version, ) model_info = ModelInfo( - device_entry.manufacturer, device_entry.model, device_entry.hw_version + device_entry.manufacturer, device_entry.model, get_device_model_id(device_entry), device_entry.hw_version ) library = await Library.factory(self.hass) @@ -188,9 +194,10 @@ async def async_step_device( if device_battery_details and not device_battery_details.is_manual: _LOGGER.debug( - "Found device %s %s %s", + "Found device %s %s %s %s", device_entry.manufacturer, device_entry.model, + get_device_model_id(device_entry) or "", device_entry.hw_version, ) self.data[CONF_BATTERY_TYPE] = device_battery_details.battery_type @@ -199,6 +206,9 @@ async def async_step_device( device_battery_details.battery_quantity ) + if device_battery_details and device_battery_details.is_manual: + return await self.async_step_manual() + return await self.async_step_battery() schema = DEVICE_SCHEMA @@ -221,6 +231,8 @@ async def async_step_entity( ) -> config_entries.FlowResult: """Handle a flow for a device or discovery.""" errors: dict[str, str] = {} + device_battery_details = None + if user_input is not None: self.data = user_input @@ -249,15 +261,17 @@ async def async_step_entity( device_entry = device_registry.async_get(entity_entry.device_id) _LOGGER.debug( - "Looking up device %s %s %s", + "Looking up device %s %s %s %s", device_entry.manufacturer, device_entry.model, + get_device_model_id(device_entry) or "", device_entry.hw_version, ) model_info = ModelInfo( device_entry.manufacturer, device_entry.model, + get_device_model_id(device_entry), device_entry.hw_version, ) @@ -269,9 +283,10 @@ async def async_step_entity( if device_battery_details and not device_battery_details.is_manual: _LOGGER.debug( - "Found device %s %s %s", + "Found device %s %s %s %s", device_entry.manufacturer, device_entry.model, + get_device_model_id(device_entry) or "", device_entry.hw_version, ) self.data[CONF_BATTERY_TYPE] = ( @@ -282,6 +297,8 @@ async def async_step_entity( device_battery_details.battery_quantity ) + if device_battery_details and device_battery_details.is_manual: + return await self.async_step_manual() return await self.async_step_battery() else: # No entity_registry entry, must be a config.yaml entity which we can't support @@ -296,6 +313,20 @@ async def async_step_entity( last_step=False, ) + async def async_step_manual(self, user_input: dict[str, Any] | None = None): + """Second step in config flow to add the battery type.""" + errors: dict[str, str] = {} + if user_input is not None: + return await self.async_step_battery() + + return self.async_show_form( + step_id="manual", + data_schema=None, + last_step=False, + errors=errors, + ) + + async def async_step_battery(self, user_input: dict[str, Any] | None = None): """Second step in config flow to add the battery type.""" errors: dict[str, str] = {} diff --git a/custom_components/battery_notes/const.py b/custom_components/battery_notes/const.py index 226272a77..f0c25b56a 100644 --- a/custom_components/battery_notes/const.py +++ b/custom_components/battery_notes/const.py @@ -4,15 +4,14 @@ from logging import Logger, getLogger from pathlib import Path from typing import Final -import voluptuous as vol +import voluptuous as vol from homeassistant.const import Platform - from homeassistant.helpers import config_validation as cv LOGGER: Logger = getLogger(__package__) -MIN_HA_VERSION = "2024.4" +MIN_HA_VERSION = "2024.5" manifestfile = Path(__file__).parent / "manifest.json" with open(file=manifestfile, encoding="UTF-8") as json_file: @@ -40,6 +39,7 @@ CONF_ENABLE_AUTODISCOVERY = "enable_autodiscovery" CONF_USER_LIBRARY = "user_library" CONF_MODEL = "model" +CONF_MODEL_ID = "model_id" CONF_MANUFACTURER = "manufacturer" CONF_DEVICE_NAME = "device_name" CONF_LIBRARY_URL = "https://raw.githubusercontent.com/andrew-codechimp/HA-Battery-Notes/main/custom_components/battery_notes/data/library.json" # pylint: disable=line-too-long diff --git a/custom_components/battery_notes/discovery.py b/custom_components/battery_notes/discovery.py index 88662f922..7913e1473 100644 --- a/custom_components/battery_notes/discovery.py +++ b/custom_components/battery_notes/discovery.py @@ -12,12 +12,14 @@ from homeassistant.helpers import discovery_flow from homeassistant.helpers.typing import ConfigType +from .common import get_device_model_id from .const import ( CONF_BATTERY_QUANTITY, CONF_BATTERY_TYPE, CONF_DEVICE_NAME, CONF_MANUFACTURER, CONF_MODEL, + CONF_MODEL_ID, DOMAIN, ) from .library import DeviceBatteryDetails, Library, ModelInfo @@ -56,12 +58,13 @@ async def get_model_information( manufacturer = device_entry.manufacturer model = device_entry.model + model_id = get_device_model_id(device_entry) hw_version = device_entry.hw_version if not manufacturer or not model: return None - return ModelInfo(manufacturer, model, hw_version) + return ModelInfo(manufacturer, model, model_id, hw_version) class DiscoveryManager: @@ -152,6 +155,7 @@ def _init_entity_discovery( ) discovery_data[CONF_MANUFACTURER] = device_battery_details.manufacturer discovery_data[CONF_MODEL] = device_battery_details.model + discovery_data[CONF_MODEL_ID] = get_device_model_id(device_entry), discovery_data[CONF_DEVICE_NAME] = get_wrapped_device_name( device_entry.id, device_entry ) diff --git a/custom_components/battery_notes/icons.json b/custom_components/battery_notes/icons.json index acbbd05b3..65f7f0ad6 100644 --- a/custom_components/battery_notes/icons.json +++ b/custom_components/battery_notes/icons.json @@ -1,4 +1,24 @@ { + "entity": { + "binary_sensor": { + "battery_low": { + "default": "mdi:battery-alert" + } + }, + "button": { + "battery_replaced": { + "default": "mdi:battery-sync" + } + }, + "sensor": { + "battery_type": { + "default": "mdi:battery-unknown" + }, + "battery_last_replaced": { + "default": "mdi:battery-clock" + } + } + }, "services": { "set_battery_replaced": "mdi:battery-sync", "check_battery_last_reported": "mdi:battery-unknown", diff --git a/custom_components/battery_notes/library.py b/custom_components/battery_notes/library.py index 9ea11b320..26410a5d3 100644 --- a/custom_components/battery_notes/library.py +++ b/custom_components/battery_notes/library.py @@ -1,26 +1,35 @@ """Battery Type library for battery_notes.""" -from __future__ import annotations -from typing import Any, cast +from __future__ import annotations import json import logging import os -from typing import NamedTuple +from typing import Any, Final, NamedTuple, cast from homeassistant.core import HomeAssistant from .const import ( - DOMAIN, + CONF_USER_LIBRARY, DATA_LIBRARY, + DOMAIN, DOMAIN_CONFIG, - CONF_USER_LIBRARY, ) BUILT_IN_DATA_DIRECTORY = os.path.join(os.path.dirname(__file__), "data") _LOGGER = logging.getLogger(__name__) +LIBRARY_DEVICES: Final[str] = "devices" +LIBRARY_MANUFACTURER: Final[str] = "manufacturer" +LIBRARY_MODEL: Final[str] = "model" +LIBRARY_MODEL_MATCH_METHOD: Final[str] = "model_match_method" +LIBRARY_MODEL_ID: Final[str] = "model_id" +LIBRARY_HW_VERSION: Final[str] = "hw_version" +LIBRARY_BATTERY_TYPE: Final[str] = "battery_type" +LIBRARY_BATTERY_QUANTITY: Final[str] = "battery_quantity" +LIBRARY_MISSING: Final[str] = "##MISSING##" + class Library: # pylint: disable=too-few-public-methods """Hold all known battery types.""" @@ -43,8 +52,10 @@ def _load_library_json(library_file: str) -> dict[str, Any]: if ( DOMAIN_CONFIG in self.hass.data[DOMAIN] and CONF_USER_LIBRARY in self.hass.data[DOMAIN][DOMAIN_CONFIG] - ): - user_library_filename = self.hass.data[DOMAIN][DOMAIN_CONFIG].get(CONF_USER_LIBRARY) + ): + user_library_filename = self.hass.data[DOMAIN][DOMAIN_CONFIG].get( + CONF_USER_LIBRARY + ) if user_library_filename != "": json_user_path = os.path.join( BUILT_IN_DATA_DIRECTORY, @@ -53,10 +64,14 @@ def _load_library_json(library_file: str) -> dict[str, Any]: _LOGGER.debug("Using user library file at %s", json_user_path) try: - user_json_data = await self.hass.async_add_executor_job(_load_library_json, json_user_path) + user_json_data = await self.hass.async_add_executor_job( + _load_library_json, json_user_path + ) self._devices = user_json_data["devices"] - _LOGGER.debug("Loaded %s user devices", len(user_json_data["devices"])) + _LOGGER.debug( + "Loaded %s user devices", len(user_json_data["devices"]) + ) except FileNotFoundError: _LOGGER.error( @@ -67,14 +82,19 @@ def _load_library_json(library_file: str) -> dict[str, Any]: # Default Library json_default_path = os.path.join( BUILT_IN_DATA_DIRECTORY, - "library.json",) + "library.json", + ) _LOGGER.debug("Using library file at %s", json_default_path) try: - default_json_data = await self.hass.async_add_executor_job(_load_library_json, json_default_path) - self._devices.extend(default_json_data["devices"]) - _LOGGER.debug("Loaded %s default devices", len(default_json_data["devices"])) + default_json_data = await self.hass.async_add_executor_job( + _load_library_json, json_default_path + ) + self._devices.extend(default_json_data[LIBRARY_DEVICES]) + _LOGGER.debug( + "Loaded %s default devices", len(default_json_data[LIBRARY_DEVICES]) + ) except FileNotFoundError: _LOGGER.error( @@ -99,84 +119,136 @@ async def factory(hass: HomeAssistant) -> Library: async def get_device_battery_details( self, - model_info: ModelInfo, + device_to_find: ModelInfo, ) -> DeviceBatteryDetails | None: """Create a battery details object from the JSON devices data.""" - if self._devices is not None: - - # If a hw_version is present try find that first - if model_info.hw_version: - matching_devices = [] - - # Find all devices that match the manufacturer and model - for device in self._devices: - if ( - str(device["manufacturer"] or "").casefold() - == str(model_info.manufacturer or "").casefold() - and str(device["model"] or "").casefold() - == str(model_info.model or "").casefold() - ): - matching_devices.append(device) - - if matching_devices is None or not matching_devices or len(matching_devices) == 0: - return None - - # Check if any matching devices have specified hw_version - for device in matching_devices: - if device.get("hw_version", "").casefold() == str(model_info.hw_version or "").casefold(): - matched_device = device - device_battery_details = DeviceBatteryDetails( - manufacturer=matched_device["manufacturer"], - model=matched_device["model"], - hw_version=matched_device["hw_version"], - battery_type=matched_device["battery_type"], - battery_quantity=matched_device.get("battery_quantity", 1), - ) - break - else: - # Return first item in list, the non hw_version one - matched_device = matching_devices[0] - - device_battery_details = DeviceBatteryDetails( - manufacturer=matched_device["manufacturer"], - model=matched_device["model"], - hw_version=matched_device.get("hw_version", None), - battery_type=matched_device["battery_type"], - battery_quantity=matched_device.get("battery_quantity", 1), - ) - return device_battery_details - - else: - # For devices that don't have hw_version - for device in self._devices: - if ( - str(device["manufacturer"] or "").casefold() - == str(model_info.manufacturer or "").casefold() - and str(device["model"] or "").casefold() - == str(model_info.model or "").casefold() - ): - device_battery_details = DeviceBatteryDetails( - manufacturer=device["manufacturer"], - model=device["model"], - hw_version=device.get("hw_version", None), - battery_type=device["battery_type"], - battery_quantity=device.get("battery_quantity", 1), - ) - return device_battery_details - - return None + if self._devices is None: + return None + + # Test only + # device_to_find = ModelInfo("Espressif", "m5stack-atom", None, None) + + # Get all devices matching manufacturer & model + matching_devices = None + partial_matching_devices = None + fully_matching_devices = None + + matching_devices = [ + x for x in self._devices if self.device_basic_match(x, device_to_find) + ] + + if matching_devices and len(matching_devices) > 1: + partial_matching_devices = [ + x + for x in matching_devices + if self.device_partial_match(x, device_to_find) + ] + + if partial_matching_devices and len(partial_matching_devices) > 0: + matching_devices = partial_matching_devices + + if matching_devices and len(matching_devices) > 1: + fully_matching_devices = [ + x for x in matching_devices if self.device_full_match(x, device_to_find) + ] + + if fully_matching_devices and len(fully_matching_devices) > 0: + matching_devices = fully_matching_devices + + if not matching_devices or len(matching_devices) == 0: + return None + + matched_device = matching_devices[0] + return DeviceBatteryDetails( + manufacturer=matched_device[LIBRARY_MANUFACTURER], + model=matched_device[LIBRARY_MODEL], + model_id=matched_device.get("model_id", ""), + hw_version=matched_device.get(LIBRARY_HW_VERSION, ""), + battery_type=matched_device[LIBRARY_BATTERY_TYPE], + battery_quantity=matched_device.get(LIBRARY_BATTERY_QUANTITY, 1), + ) def loaded(self) -> bool: """Library loaded successfully.""" return self._devices is not None + def device_basic_match(self, device: dict[str, Any], model_info: ModelInfo) -> bool: + """Check if device match on manufacturer and model.""" + if ( + str(device[LIBRARY_MANUFACTURER] or "").casefold() + != str(model_info.manufacturer or "").casefold() + ): + return False + + if LIBRARY_MODEL_MATCH_METHOD in device: + if device[LIBRARY_MODEL_MATCH_METHOD] == "startswith": + if ( + str(model_info.model or "") + .casefold() + .startswith(str(device[LIBRARY_MODEL] or "").casefold()) + ): + return True + if device[LIBRARY_MODEL_MATCH_METHOD] == "endswith": + if ( + str(model_info.model or "") + .casefold() + .endswith(str(device[LIBRARY_MODEL] or "").casefold()) + ): + return True + if device[LIBRARY_MODEL_MATCH_METHOD] == "contains": + if str(model_info.model or "").casefold() in ( + str(device[LIBRARY_MODEL] or "").casefold() + ): + return True + else: + if ( + str(device[LIBRARY_MODEL] or "").casefold() + == str(model_info.model or "").casefold() + ): + return True + return False + + def device_partial_match( + self, device: dict[str, Any], model_info: ModelInfo + ) -> bool: + """Check if device match on hw_version or model_id.""" + if model_info.hw_version is None or model_info.model_id is None: + if ( + device.get(LIBRARY_HW_VERSION, LIBRARY_MISSING).casefold() + == str(model_info.hw_version).casefold() + and device.get(LIBRARY_MODEL_ID, LIBRARY_MISSING).casefold() + == str(model_info.model_id).casefold() + ): + return True + else: + if ( + device.get(LIBRARY_HW_VERSION, LIBRARY_MISSING).casefold() + == str(model_info.hw_version).casefold() + or device.get(LIBRARY_MODEL_ID, LIBRARY_MISSING).casefold() + == str(model_info.model_id).casefold() + ): + return True + return False + + def device_full_match(self, device: dict[str, Any], model_info: ModelInfo) -> bool: + """Check if device match on hw_version and model_id.""" + if ( + device.get(LIBRARY_HW_VERSION, LIBRARY_MISSING).casefold() + == str(model_info.hw_version).casefold() + and device.get(LIBRARY_MODEL_ID, LIBRARY_MISSING).casefold() + == str(model_info.model_id).casefold() + ): + return True + return False + class DeviceBatteryDetails(NamedTuple): """Describes a device battery type.""" manufacturer: str model: str + model_id: str hw_version: str battery_type: str battery_quantity: int @@ -209,4 +281,5 @@ class ModelInfo(NamedTuple): manufacturer: str model: str - hw_version: str + model_id: str | None + hw_version: str | None diff --git a/custom_components/battery_notes/library_updater.py b/custom_components/battery_notes/library_updater.py index e34abd7f8..15f810f68 100644 --- a/custom_components/battery_notes/library_updater.py +++ b/custom_components/battery_notes/library_updater.py @@ -104,7 +104,8 @@ def _update_library_json(library_file: str, content: str) -> dict[str, Any]: except LibraryUpdaterClientError: _LOGGER.warning( - "Unable to update library, this could be a GitHub or internet connectivity issue, will retry later." + "Unable to update library, this could be a GitHub or internet " + "connectivity issue, will retry later." ) async def time_to_update_library(self) -> bool: diff --git a/custom_components/battery_notes/schema.json b/custom_components/battery_notes/schema.json index 7ccc517dd..fc25913df 100644 --- a/custom_components/battery_notes/schema.json +++ b/custom_components/battery_notes/schema.json @@ -32,9 +32,13 @@ "type": "string", "description": "The model of the device as it appears in Home Assistant." }, + "model_id": { + "type": "string", + "description": "The model id of the device as it appears in Home Assistant. Only required where a model id is specified." + }, "hw_version": { "type": "string", - "description": "The hardware version of the device as it appears in Home Assistant. Only required where there are two versions of a model." + "description": "The hardware version of the device as it appears in Home Assistant. Only required where there are multiple versions of a model." }, "battery_type": { "type": "string", @@ -44,6 +48,15 @@ "type": "integer", "exclusiveMinimum": 1, "description": "The number of batteries required by the device. If a device only has one battery this property should be omitted." + }, + "model_match_method": { + "type": "string", + "description": "The matching method for model, default exact.", + "enum": [ + "startswith", + "endswith", + "contains" + ] } } } diff --git a/custom_components/battery_notes/sensor.py b/custom_components/battery_notes/sensor.py index d80df8c8e..bfce69bcb 100644 --- a/custom_components/battery_notes/sensor.py +++ b/custom_components/battery_notes/sensor.py @@ -199,7 +199,6 @@ async def async_registry_updated(event: Event) -> None: unique_id_suffix="", # battery_type has uniqueId set to entityId in V1, never add a suffix key="battery_type", translation_key="battery_type", - icon="mdi:battery-unknown", entity_category=EntityCategory.DIAGNOSTIC, ) @@ -207,7 +206,6 @@ async def async_registry_updated(event: Event) -> None: unique_id_suffix="_battery_last_replaced", key="battery_last_replaced", translation_key="battery_last_replaced", - icon="mdi:battery-clock", entity_category=EntityCategory.DIAGNOSTIC, device_class=SensorDeviceClass.TIMESTAMP, entity_registry_enabled_default=enable_replaced, diff --git a/custom_components/battery_notes/translations/ca.json b/custom_components/battery_notes/translations/ca.json index c2e55e2ed..f33701524 100644 --- a/custom_components/battery_notes/translations/ca.json +++ b/custom_components/battery_notes/translations/ca.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 utilitzarà el llindar predeterminat global", "battery_low_template": "La plantilla per determinar que la bateria és baixa, hauria de tornar correcte si és baixa\nNomés és necessari per a nivells de bateria no estàndard" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/da.json b/custom_components/battery_notes/translations/da.json index e1a737a13..ed3ff99ef 100644 --- a/custom_components/battery_notes/translations/da.json +++ b/custom_components/battery_notes/translations/da.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 vil bruge den globale standardtærskel", "battery_low_template": "Skabelon til at bestemme om et batteri er lavt, skal returnere sand, hvis lavt.\nKun nødvendigt for ikke-standard batteriniveauer" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/de.json b/custom_components/battery_notes/translations/de.json index dec755e1d..da440d236 100644 --- a/custom_components/battery_notes/translations/de.json +++ b/custom_components/battery_notes/translations/de.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 verwendet den globalen Standardschwellenwert", "battery_low_template": "Vorlage um zu bestimmen, ob eine Batterie schwach ist; sollte \"wahr\" (true) rückmelden, wenn schwach.\nNur für nicht-Standard Batteriewerte benötigt." } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/el.json b/custom_components/battery_notes/translations/el.json index e0698462c..84324807f 100644 --- a/custom_components/battery_notes/translations/el.json +++ b/custom_components/battery_notes/translations/el.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 θα χρησιμοποιηθεί το καθολικό προεπιλεγμένο ελάχιστο όριο", "battery_low_template": "Template για τον προσδιορισμό μιας μπαταρίας είναι χαμηλή, θα πρέπει να επιστρέψει true εάν είναι χαμηλή\nΧρειάζεται μόνο για μη τυπικές στάθμες μπαταρίας" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/en.json b/custom_components/battery_notes/translations/en.json index 2a8d93693..2d8697866 100644 --- a/custom_components/battery_notes/translations/en.json +++ b/custom_components/battery_notes/translations/en.json @@ -41,6 +41,10 @@ "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" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/es-ES.json b/custom_components/battery_notes/translations/es-ES.json index 50ac65f60..b19ed585b 100644 --- a/custom_components/battery_notes/translations/es-ES.json +++ b/custom_components/battery_notes/translations/es-ES.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 usará el umbral global por defecto", "battery_low_template": "Plantilla para determinar que una batería es baja, debe devolver verdadero si es baja\nSolo necesario para niveles de batería no estándar" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/fi.json b/custom_components/battery_notes/translations/fi.json index 36ed36aea..4b088eff0 100644 --- a/custom_components/battery_notes/translations/fi.json +++ b/custom_components/battery_notes/translations/fi.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 käyttää yleistä oletusarvoa", "battery_low_template": "Template to determine a battery is low, should return true if low\nOnly needed for non-standard battery levels" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/fr.json b/custom_components/battery_notes/translations/fr.json index c330ce7b2..2cc36613a 100644 --- a/custom_components/battery_notes/translations/fr.json +++ b/custom_components/battery_notes/translations/fr.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 gardera le seuil par defaut", "battery_low_template": "Modèle pour déterminer si une batterie est faible, devrait retourner vrai si faible\nNécessaire uniquement pour les niveaux de batterie non standard" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/hu.json b/custom_components/battery_notes/translations/hu.json index e87af4ac8..c0e5c8333 100644 --- a/custom_components/battery_notes/translations/hu.json +++ b/custom_components/battery_notes/translations/hu.json @@ -41,6 +41,10 @@ "battery_low_threshold": "A 0 a globális alapértelmezett küszöböt fogja használni", "battery_low_template": "Template to determine a battery is low, should return true if low\nOnly needed for non-standard battery levels" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/it.json b/custom_components/battery_notes/translations/it.json index 1e6a47a4a..bb5d2f09f 100644 --- a/custom_components/battery_notes/translations/it.json +++ b/custom_components/battery_notes/translations/it.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 utilizzerà la soglia globale predefinita", "battery_low_template": "Modello per determinare se una batteria è scarica, dovrebbe restituire vero se scarica. \nNecessario solo per livelli di batteria non standard" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/lt.json b/custom_components/battery_notes/translations/lt.json index d9049fdb0..19bb0ba7b 100644 --- a/custom_components/battery_notes/translations/lt.json +++ b/custom_components/battery_notes/translations/lt.json @@ -41,6 +41,10 @@ "battery_low_threshold": "Įrašius 0 bus naudojama numatytoji vertė", "battery_low_template": "Template to determine a battery is low, should return true if low\nOnly needed for non-standard battery levels" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/nl.json b/custom_components/battery_notes/translations/nl.json index c607bf942..5779021f3 100644 --- a/custom_components/battery_notes/translations/nl.json +++ b/custom_components/battery_notes/translations/nl.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 zal de globale standaard drempelwaarde gebruiken", "battery_low_template": "Sjabloon om te bepalen dat een batterij bijna leeg is, zal 'waar' teruggeven als bijna leeg\nAlleen nodig voor niet-standaard batterijniveaus" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/pl.json b/custom_components/battery_notes/translations/pl.json index 03982228b..351515e96 100644 --- a/custom_components/battery_notes/translations/pl.json +++ b/custom_components/battery_notes/translations/pl.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 użyje globalnego progu domyślnego", "battery_low_template": "Szablon do określenia czy poziom naładowania baterii jest niski, powinien zwrócić wartość true, jeśli poziom jest niski.\nJest wymagany tylko dla niestandardowych raportów poziomu baterii" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/pt.json b/custom_components/battery_notes/translations/pt.json index c1e35e571..384861514 100644 --- a/custom_components/battery_notes/translations/pt.json +++ b/custom_components/battery_notes/translations/pt.json @@ -20,6 +20,10 @@ "data_description": { "battery_low_threshold": "0 irá ser usado como valor por defeito para definir o descarregada" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { @@ -121,4 +125,4 @@ "name": "Substituir bateria" } } -} +} \ No newline at end of file diff --git a/custom_components/battery_notes/translations/ru.json b/custom_components/battery_notes/translations/ru.json index 37c232ec7..5d1e06fb1 100644 --- a/custom_components/battery_notes/translations/ru.json +++ b/custom_components/battery_notes/translations/ru.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 будет использовать общий порог по умолчанию", "battery_low_template": "Шаблон низкого заряда батареи. В случае низкого заряда должен возвращать true.\nТребуется только для нестандартных уровней заряда батареи." } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/sk.json b/custom_components/battery_notes/translations/sk.json index 23836fca1..340a5f6e8 100644 --- a/custom_components/battery_notes/translations/sk.json +++ b/custom_components/battery_notes/translations/sk.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 použije globálny predvolený prah", "battery_low_template": "Šablóna na určenie nízkej úrovne batérie by mala vrátiť hodnotu Pravda, ak je nízke\nPotrebné iba pri neštandardných úrovniach batérie" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/sr-Latn.json b/custom_components/battery_notes/translations/sr-Latn.json index 2d764b799..8d90831b4 100644 --- a/custom_components/battery_notes/translations/sr-Latn.json +++ b/custom_components/battery_notes/translations/sr-Latn.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 će koristiti globalni podrazumevani prag", "battery_low_template": "Šablon za određivanje da je baterija prazna, treba da vrati true ako je nizak nivo\nPotrebno samo za nestandardne nivoe baterije" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/sv-SE.json b/custom_components/battery_notes/translations/sv-SE.json index 70d07e618..684ed54cd 100644 --- a/custom_components/battery_notes/translations/sv-SE.json +++ b/custom_components/battery_notes/translations/sv-SE.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 kommer att använda det globala standardtröskelvärdet", "battery_low_template": "Template för att bestämma om batteriet är lågt, borde returnera true om lågt.\nEndast nödvändigt för icke-standard batterinivåer. " } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/custom_components/battery_notes/translations/ur-IN.json b/custom_components/battery_notes/translations/ur-IN.json index 9e8da33bd..c630f7397 100644 --- a/custom_components/battery_notes/translations/ur-IN.json +++ b/custom_components/battery_notes/translations/ur-IN.json @@ -41,6 +41,10 @@ "battery_low_threshold": "0 عالمی ڈیفالٹ حد استعمال کرے گا۔", "battery_low_template": "بیٹری کم ہونے کا تعین کرنے کے لیے ٹیمپلیٹ، کم ہونے کی صورت میں درست ہونا چاہیے\nصرف غیر معیاری بیٹری کی سطح کے لیے ضروری ہے" } + }, + "manual": { + "description": "This device is marked in the library as manual, variants use different battery types so it cannot be set in the library.\nThe next step will allow you to set your battery type but please do not submit a device request.", + "title": "Device manual configuration" } }, "abort": { diff --git a/docs/blueprints/battery_notes_battery_not_reported.yaml b/docs/blueprints/battery_notes_battery_not_reported.yaml index ca541622f..7f34c8901 100755 --- a/docs/blueprints/battery_notes_battery_not_reported.yaml +++ b/docs/blueprints/battery_notes_battery_not_reported.yaml @@ -20,6 +20,14 @@ blueprint: multiple: true entity: - integration: battery_notes + additional_conditions: + name: Additional conditions + description: | + Extra conditions you may want to add to this automation + (Example: Home occupied) + default: [] + selector: + condition: user_actions: name: User Actions description: User actions to run on battery not reported. @@ -37,6 +45,8 @@ condition: - condition: template value_template: |- {{ trigger.event.data.device_id not in excluded_devices}} + - alias: User pick + condition: !input additional_conditions action: - if: - condition: template diff --git a/docs/blueprints/battery_notes_battery_replaced.yaml b/docs/blueprints/battery_notes_battery_replaced.yaml index 2f9bb53b3..97e017503 100644 --- a/docs/blueprints/battery_notes_battery_replaced.yaml +++ b/docs/blueprints/battery_notes_battery_replaced.yaml @@ -6,6 +6,14 @@ blueprint: domain: automation input: + additional_conditions: + name: Additional conditions + description: | + Extra conditions you may want to add to this automation + (Example: Home occupied) + default: [] + selector: + condition: on_replaced_actions: name: On Replaced Actions description: User actions to run on battery replacement, the battery is marked as replaced automatically. Use any event data via trigger.event.data.xxx @@ -17,7 +25,9 @@ trigger: - platform: event event_type: battery_notes_battery_increased -condition: [] +condition: + - alias: User pick + condition: !input additional_conditions action: - service: battery_notes.set_battery_replaced diff --git a/docs/blueprints/battery_notes_battery_threshold.yaml b/docs/blueprints/battery_notes_battery_threshold.yaml index 4c66a1b5c..d0c848cce 100644 --- a/docs/blueprints/battery_notes_battery_threshold.yaml +++ b/docs/blueprints/battery_notes_battery_threshold.yaml @@ -14,7 +14,7 @@ blueprint: boolean: high_notification: name: Remove Low Notification - description: Remove the persistent notification when the battery is no longer low. Use any event data via trigger.event.data.xxx + description: Remove the persistent notification when the battery is no longer low. default: True selector: boolean: @@ -27,6 +27,14 @@ blueprint: multiple: true entity: - integration: battery_notes + additional_conditions: + name: Additional conditions + description: | + Extra conditions you may want to add to this automation + (Example: Home occupied) + default: [] + selector: + condition: on_low_actions: name: On Low Actions description: User actions to run on battery low. Use any event data via trigger.event.data.xxx @@ -63,6 +71,8 @@ condition: - condition: template value_template: |- {{ trigger.event.data.device_id not in excluded_devices}} + - alias: User pick + condition: !input additional_conditions action: - choose: diff --git a/docs/library.md b/docs/library.md index 477d12607..e220ac213 100644 --- a/docs/library.md +++ b/docs/library.md @@ -16,8 +16,9 @@ Upon submission using the form above GitHub will attempt to make the required co Fork the repository, add your device details to the JSON document `custom_components/battery_notes/data/library.json`, and then submit a pull request. Do not enable GitHub Actions (disabled by default) as this will mess with the pull request and are unnecessary for a library submission. -* The manufacturer and model should be exactly what is displayed on the Device screen within Home Assistant. -* The make & model names may be different between integrations such as Zigbee2MQTT and ZHA, if you see a similar device please duplicate the entry rather than changing it. +* The manufacturer and model should be exactly what is displayed on the Device screen within Home Assistant. If the Device screen has a Model ID or Hardware Version then this should be included. +* In some rare cases models could contain unique identifiers and you want the battery type to apply to all models that match a pattern, in these instances you can use the optional model_match_method attribute. For example Apple iPhone's have a model of iPhoneXX.X but all have a rechargeable battery, here you could use the "model_match_method": "startswith" and just specify iPhone as the model. +* The manufacturer & model names may be different between integrations such as Zigbee2MQTT and ZHA, if you see a similar device please duplicate the entry rather than changing it. * Please keep devices in alphabetical order by manufacturer/model. * The `battery_quantity` data is numeric (no quotes) and optional. If a device only requires a single battery, it should be omitted. * The `battery_type` data should follow the most common naming for general batteries (ex. AAA, D) and the IEC naming for battery cells according to [Wikipedia](https://en.wikipedia.org/wiki/List_of_battery_sizes) (ex. CR2032, 18650) @@ -30,10 +31,12 @@ For the example image below, your JSON entry will look like this: ``` { "manufacturer": "Philips", - "model": "Hue motion sensor (9290012607)", - "hw_version": "Some specific hardware detail", < Optional, only use if two devices have the same model and the hw_version are different. + "model": "Hue motion sensor", + "model_id": "9290012607", < Optional, only add it if your device shows it. + "hw_version": "Some specific hardware detail", < Optional, only add it if your device shows it. "battery_type": "AAA", - "battery_quantity": 2 < Only use if more than 1 battery + "battery_quantity": 2, < Only use if more than 1 battery + "model_match_method": "startswith|endswith|contains" < Only use if you are creating a model with unique identifier (ex. trailing serial numbers) }, ``` diff --git a/hacs.json b/hacs.json index dbf288766..cece89a43 100644 --- a/hacs.json +++ b/hacs.json @@ -2,7 +2,7 @@ "name": "Battery Notes", "filename": "battery_notes.zip", "hide_default_branch": true, - "homeassistant": "2024.4.4", + "homeassistant": "2024.5.0", "render_readme": true, "zip_release": true, "persistent_directory": "data" diff --git a/requirements.txt b/requirements.txt index 2e5764406..e14ea07b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ colorlog>=6.8.2,<7.0 -homeassistant==2024.4.4 +homeassistant==2024.5.0 ruff>=0.5.0,<0.6 \ No newline at end of file