diff --git a/custom_components/owlet/binary_sensor.py b/custom_components/owlet/binary_sensor.py index 9674335..0dd12ac 100644 --- a/custom_components/owlet/binary_sensor.py +++ b/custom_components/owlet/binary_sensor.py @@ -106,9 +106,11 @@ async def async_setup_entry( sensors = [] for coordinator in coordinators: - for sensor in SENSORS: - if sensor.key in coordinator.sock.properties: - sensors.append(OwletBinarySensor(coordinator, sensor)) + sensors = [ + OwletBinarySensor(coordinator, sensor) + for sensor in SENSORS + if sensor.key in coordinator.sock.properties + ] if OwletAwakeSensor.entity_description.key in coordinator.sock.properties: sensors.append(OwletAwakeSensor(coordinator)) @@ -140,13 +142,6 @@ class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity): @property def is_on(self) -> bool: """Return true if the binary sensor is on.""" - if self.entity_description.key == "sleep_state": - if self.sock.properties["charging"]: - return None - if state in [8, 15]: - state = False - else: - state = True return self.sock.properties[self.entity_description.key] @@ -171,8 +166,4 @@ class OwletAwakeSensor(OwletBinarySensor): @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 - ) + return self.sock.properties[self.entity_description.key] not in [8, 15] diff --git a/custom_components/owlet/config_flow.py b/custom_components/owlet/config_flow.py index da52f29..5c81c15 100644 --- a/custom_components/owlet/config_flow.py +++ b/custom_components/owlet/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Owlet Smart Sock integration.""" + from __future__ import annotations from collections.abc import Mapping @@ -15,19 +16,17 @@ from pyowletapi.exceptions import ( import voluptuous as vol from homeassistant import config_entries, exceptions -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigFlowResult from homeassistant.const import ( - CONF_API_TOKEN, CONF_PASSWORD, CONF_REGION, CONF_SCAN_INTERVAL, CONF_USERNAME, ) from homeassistant.core import callback -from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .const import CONF_OWLET_EXPIRY, CONF_OWLET_REFRESH, DOMAIN, POLLING_INTERVAL +from .const import DOMAIN, POLLING_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -51,7 +50,7 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle the initial step.""" errors: dict[str, str] = {} if user_input is not None: @@ -101,7 +100,9 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult: + async def async_step_reauth( + self, user_input: Mapping[str, Any] + ) -> ConfigFlowResult: """Handle reauth.""" self.reauth_entry = self.hass.config_entries.async_get_entry( self.context["entry_id"] @@ -110,7 +111,7 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_reauth_confirm( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Dialog that informs the user that reauth is required.""" assert self.reauth_entry is not None errors: dict[str, str] = {} @@ -130,7 +131,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") @@ -155,7 +158,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_init( self, user_input: dict[str, Any] | None = None - ) -> FlowResult: + ) -> ConfigFlowResult: """Handle options flow.""" if user_input is not None: return self.async_create_entry(title="", data=user_input) diff --git a/custom_components/owlet/entity.py b/custom_components/owlet/entity.py index d4d9810..b14f97b 100644 --- a/custom_components/owlet/entity.py +++ b/custom_components/owlet/entity.py @@ -1,11 +1,11 @@ """Base class for Owlet entities.""" +from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity from homeassistant.helpers.update_coordinator import CoordinatorEntity -from homeassistant.helpers.device_registry import DeviceInfo -from .coordinator import OwletCoordinator from .const import DOMAIN, MANUFACTURER +from .coordinator import OwletCoordinator class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity): diff --git a/custom_components/owlet/sensor.py b/custom_components/owlet/sensor.py index e493ea0..f054771 100644 --- a/custom_components/owlet/sensor.py +++ b/custom_components/owlet/sensor.py @@ -115,9 +115,11 @@ async def async_setup_entry( sensors = [] for coordinator in coordinators: - for sensor in SENSORS: - if sensor.key in coordinator.sock.properties: - sensors.append(OwletSensor(coordinator, sensor)) + sensors = [ + OwletSensor(coordinator, sensor) + for sensor in SENSORS + if sensor.key in coordinator.sock.properties + ] if OwletSleepSensor.entity_description.key in coordinator.sock.properties: sensors.append(OwletSleepSensor(coordinator)) diff --git a/custom_components/owlet/switch.py b/custom_components/owlet/switch.py index 32b83f4..39be5a9 100644 --- a/custom_components/owlet/switch.py +++ b/custom_components/owlet/switch.py @@ -2,12 +2,15 @@ from __future__ import annotations +from collections.abc import Callable, Coroutine from dataclasses import dataclass from datetime import timedelta from typing import Any -from homeassistant.config_entries import ConfigEntry +from pyowletapi.sock import Sock + from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -19,6 +22,26 @@ SCAN_INTERVAL = timedelta(seconds=5) PARALLEL_UPDATES = 0 +@dataclass(frozen=True, kw_only=True) +class OwletSwitchEntityDescription(SwitchEntityDescription): + """Describes Owlet switch entity.""" + + turn_on_fn: Callable[[Sock], Callable[[bool], Coroutine[Any, Any, None]]] + turn_off_fn: Callable[[Sock], Callable[[bool], Coroutine[Any, Any, None]]] + available_during_charging: bool + + +SWITCHES: tuple[OwletSwitchEntityDescription, ...] = ( + OwletSwitchEntityDescription( + key="base_station_on", + translation_key="base_on", + turn_on_fn=lambda sock: (lambda state: sock.control_base_station(state)), + turn_off_fn=lambda sock: (lambda state: sock.control_base_station(state)), + available_during_charging=False, + ), +) + + async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, @@ -29,30 +52,43 @@ async def async_setup_entry( switches = [] for coordinator in coordinators: - switches.append(OwletBaseSwitch(coordinator)) + switches = [OwletBaseSwitch(coordinator, switch) for switch in SWITCHES] async_add_entities(switches) class OwletBaseSwitch(OwletBaseEntity, SwitchEntity): """Defines a Owlet switch.""" - _attr_has_entity_name = True - _attr_translation_key = "base_on" + entity_description: OwletSwitchEntityDescription - def __init__(self, coordinator: OwletCoordinator) -> None: - """Initialize ecobee ventilator platform.""" + def __init__( + self, + coordinator: OwletCoordinator, + description: OwletSwitchEntityDescription, + ) -> None: + """Initialize owlet switch platform.""" super().__init__(coordinator) - self._attr_unique_id = f"{self.sock.serial}-base_station_on" + self.entity_description = description + self._attr_unique_id = f"{self.sock.serial}-{description.key}" self._attr_is_on = False @property - def is_on(self): - return self.sock.properties["base_station_on"] + 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 if switch is on or off.""" + return self.sock.properties[self.entity_description.key] async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the switch.""" - await self.sock.control_base_station(True) + await self.entity_description.turn_on_fn(self.sock)(True) async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the switch.""" - await self.sock.control_base_station(False) + await self.entity_description.turn_off_fn(self.sock)(False) diff --git a/custom_components/owlet/translations/en.json b/custom_components/owlet/translations/en.json index 1572952..1e1e4fb 100644 --- a/custom_components/owlet/translations/en.json +++ b/custom_components/owlet/translations/en.json @@ -1,87 +1,86 @@ { "config": { - "step": { - "user": { - "data": { - "region": "Region", - "username": "Email", - "password": "Password" - } - }, - "reauth_confirm": { - "title": "Reauthentiaction required for Owlet", - "data": { - "password": "Password" - } - } - }, - "error": { - "cannot_connect": "Failed to connect", - "invalid_email": "Entered email address is incorrect", - "invalid_password": "Entered password is incorrect", - "invalid_credentials": "Entered credentials are incorrect", - "unknown": "Unknown error occured" - }, "abort": { "already_configured": "Device already configured", "reauth_successful": "Reauthentication successful" - } - }, - "options": { + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_credentials": "Entered credentials are incorrect", + "invalid_email": "Entered email address is incorrect", + "invalid_password": "Entered password is incorrect", + "unknown": "Unknown error occured" + }, "step": { - "init": { - "title": "Configure options for Owlet", + "reauth_confirm": { "data": { - "pollinterval": "Polling interval in seconds, min 10" + "password": "Password" + }, + "title": "Reauthentiaction required for Owlet" + }, + "user": { + "data": { + "password": "Password", + "region": "Region", + "username": "Email" } } } }, "entity": { "binary_sensor": { + "awake": { + "name": "Awake" + }, "charging": { "name": "Charging" }, - "high_hr_alrt": { - "name": "High heart rate alert" - }, - "low_hr_alrt": { - "name": "Low heart rate alert" - }, - "high_ox_alrt": { - "name": "High oxygen alert" - }, - "low_ox_alrt": { - "name": "Low oxygen alert" - }, - "crit_ox_alrt": { - "name": "Critical oxygen alert" - }, - "low_batt_alrt": { - "name": "Low battery alert" - }, "crit_batt_alrt": { "name": "Critical battery alert" }, + "crit_ox_alrt": { + "name": "Critical oxygen alert" + }, + "high_hr_alrt": { + "name": "High heart rate alert" + }, + "high_ox_alrt": { + "name": "High oxygen alert" + }, "lost_pwr_alrt": { "name": "Lost power alert" }, + "low_batt_alrt": { + "name": "Low battery alert" + }, + "low_hr_alrt": { + "name": "Low heart rate alert" + }, + "low_ox_alrt": { + "name": "Low oxygen alert" + }, "sock_discon_alrt": { "name": "Sock disconnected alert" }, "sock_off": { "name": "Sock off" - }, - "awake": { - "name": "Awake" } }, "sensor": { + "batterymin": { + "name": "Battery remaining" + }, "batterypercent": { "name": "Battery percentage" }, - "signalstrength": { - "name": "Signal strength" + "heartrate": { + "name": "Heart rate" + }, + "movement": { + "name": "Movement" + }, + "movementbucket": { + "name": "Movement bucket" }, "o2saturation": { "name": "O2 saturation" @@ -89,11 +88,8 @@ "o2saturation10a": { "name": "O2 saturation 10 minute average" }, - "heartrate": { - "name": "Heart rate" - }, - "batterymin": { - "name": "Battery remaining" + "signalstrength": { + "name": "Signal strength" }, "skintemp": { "name": "Skin temperature" @@ -101,17 +97,11 @@ "sleepstate": { "name": "Sleep state", "state": { - "unknown": "Unknown", "awake": "Awake", + "deep_sleep": "Deep sleep", "light_sleep": "Light sleep", - "deep_sleep": "Deep sleep" + "unknown": "Unknown" } - }, - "movement": { - "name": "Movement" - }, - "movementbucket": { - "name": "Movement bucket" } }, "switch": { @@ -119,5 +109,15 @@ "name": "Base station on" } } + }, + "options": { + "step": { + "init": { + "data": { + "pollinterval": "Polling interval in seconds, min 10" + }, + "title": "Configure options for Owlet" + } + } } } \ No newline at end of file