From fab9db94119c452269f0258953323b2c274f2e74 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 16 Dec 2023 21:44:26 +0000 Subject: [PATCH 1/5] Update HA --- config/configuration.yaml | 2 +- requirements_dev.txt | 2 +- requirements_test.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/config/configuration.yaml b/config/configuration.yaml index 8feeb5f..a0e5735 100644 --- a/config/configuration.yaml +++ b/config/configuration.yaml @@ -1,7 +1,7 @@ default_config: logger: - default: info + default: error logs: custom_components.nest_protect: debug diff --git a/requirements_dev.txt b/requirements_dev.txt index 999b75f..a511f30 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,2 +1,2 @@ -homeassistant==2023.12.0 +homeassistant==2023.12.3 pre-commit \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt index 22ce793..b4e75df 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -2,5 +2,5 @@ pytest==7.4.3 pytest-socket==0.6.0 -pytest-homeassistant-custom-component==0.13.82 # 2023.12.0 +pytest-homeassistant-custom-component==0.13.85 # 2023.12.3 pytest-timeout==2.1.0 \ No newline at end of file From f376fd4b639c16d35630accda0b6de9ff3ba2f3d Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 16 Dec 2023 22:09:47 +0000 Subject: [PATCH 2/5] Remove extra code --- custom_components/nest_protect/entity.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/custom_components/nest_protect/entity.py b/custom_components/nest_protect/entity.py index a0f51ef..416398f 100644 --- a/custom_components/nest_protect/entity.py +++ b/custom_components/nest_protect/entity.py @@ -1,9 +1,6 @@ """Entity class for Nest Protect.""" from __future__ import annotations -from enum import unique - -from homeassistant.backports.enum import StrEnum from homeassistant.core import callback from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -118,11 +115,3 @@ def __init__( super().__init__(bucket, description, areas, client) self._attr_name = f"{super().name} {self.entity_description.name}" self._attr_unique_id = f"{super().unique_id}-{self.entity_description.key}" - - -# Used by state translations for sensor and select entities -@unique -class NestProtectDeviceClass(StrEnum): - """Device class for Nest Protect specific devices.""" - - NIGHT_LIGHT_BRIGHTNESS = "nest_protect__night_light_brightness" From fb44d6be74a20c6b961e91c3f81fd08b5341d69f Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 16 Dec 2023 22:37:44 +0000 Subject: [PATCH 3/5] Add support for battery voltage and improve battery level calculation Co-authored-by: Ryan McGinty --- custom_components/nest_protect/__init__.py | 2 + .../nest_protect/pynest/enums.py | 22 ++++++++ .../nest_protect/pynest/models.py | 7 +++ custom_components/nest_protect/sensor.py | 56 +++++++++++++++++-- 4 files changed, 81 insertions(+), 6 deletions(-) create mode 100644 custom_components/nest_protect/pynest/enums.py diff --git a/custom_components/nest_protect/__init__.py b/custom_components/nest_protect/__init__.py index 0b41e3c..ecab980 100644 --- a/custom_components/nest_protect/__init__.py +++ b/custom_components/nest_protect/__init__.py @@ -74,8 +74,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): client = NestClient(session=session, environment=NEST_ENVIRONMENTS[account_type]) try: + # Using user-retrieved cookies for authentication if issue_token and cookies: auth = await client.get_access_token_from_cookies(issue_token, cookies) + # Using refresh_token from legacy authentication method elif refresh_token: auth = await client.get_access_token_from_refresh_token(refresh_token) else: diff --git a/custom_components/nest_protect/pynest/enums.py b/custom_components/nest_protect/pynest/enums.py new file mode 100644 index 0000000..77f780b --- /dev/null +++ b/custom_components/nest_protect/pynest/enums.py @@ -0,0 +1,22 @@ +"""Enums for Nest Protect.""" +from enum import StrEnum, unique +import logging + +_LOGGER = logging.getLogger(__name__) + + +@unique +class BucketType(StrEnum): + """Bucket types.""" + + KRYPTONITE = "kryptonite" + TOPAZ = "topaz" + WHERE = "where" + + UNKNOWN = "unknown" + + @classmethod + def _missing_(cls, value): # type: ignore + _LOGGER.warning(f"Unsupported value {value} has been returned for {cls}") + + return cls.UNKNOWN diff --git a/custom_components/nest_protect/pynest/models.py b/custom_components/nest_protect/pynest/models.py index fe35b93..eb48f39 100644 --- a/custom_components/nest_protect/pynest/models.py +++ b/custom_components/nest_protect/pynest/models.py @@ -5,6 +5,8 @@ import datetime from typing import Any +from .enums import BucketType + @dataclass class NestLimits: @@ -72,6 +74,11 @@ class Bucket: object_revision: str object_timestamp: str value: Any + type: str = "" + + def __post_init__(self): + """Set the expiry date during post init.""" + self.type = BucketType(self.object_key.split(".")[0]) @dataclass diff --git a/custom_components/nest_protect/sensor.py b/custom_components/nest_protect/sensor.py index dcb0892..0d242ec 100644 --- a/custom_components/nest_protect/sensor.py +++ b/custom_components/nest_protect/sensor.py @@ -10,15 +10,36 @@ SensorDeviceClass, SensorEntity, SensorEntityDescription, - SensorStateClass, ) -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, UnitOfElectricPotential from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.typing import StateType from . import HomeAssistantNestProtectData from .const import DOMAIN from .entity import NestDescriptiveEntity +from .pynest.enums import BucketType + + +def milli_volt_to_percentage(state: int): + """Calculate battery level if device is reporting in mV.""" + if 3000 < state <= 6000: + if 4950 < state <= 6000: + slope = 0.001816609 + yint = -8.548096886 + elif 4800 < state <= 4950: + slope = 0.000291667 + yint = -0.991176471 + elif 4500 < state <= 4800: + slope = 0.001077342 + yint = -4.730392157 + else: + slope = 0.000434641 + yint = -1.825490196 + + return max(0, min(100, round(((slope * state) + yint) * 100))) + + return None @dataclass @@ -26,16 +47,37 @@ class NestProtectSensorDescription(SensorEntityDescription): """Class to describe an Nest Protect sensor.""" value_fn: Callable[[Any], StateType] | None = None + bucket_type: BucketType | None = ( + None # used to filter out sensors that are not supported by the device + ) -SENSOR_DESCRIPTIONS: list[SensorEntityDescription] = [ +SENSOR_DESCRIPTIONS: list[NestProtectSensorDescription] = [ NestProtectSensorDescription( key="battery_level", name="Battery Level", - value_fn=lambda state: state if state <= 100 else None, device_class=SensorDeviceClass.BATTERY, native_unit_of_measurement=PERCENTAGE, entity_category=EntityCategory.DIAGNOSTIC, + bucket_type=BucketType.KRYPTONITE, + ), + NestProtectSensorDescription( + key="battery_level", + name="Battery Voltage", + value_fn=lambda state: round(state / 1000, 3), + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=UnitOfElectricPotential.VOLT, + entity_category=EntityCategory.DIAGNOSTIC, + bucket_type=BucketType.TOPAZ, + ), + NestProtectSensorDescription( + key="battery_level", + name="Battery Level", + value_fn=lambda state: milli_volt_to_percentage(state), + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=PERCENTAGE, + entity_category=EntityCategory.DIAGNOSTIC, + bucket_type=BucketType.TOPAZ, ), NestProtectSensorDescription( name="Replace By", @@ -50,7 +92,6 @@ class NestProtectSensorDescription(SensorEntityDescription): value_fn=lambda state: round(state, 2), device_class=SensorDeviceClass.TEMPERATURE, native_unit_of_measurement=TEMP_CELSIUS, - state_class=SensorStateClass.MEASUREMENT, ), # TODO Add Color Status (gray, green, yellow, red) # TODO Smoke Status (OK, Warning, Emergency) @@ -64,13 +105,16 @@ async def async_setup_entry(hass, entry, async_add_devices): data: HomeAssistantNestProtectData = hass.data[DOMAIN][entry.entry_id] entities: list[NestProtectSensor] = [] - SUPPORTED_KEYS = { + SUPPORTED_KEYS: dict[str, NestProtectSensorDescription] = { description.key: description for description in SENSOR_DESCRIPTIONS } for device in data.devices.values(): for key in device.value: if description := SUPPORTED_KEYS.get(key): + if description.bucket_type and device.type != description.bucket_type: + continue + entities.append( NestProtectSensor(device, description, data.areas, data.client) ) From 2bb54f1ccda8b5ba24b161454856f07db1a882cf Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 16 Dec 2023 22:40:51 +0000 Subject: [PATCH 4/5] Improve comments --- custom_components/nest_protect/sensor.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/custom_components/nest_protect/sensor.py b/custom_components/nest_protect/sensor.py index 0d242ec..cd0efc7 100644 --- a/custom_components/nest_protect/sensor.py +++ b/custom_components/nest_protect/sensor.py @@ -22,7 +22,14 @@ def milli_volt_to_percentage(state: int): - """Calculate battery level if device is reporting in mV.""" + """ + Convert battery level in mV to a percentage. + + The battery life percentage in devices is estimated using slopes from the L91 battery's datasheet. + This is a rough estimation, and the battery life percentage is not linear. + + Tests on various devices have shown accurate results. + """ if 3000 < state <= 6000: if 4950 < state <= 6000: slope = 0.001816609 From 5aa68adb064caf4d6f15cda0fd6baf575b0be280 Mon Sep 17 00:00:00 2001 From: Mick Vleeshouwer Date: Sat, 16 Dec 2023 22:45:03 +0000 Subject: [PATCH 5/5] Add todo --- custom_components/nest_protect/sensor.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/custom_components/nest_protect/sensor.py b/custom_components/nest_protect/sensor.py index cd0efc7..9e6bc7f 100644 --- a/custom_components/nest_protect/sensor.py +++ b/custom_components/nest_protect/sensor.py @@ -11,7 +11,7 @@ SensorEntity, SensorEntityDescription, ) -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, UnitOfElectricPotential +from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.typing import StateType @@ -68,15 +68,16 @@ class NestProtectSensorDescription(SensorEntityDescription): entity_category=EntityCategory.DIAGNOSTIC, bucket_type=BucketType.KRYPTONITE, ), - NestProtectSensorDescription( - key="battery_level", - name="Battery Voltage", - value_fn=lambda state: round(state / 1000, 3), - device_class=SensorDeviceClass.BATTERY, - native_unit_of_measurement=UnitOfElectricPotential.VOLT, - entity_category=EntityCategory.DIAGNOSTIC, - bucket_type=BucketType.TOPAZ, - ), + # TODO Due to duplicate keys, this sensor is not available yet + # NestProtectSensorDescription( + # key="battery_level", + # name="Battery Voltage", + # value_fn=lambda state: round(state / 1000, 3), + # device_class=SensorDeviceClass.BATTERY, + # native_unit_of_measurement=UnitOfElectricPotential.VOLT, + # entity_category=EntityCategory.DIAGNOSTIC, + # bucket_type=BucketType.TOPAZ, + # ), NestProtectSensorDescription( key="battery_level", name="Battery Level",