From ceade24851479b8c9bc60b7b8bed74a7bdb927e9 Mon Sep 17 00:00:00 2001 From: RyanClark123 Date: Mon, 13 May 2024 13:41:51 +0100 Subject: [PATCH] Sensors now show unavailable, refactoring ### Feature * As per HA core patterns, certain sensors will now show as unavailable when sock is charging ### * Refactoring as per core maintainers suggestions --- custom_components/owlet/__init__.py | 12 +- custom_components/owlet/binary_sensor.py | 60 ++++++++-- custom_components/owlet/config_flow.py | 4 +- custom_components/owlet/manifest.json | 2 +- custom_components/owlet/sensor.py | 117 ++++++++++++------- custom_components/owlet/strings.json | 4 +- custom_components/owlet/translations/en.json | 6 +- custom_components/owlet/translations/uk.json | 4 +- 8 files changed, 141 insertions(+), 68 deletions(-) diff --git a/custom_components/owlet/__init__.py b/custom_components/owlet/__init__.py index 333cb86..79e1de9 100644 --- a/custom_components/owlet/__init__.py +++ b/custom_components/owlet/__init__.py @@ -1,4 +1,5 @@ """The Owlet Smart Sock integration.""" + from __future__ import annotations import asyncio @@ -72,15 +73,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, data={**entry.data, **devices["tokens"]} ) - socks = { - device["device"]["dsn"]: Sock(owlet_api, device["device"]) - for device in devices["response"] - } - scan_interval = entry.options.get(CONF_SCAN_INTERVAL) coordinators = { - serial: OwletCoordinator(hass, sock, scan_interval, entry) - for (serial, sock) in socks.items() + device["device"]["dsn"]: OwletCoordinator( + hass, Sock(owlet_api, device["device"]), scan_interval, entry + ) + for device in devices["response"] } await asyncio.gather( diff --git a/custom_components/owlet/binary_sensor.py b/custom_components/owlet/binary_sensor.py index 96c8b77..81e7bf9 100644 --- a/custom_components/owlet/binary_sensor.py +++ b/custom_components/owlet/binary_sensor.py @@ -1,4 +1,5 @@ """Support for Owlet binary sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -17,76 +18,85 @@ from .coordinator import OwletCoordinator from .entity import OwletBaseEntity -@dataclass +@dataclass(kw_only=True) class OwletBinarySensorEntityDescription(BinarySensorEntityDescription): """Represent the owlet binary sensor entity description.""" + available_during_charging: bool + SENSORS: tuple[OwletBinarySensorEntityDescription, ...] = ( OwletBinarySensorEntityDescription( key="charging", translation_key="charging", device_class=BinarySensorDeviceClass.BATTERY_CHARGING, + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="high_heart_rate_alert", translation_key="high_hr_alrt", device_class=BinarySensorDeviceClass.SOUND, + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="low_heart_rate_alert", translation_key="low_hr_alrt", device_class=BinarySensorDeviceClass.SOUND, + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="high_oxygen_alert", translation_key="high_ox_alrt", device_class=BinarySensorDeviceClass.SOUND, + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="low_oxygen_alert", translation_key="low_ox_alrt", device_class=BinarySensorDeviceClass.SOUND, + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="critical_oxygen_alert", translation_key="crit_ox_alrt", device_class=BinarySensorDeviceClass.SOUND, + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="low_battery_alert", translation_key="low_batt_alrt", device_class=BinarySensorDeviceClass.SOUND, + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="critical_battery_alert", translation_key="crit_batt_alrt", device_class=BinarySensorDeviceClass.SOUND, + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="lost_power_alert", translation_key="lost_pwr_alrt", device_class=BinarySensorDeviceClass.SOUND, + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="sock_disconnected", translation_key="sock_discon_alrt", device_class=BinarySensorDeviceClass.SOUND, + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="sock_off", translation_key="sock_off", device_class=BinarySensorDeviceClass.POWER, - ), - OwletBinarySensorEntityDescription( - key="sleep_state", - translation_key="awake", - icon="mdi:sleep", + available_during_charging=True, ), OwletBinarySensorEntityDescription( key="base_station_on", translation_key="base_on", device_class=BinarySensorDeviceClass.POWER, + available_during_charging=True, ), ) @@ -106,6 +116,9 @@ async def async_setup_entry( if sensor.key in coordinator.sock.properties: sensors.append(OwletBinarySensor(coordinator, sensor)) + if OwletAwakeSensor.entity_description.key in coordinator.sock.properties: + sensors.append(OwletAwakeSensor(coordinator)) + async_add_entities(sensors) @@ -122,11 +135,17 @@ class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity): self.entity_description = description self._attr_unique_id = f"{self.sock.serial}-{description.key}" + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and ( + not self.sock.properties["charging"] + or self.entity_description.available_during_charging + ) + @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - state = self.sock.properties[self.entity_description.key] - if self.entity_description.key == "sleep_state": if self.sock.properties["charging"]: return None @@ -135,4 +154,27 @@ class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity): else: state = True - return state + return self.sock.properties[self.entity_description.key] + + +class OwletAwakeSensor(OwletBinarySensor): + """Representation of an Owlet sleep sensor.""" + + entity_description = OwletBinarySensorEntityDescription( + key="sleep_state", + translation_key="awake", + icon="mdi:sleep", + available_during_charging=False, + ) + + def __init__( + self, + coordinator: OwletCoordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, self.entity_description) + + @property + def is_on(self) -> bool: + """Return true if the binary sensor is on.""" + return False if self.sock.properties[self.entity_description.key] in [8, 15] else True diff --git a/custom_components/owlet/config_flow.py b/custom_components/owlet/config_flow.py index 91ef91a..da52f29 100644 --- a/custom_components/owlet/config_flow.py +++ b/custom_components/owlet/config_flow.py @@ -130,9 +130,9 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.reauth_entry, data={**entry_data, **token} ) - await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) + await self.hass.config_entries.async_reload(self.reauth_entry.entry_id) - return self.async_abort(reason="reauth_successful") + return self.async_abort(reason="reauth_successful") except OwletPasswordError: errors[CONF_PASSWORD] = "invalid_password" diff --git a/custom_components/owlet/manifest.json b/custom_components/owlet/manifest.json index 2fb51c9..9bdc946 100644 --- a/custom_components/owlet/manifest.json +++ b/custom_components/owlet/manifest.json @@ -11,5 +11,5 @@ "requirements": [ "pyowletapi==2024.3.2" ], - "version": "2024.3.1" + "version": "2024.5.1" } \ No newline at end of file diff --git a/custom_components/owlet/sensor.py b/custom_components/owlet/sensor.py index 15697ea..6b55753 100644 --- a/custom_components/owlet/sensor.py +++ b/custom_components/owlet/sensor.py @@ -1,4 +1,5 @@ """Support for Owlet sensors.""" + from __future__ import annotations from dataclasses import dataclass @@ -25,10 +26,12 @@ from .coordinator import OwletCoordinator from .entity import OwletBaseEntity -@dataclass +@dataclass(kw_only=True) class OwletSensorEntityDescription(SensorEntityDescription): """Represent the owlet sensor entity description.""" + available_during_charging: bool + SENSORS: tuple[OwletSensorEntityDescription, ...] = ( OwletSensorEntityDescription( @@ -37,6 +40,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, device_class=SensorDeviceClass.BATTERY, state_class=SensorStateClass.MEASUREMENT, + available_during_charging=True, ), OwletSensorEntityDescription( key="oxygen_saturation", @@ -44,6 +48,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = ( native_unit_of_measurement=PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, icon="mdi:leaf", + available_during_charging=False, ), OwletSensorEntityDescription( key="heart_rate", @@ -51,13 +56,14 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = ( native_unit_of_measurement="bpm", state_class=SensorStateClass.MEASUREMENT, icon="mdi:heart-pulse", + available_during_charging=False, ), OwletSensorEntityDescription( key="battery_minutes", translation_key="batterymin", native_unit_of_measurement=UnitOfTime.MINUTES, device_class=SensorDeviceClass.DURATION, - state_class=SensorStateClass.MEASUREMENT, + available_during_charging=False, ), OwletSensorEntityDescription( key="signal_strength", @@ -65,6 +71,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = ( native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, device_class=SensorDeviceClass.SIGNAL_STRENGTH, state_class=SensorStateClass.MEASUREMENT, + available_during_charging=True, ), OwletSensorEntityDescription( key="skin_temperature", @@ -72,11 +79,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = ( native_unit_of_measurement=UnitOfTemperature.CELSIUS, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, - ), - OwletSensorEntityDescription( - key="sleep_state", - translation_key="sleepstate", - device_class=SensorDeviceClass.ENUM, + available_during_charging=False, ), OwletSensorEntityDescription( key="movement", @@ -84,13 +87,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, icon="mdi:cursor-move", entity_registry_enabled_default=False, - ), - OwletSensorEntityDescription( - key="oxygen_10_av", - translation_key="o2saturation10a", - native_unit_of_measurement=PERCENTAGE, - state_class=SensorStateClass.MEASUREMENT, - icon="mdi:leaf", + available_during_charging=False, ), OwletSensorEntityDescription( key="movement_bucket", @@ -98,6 +95,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = ( state_class=SensorStateClass.MEASUREMENT, icon="mdi:bucket-outline", entity_registry_enabled_default=False, + available_during_charging=False, ), ) @@ -120,6 +118,11 @@ async def async_setup_entry( if sensor.key in coordinator.sock.properties: sensors.append(OwletSensor(coordinator, sensor)) + if OwletSleepSensor.entity_description.key in coordinator.sock.properties: + sensors.append(OwletSleepSensor(coordinator)) + if OwletOxygenAverageSensor.entity_description.key in coordinator.sock.properties: + sensors.append(OwletOxygenAverageSensor(coordinator)) + async_add_entities(sensors) @@ -136,38 +139,68 @@ class OwletSensor(OwletBaseEntity, SensorEntity): self.entity_description: OwletSensorEntityDescription = description self._attr_unique_id = f"{self.sock.serial}-{description.key}" + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and ( + not self.sock.properties["charging"] + or self.entity_description.available_during_charging + ) + @property def native_value(self) -> StateType: """Return sensor value.""" - if ( - self.entity_description.key - in [ - "heart_rate", - "battery_minutes", - "oxygen_saturation", - "skin_temperature", - "oxygen_10_av", - "sleep_state", - ] - and self.sock.properties["charging"] - ): - return None - - if self.entity_description.key == "sleep_state": - return SLEEP_STATES[self.sock.properties["sleep_state"]] - - if self.entity_description.key == "oxygen_10_av": - val = self.sock.properties[self.entity_description.key] - if val is None or not isinstance(val, (int, float)) or val < 0 or val > 100: - return None - return val - return self.sock.properties[self.entity_description.key] + +class OwletSleepSensor(OwletSensor): + """Representation of an Owlet sleep sensor.""" + + _attr_options = list(SLEEP_STATES.values()) + entity_description = OwletSensorEntityDescription( + key="sleep_state", + translation_key="sleepstate", + device_class=SensorDeviceClass.ENUM, + available_during_charging=False, + ) + + def __init__( + self, + coordinator: OwletCoordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, self.entity_description) + @property - def options(self) -> list[str] | None: - """Set options for sleep state.""" - if self.entity_description.key != "sleep_state": - return None - return list(SLEEP_STATES.values()) + def native_value(self) -> StateType: + """Return sensor value.""" + return SLEEP_STATES[self.sock.properties["sleep_state"]] + + +class OwletOxygenAverageSensor(OwletSensor): + """Representation of an Owlet sleep sensor.""" + + entity_description = OwletSensorEntityDescription( + key="oxygen_10_av", + translation_key="o2saturation10a", + native_unit_of_measurement=PERCENTAGE, + icon="mdi:leaf", + available_during_charging=False, + ) + + def __init__( + self, + coordinator: OwletCoordinator, + ) -> None: + """Initialize the sensor.""" + super().__init__(coordinator, self.entity_description) + + @property + def available(self) -> bool: + """Return if entity is available.""" + return super().available and ( + not self.sock.properties["charging"] + or self.entity_description.available_during_charging + ) and (self.sock.properties["oxygen_10_av"] >= 0 and self.sock.properties["oxygen_10_av"] <= 100) + diff --git a/custom_components/owlet/strings.json b/custom_components/owlet/strings.json index 56c3550..3bf9f63 100644 --- a/custom_components/owlet/strings.json +++ b/custom_components/owlet/strings.json @@ -5,13 +5,13 @@ "data": { "region": "Region", "username": "Email", - "password": "Password" + "password": "[%key:common::config_flow::data::password%]" } }, "reauth_confirm": { "title": "Reauthentiaction required for Owlet", "data": { - "password": "Password" + "password": "[%key:common::config_flow::data::password%]" } } }, diff --git a/custom_components/owlet/translations/en.json b/custom_components/owlet/translations/en.json index 93e2e99..273a7a4 100644 --- a/custom_components/owlet/translations/en.json +++ b/custom_components/owlet/translations/en.json @@ -5,13 +5,13 @@ "data": { "region": "Region", "username": "Email", - "password": "Password" + "password": "[%key:common::config_flow::data::password%]" } }, "reauth_confirm": { "title": "Reauthentiaction required for Owlet", "data": { - "password": "Password" + "password": "[%key:common::config_flow::data::password%]" } } }, @@ -77,7 +77,7 @@ }, "base_on": { "name": "Base station on" - } + } }, "sensor": { "batterypercent": { diff --git a/custom_components/owlet/translations/uk.json b/custom_components/owlet/translations/uk.json index 56c3550..3bf9f63 100644 --- a/custom_components/owlet/translations/uk.json +++ b/custom_components/owlet/translations/uk.json @@ -5,13 +5,13 @@ "data": { "region": "Region", "username": "Email", - "password": "Password" + "password": "[%key:common::config_flow::data::password%]" } }, "reauth_confirm": { "title": "Reauthentiaction required for Owlet", "data": { - "password": "Password" + "password": "[%key:common::config_flow::data::password%]" } } },