Skip to content

Commit

Permalink
Fix battery level calculation for Nest Protect by adding support for …
Browse files Browse the repository at this point in the history
…battery voltage conversions (#283)

Co-authored-by: Ryan McGinty <[email protected]>
  • Loading branch information
iMicknl and rmcginty committed Jan 27, 2024
1 parent 943c1d1 commit 22623df
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 19 deletions.
2 changes: 1 addition & 1 deletion config/configuration.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
default_config:

logger:
default: info
default: error
logs:
custom_components.nest_protect: debug

Expand Down
2 changes: 2 additions & 0 deletions custom_components/nest_protect/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 0 additions & 11 deletions custom_components/nest_protect/entity.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"
22 changes: 22 additions & 0 deletions custom_components/nest_protect/pynest/enums.py
Original file line number Diff line number Diff line change
@@ -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
7 changes: 7 additions & 0 deletions custom_components/nest_protect/pynest/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import datetime
from typing import Any

from .enums import BucketType


@dataclass
class NestLimits:
Expand Down Expand Up @@ -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
Expand Down
62 changes: 57 additions & 5 deletions custom_components/nest_protect/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import PERCENTAGE, TEMP_CELSIUS
from homeassistant.helpers.entity import EntityCategory
Expand All @@ -19,23 +18,74 @@
from . import HomeAssistantNestProtectData
from .const import DOMAIN
from .entity import NestDescriptiveEntity
from .pynest.enums import BucketType


def milli_volt_to_percentage(state: int):
"""
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
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
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",
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=PERCENTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
bucket_type=BucketType.KRYPTONITE,
),
# 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",
value_fn=lambda state: state if state <= 100 else None,
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",
Expand All @@ -50,7 +100,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)
Expand All @@ -64,13 +113,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)
)
Expand Down
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
homeassistant==2023.12.0
homeassistant==2023.12.3
pre-commit
2 changes: 1 addition & 1 deletion requirements_test.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 22623df

Please sign in to comment.