23 Commits

Author SHA1 Message Date
ryanbdclark
1cfff537d7 Update CHANGELOG.md 2023-11-16 11:45:00 +00:00
RyanClark123
3acf847352 Correcting error where properties may not exist
### Fix
* Bumping pyowletapi to 2023.11.1
* Sensors and binary sensors are now only created where the sock contains that property, this stops errors where different sock versions have different properties
2023-11-16 11:42:34 +00:00
ryanbdclark
c0bf404f6a Update CHANGELOG.md 2023-09-20 15:05:45 +01:00
RyanClark123
0a7f703100 Fix for new sock revision 5
### Fix
* Bumping pyowletapi to 2023.9.1 to allow for revisions
* New revision of sock, revision 5 doesn't report all vitals as before, this would cause the integration to fail to update. Have adjusted the integration to detect the revision and ignore the vitals that are no longer reported
2023-09-20 15:03:10 +01:00
RyanClark123
cab737cae4 Merge branch 'main' of https://github.com/ryanbdclark/owlet 2023-08-21 15:11:14 +01:00
RyanClark123
092321cbae Update manifest.json 2023-08-21 15:10:57 +01:00
ryanbdclark
835786b89b Update CHANGELOG.md 2023-08-21 15:10:38 +01:00
RyanClark123
2cd46c18f8 Refactoring
###Fix
* Refactoring done based on home assistant style guidelines and suggestions submitted on the core pull request version of this integration
* Added new sensors to strings json and disabled by default
2023-08-21 15:06:28 +01:00
ryanbdclark
575b213ddd Merge pull request #3 from seanford/main
Update sensor.py
2023-08-21 11:49:48 +01:00
Sean Ford
3afa43c82c Update sensor.py
Added Movement and Movement Bucket values as sensors
2023-08-10 10:09:47 -04:00
ryanbdclark
5c8411fab7 Update CHANGELOG.md 2023-07-04 15:42:31 +01:00
RyanClark123
c45959b123 Bumping pyowlet
###Fix
* Bumping pyowlet version to 2023.7.2
2023-07-04 15:38:44 +01:00
ryanbdclark
4e30d4652f Update cron.yml 2023-07-04 13:52:42 +01:00
ryanbdclark
02f8679ed1 Update manifest.json 2023-07-03 14:52:04 +01:00
ryanbdclark
534ad8a351 Update CHANGELOG.md 2023-07-03 14:51:14 +01:00
ryanbdclark
c693fefbf3 Update manifest.json 2023-07-03 14:38:17 +01:00
ryanbdclark
523ba949dd Bumping pyowlet api
Bumping pyowletapi version to 2023.07.01
2023-07-03 14:37:49 +01:00
RyanClark123
3c35d87fd2 Minor changes to description
Minor changes to description of entities made
2023-06-15 11:11:06 +01:00
ryanbdclark
6c2c531a19 Update manifest.json 2023-05-30 14:30:23 +01:00
ryanbdclark
f4e38ec521 Update manifest.json 2023-05-30 14:27:07 +01:00
ryanbdclark
ecb950da8a Create cron.yml 2023-05-30 14:23:23 +01:00
RyanClark123
a7d4276671 Merge branch 'main' of https://github.com/ryanbdclark/owlet 2023-05-30 13:57:30 +01:00
RyanClark123
ef0a3c3ddb Merge branch 'main' of https://github.com/ryanbdclark/owlet 2023-05-30 13:57:14 +01:00
12 changed files with 308 additions and 276 deletions

25
.github/workflows/cron.yml vendored Normal file
View File

@@ -0,0 +1,25 @@
name: Validate with hassfest
on:
push:
pull_request:
schedule:
- cron: '0 0 * * *'
jobs:
hassfest:
name: "Hassfest validation"
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- uses: "home-assistant/actions/hassfest@master"
hacs:
name: "HACS Action"
runs-on: "ubuntu-latest"
steps:
- name: "HACS Action"
uses: "hacs/action@main"
with:
category: "integration"

View File

@@ -1,11 +1,38 @@
# Changelog # Changelog
<!--next-version-placeholder--> <!--next-version-placeholder-->
## 2023.05.7 (2023-05-30) ## 2023.11.1 (2023-11-16)
### Fix
* Bumping pyowletapi to 2023.11.1 ([`3acf847`](https://github.com/ryanbdclark/owlet/commit/3acf8473526665382b44ef6325d708a6c62fff45))
* Sensors and binary sensors are now only created where the sock contains that property, this stops errors where different sock versions have different properties ([`3acf847`](https://github.com/ryanbdclark/owlet/commit/3acf8473526665382b44ef6325d708a6c62fff45))
## 2023.9.1 (2023-09-20)
### Fix
* Bumping pyowletapi to 2023.9.1 to allow for revisions ([`0a7f703`](https://github.com/ryanbdclark/owlet/commit/0a7f70310080a129c988e9607331baa2f6c691e0))
* New revision of sock, revision 5 doesn't report all vitals as before, this would cause the integration to fail to update. Have adjusted the integration to detect the revision and ignore the vitals that are no longer reported ([`0a7f703`](https://github.com/ryanbdclark/owlet/commit/0a7f70310080a129c988e9607331baa2f6c691e0))
## 2023.8.1 (2023-08-21)
### Feature
* 2 new sensors, movement and movement bucket disabled by default, thanks [`@seanford`](https://github.com/seanford) ([`575b213`](https://github.com/ryanbdclark/owlet/commit/575b213ddd732779cd7938e575fc87c8881a69b0))
### Fix
* Various refactoring tasks completed to make this integration more inline with home assistants style guidelines ([`2cd46c1`](https://github.com/ryanbdclark/owlet/commit/c45959b123a6e5f77747475f11d3d3ab67859756))
* Added new sensors to strings jsons ([`2cd46c1`](https://github.com/ryanbdclark/owlet/commit/c45959b123a6e5f77747475f11d3d3ab67859756))
## 2023.7.2 (2023-07-04)
### Fix
* Bumping pyowletapi version to 2023.7.2 ([`c45959b`](https://github.com/ryanbdclark/owlet/commit/c45959b123a6e5f77747475f11d3d3ab67859756))
## 2023.7.1 (2023-07-03)
### Fix
* Bumping pyowletapi to 2023.7.1 ([`c693fef`](https://github.com/ryanbdclark/owlet/commit/c693fefbf3dba8f35802b87d064401dadbb211b5))
## 2023.5.7 (2023-05-30)
### Fix ### Fix
* Fixed issue with binary sensors not loading, caused by change to way the coordinators are stored ([`8d17317`](https://github.com/ryanbdclark/owlet/commit/8d173174e286b0451cbb2c0d4ae3087028d1ea23)) * Fixed issue with binary sensors not loading, caused by change to way the coordinators are stored ([`8d17317`](https://github.com/ryanbdclark/owlet/commit/8d173174e286b0451cbb2c0d4ae3087028d1ea23))
## 2023.05.6 (2023-05-30) ## 2023.5.6 (2023-05-30)
### Fix ### Fix
* In light of submitting this as a pull request to the core of HA there have been some refactoring changes to comply with HA's style requirements * In light of submitting this as a pull request to the core of HA there have been some refactoring changes to comply with HA's style requirements
* Sensor names now moved to strings file to allow for translations * Sensor names now moved to strings file to allow for translations
@@ -15,25 +42,25 @@
### Feature ### Feature
* Tests added * Tests added
## 2023.05.5 (2023-05-19) ## 2023.5.5 (2023-05-19)
#### Fix #### Fix
* Owlet refresh token becomes invalid after 24 hours. Meant that after 1 day the integration would stop working. Moved to pyowletapi v2023.5.28 which uses different refresh token, should no longer need reconfiguring after 24 hours ([`dc58b19`](https://github.com/ryanbdclark/owlet/commit/0141f7d01a9ac9b3e1dcc74cabb896e19bd4a821)) * Owlet refresh token becomes invalid after 24 hours. Meant that after 1 day the integration would stop working. Moved to pyowletapi v2023.5.28 which uses different refresh token, should no longer need reconfiguring after 24 hours ([`dc58b19`](https://github.com/ryanbdclark/owlet/commit/0141f7d01a9ac9b3e1dcc74cabb896e19bd4a821))
## 2023.05.4 (2023-05-17) ## 2023.5.4 (2023-05-17)
#### Fix #### Fix
* Bumping to pyowletapi 2023.5.25 * Bumping to pyowletapi 2023.5.25
## 2023.05.3 (2023-05-17) ## 2023.5.3 (2023-05-17)
#### Fix #### Fix
* Bumping to pyowletapi 2023.5.24 * Bumping to pyowletapi 2023.5.24
* Reauthing now no longer re adds users' password to config entry * Reauthing now no longer re adds users' password to config entry
## 2023.05.2 (2023-05-16) ## 2023.5.2 (2023-05-16)
#### Feature #### Feature
* Integration now makes use of refresh token from pyowletapi to reauthenticate, user password in no longer stored by integration ([`dc710a1`](https://github.com/ryanbdclark/owlet/commit/dc710a1783a4cad9d6cf355240fe12ac779a87ef)) * Integration now makes use of refresh token from pyowletapi to reauthenticate, user password in no longer stored by integration ([`dc710a1`](https://github.com/ryanbdclark/owlet/commit/dc710a1783a4cad9d6cf355240fe12ac779a87ef))
* New sensors create for baby sleep state ([`9b3392b`](https://github.com/ryanbdclark/owlet/commit/9b3392bdbcd82015ed31d3a50a517e4e22905684)) * New sensors create for baby sleep state ([`9b3392b`](https://github.com/ryanbdclark/owlet/commit/9b3392bdbcd82015ed31d3a50a517e4e22905684))
## 2023.05.1 (2023-05-15) ## 2023.5.1 (2023-05-15)
#### Feature #### Feature
* Changed versioning to date based * Changed versioning to date based
### Fix ### Fix

View File

@@ -79,7 +79,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
scan_interval = entry.options.get(CONF_SCAN_INTERVAL) scan_interval = entry.options.get(CONF_SCAN_INTERVAL)
coordinators = { coordinators = {
serial: OwletCoordinator(hass, sock, scan_interval) serial: OwletCoordinator(hass, sock, scan_interval, entry)
for (serial, sock) in socks.items() for (serial, sock) in socks.items()
} }

View File

@@ -18,16 +18,7 @@ from .entity import OwletBaseEntity
@dataclass @dataclass
class OwletBinarySensorEntityMixin: class OwletBinarySensorEntityDescription(BinarySensorEntityDescription):
"""Owlet binary sensor element mixin"""
element: str
@dataclass
class OwletBinarySensorEntityDescription(
BinarySensorEntityDescription, OwletBinarySensorEntityMixin
):
"""Represent the owlet binary sensor entity description.""" """Represent the owlet binary sensor entity description."""
@@ -36,60 +27,50 @@ SENSORS: tuple[OwletBinarySensorEntityDescription, ...] = (
key="charging", key="charging",
translation_key="charging", translation_key="charging",
device_class=BinarySensorDeviceClass.BATTERY_CHARGING, device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
element="charging",
), ),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="highhr", key="high_heart_rate_alert",
translation_key="high_hr_alrt", translation_key="high_hr_alrt",
device_class=BinarySensorDeviceClass.SOUND, device_class=BinarySensorDeviceClass.SOUND,
element="high_heart_rate_alert",
), ),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="lowhr", key="low_heart_rate_alert",
translation_key="low_hr_alrt", translation_key="low_hr_alrt",
device_class=BinarySensorDeviceClass.SOUND, device_class=BinarySensorDeviceClass.SOUND,
element="low_heart_rate_alert",
), ),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="higho2", key="high_oxygen_alert",
translation_key="high_ox_alrt", translation_key="high_ox_alrt",
device_class=BinarySensorDeviceClass.SOUND, device_class=BinarySensorDeviceClass.SOUND,
element="high_oxygen_alert",
), ),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="lowo2", key="low_oxygen_alert",
translation_key="low_ox_alrt", translation_key="low_ox_alrt",
device_class=BinarySensorDeviceClass.SOUND, device_class=BinarySensorDeviceClass.SOUND,
element="low_oxygen_alert",
), ),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="lowbattery", key="low_battery_alert",
translation_key="low_batt_alrt", translation_key="low_batt_alrt",
device_class=BinarySensorDeviceClass.SOUND, device_class=BinarySensorDeviceClass.SOUND,
element="low_battery_alert",
), ),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="lostpower", key="lost_power_alert",
translation_key="lost_pwr_alrt", translation_key="lost_pwr_alrt",
device_class=BinarySensorDeviceClass.SOUND, device_class=BinarySensorDeviceClass.SOUND,
element="lost_power_alert",
), ),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="sockdisconnected", key="sock_disconnected",
translation_key="sock_discon_alrt", translation_key="sock_discon_alrt",
device_class=BinarySensorDeviceClass.SOUND, device_class=BinarySensorDeviceClass.SOUND,
element="sock_disconnected",
), ),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="sock_off", key="sock_off",
translation_key="sock_off", translation_key="sock_off",
device_class=BinarySensorDeviceClass.POWER, device_class=BinarySensorDeviceClass.POWER,
element="sock_off",
), ),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="awake", key="sleep_state",
translation_key="awake", translation_key="awake",
element="sleep_state",
icon="mdi:sleep", icon="mdi:sleep",
), ),
) )
@@ -102,14 +83,15 @@ async def async_setup_entry(
) -> None: ) -> None:
"""Set up the owlet sensors from config entry.""" """Set up the owlet sensors from config entry."""
coordinators: OwletCoordinator = hass.data[DOMAIN][config_entry.entry_id] coordinators: OwletCoordinator = hass.data[DOMAIN][config_entry.entry_id].values()
async_add_entities( sensors = []
OwletBinarySensor(coordinator, sensor) for coordinator in coordinators:
for coordinator in coordinators for sensor in SENSORS:
for sensor in SENSORS if sensor.key in coordinator.sock.properties:
) sensors.append(OwletBinarySensor(coordinator, sensor))
async_add_entities(sensors)
class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity): class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
"""Representation of an Owlet binary sensor.""" """Representation of an Owlet binary sensor."""
@@ -117,19 +99,19 @@ class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
def __init__( def __init__(
self, self,
coordinator: OwletCoordinator, coordinator: OwletCoordinator,
sensor_description: OwletBinarySensorEntityDescription, description: OwletBinarySensorEntityDescription,
) -> None: ) -> None:
"""Initialize the binary sensor.""" """Initialize the binary sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description = sensor_description self.entity_description = description
self._attr_unique_id = f"{self.sock.serial}-{self.entity_description.name}" self._attr_unique_id = f"{self.sock.serial}-{description.key}"
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return true if the binary sensor is on.""" """Return true if the binary sensor is on."""
state = self.sock.properties[self.entity_description.element] state = self.sock.properties[self.entity_description.key]
if self.entity_description.element == "sleep_state": if self.entity_description.key == "sleep_state":
if self.sock.properties["charging"]: if self.sock.properties["charging"]:
return None return None
if state in [8, 15]: if state in [8, 15]:

View File

@@ -34,9 +34,9 @@ _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema( STEP_USER_DATA_SCHEMA = vol.Schema(
{ {
vol.Required("region"): vol.In(["europe", "world"]), vol.Required(CONF_REGION): vol.In(["europe", "world"]),
vol.Required("username"): str, vol.Required(CONF_USERNAME): str,
vol.Required("password"): str, vol.Required(CONF_PASSWORD): str,
} }
) )
@@ -45,15 +45,10 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Owlet Smart Sock.""" """Handle a config flow for Owlet Smart Sock."""
VERSION = 1 VERSION = 1
reauth_entry: ConfigEntry | None = None
def __init__(self) -> None: def __init__(self) -> None:
"""Initialise config flow.""" """Initialise config flow."""
self._entry: ConfigEntry
self._region: str
self._username: str
self._password: str
self._devices: dict[str, Sock]
self.reauth_entry: ConfigEntry | None = None
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@@ -61,18 +56,14 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle the initial step.""" """Handle the initial step."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
self._region = user_input[CONF_REGION]
self._username = user_input[CONF_USERNAME]
self._password = user_input[CONF_PASSWORD]
owlet_api = OwletAPI( owlet_api = OwletAPI(
region=self._region, region=user_input[CONF_REGION],
user=self._username, user=user_input[CONF_USERNAME],
password=self._password, password=user_input[CONF_PASSWORD],
session=async_get_clientsession(self.hass), session=async_get_clientsession(self.hass),
) )
await self.async_set_unique_id(self._username.lower()) await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
try: try:
@@ -82,9 +73,9 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except OwletDevicesError: except OwletDevicesError:
errors["base"] = "no_devices" errors["base"] = "no_devices"
except OwletEmailError: except OwletEmailError:
errors["base"] = "invalid_email" errors[CONF_USERNAME] = "invalid_email"
except OwletPasswordError: except OwletPasswordError:
errors["base"] = "invalid_password" errors[CONF_PASSWORD] = "invalid_password"
except OwletCredentialsError: except OwletCredentialsError:
errors["base"] = "invalid_credentials" errors["base"] = "invalid_credentials"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
@@ -92,10 +83,10 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown" errors["base"] = "unknown"
else: else:
return self.async_create_entry( return self.async_create_entry(
title=self._username, title=user_input[CONF_USERNAME],
data={ data={
CONF_REGION: self._region, CONF_REGION: user_input[CONF_REGION],
CONF_USERNAME: self._username, CONF_USERNAME: user_input[CONF_PASSWORD],
CONF_API_TOKEN: token[CONF_API_TOKEN], CONF_API_TOKEN: token[CONF_API_TOKEN],
CONF_OWLET_EXPIRY: token[CONF_OWLET_EXPIRY], CONF_OWLET_EXPIRY: token[CONF_OWLET_EXPIRY],
CONF_OWLET_REFRESH: token[CONF_OWLET_REFRESH], CONF_OWLET_REFRESH: token[CONF_OWLET_REFRESH],
@@ -147,7 +138,7 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
return self.async_abort(reason="reauth_successful") return self.async_abort(reason="reauth_successful")
except OwletPasswordError: except OwletPasswordError:
errors["base"] = "invalid_password" errors[CONF_PASSWORD] = "invalid_password"
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error reauthenticating") _LOGGER.exception("Error reauthenticating")

View File

@@ -15,10 +15,9 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL from homeassistant.const import CONF_EMAIL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, MANUFACTURER from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -26,7 +25,9 @@ _LOGGER = logging.getLogger(__name__)
class OwletCoordinator(DataUpdateCoordinator): class OwletCoordinator(DataUpdateCoordinator):
"""Coordinator is responsible for querying the device at a specified route.""" """Coordinator is responsible for querying the device at a specified route."""
def __init__(self, hass: HomeAssistant, sock: Sock, interval) -> None: def __init__(
self, hass: HomeAssistant, sock: Sock, interval, entry: ConfigEntry
) -> None:
"""Initialise a custom coordinator.""" """Initialise a custom coordinator."""
super().__init__( super().__init__(
hass, hass,
@@ -34,17 +35,8 @@ class OwletCoordinator(DataUpdateCoordinator):
name=DOMAIN, name=DOMAIN,
update_interval=timedelta(seconds=interval), update_interval=timedelta(seconds=interval),
) )
assert self.config_entry is not None
self.config_entry: ConfigEntry
self.sock = sock self.sock = sock
self.device_info = DeviceInfo( self.config_entry = entry
identifiers={(DOMAIN, sock.serial)},
name="Owlet Baby Care Sock",
manufacturer=MANUFACTURER,
model=sock.model,
sw_version=sock.sw_version,
hw_version=sock.version,
)
async def _async_update_data(self) -> None: async def _async_update_data(self) -> None:
"""Fetch the data from the device.""" """Fetch the data from the device."""

View File

@@ -2,13 +2,17 @@
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.entity import DeviceInfo
from .coordinator import OwletCoordinator from .coordinator import OwletCoordinator
from .const import DOMAIN, MANUFACTURER
class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity): class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
"""Base class for Owlet Sock entities.""" """Base class for Owlet Sock entities."""
_attr_has_entity_name = True
def __init__( def __init__(
self, self,
coordinator: OwletCoordinator, coordinator: OwletCoordinator,
@@ -16,5 +20,15 @@ class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
"""Initialize the base entity.""" """Initialize the base entity."""
super().__init__(coordinator) super().__init__(coordinator)
self.sock = coordinator.sock self.sock = coordinator.sock
self._attr_device_info = coordinator.device_info
self._attr_has_entity_name = True @property
def device_info(self) -> DeviceInfo:
"""Return the device info of the device"""
return DeviceInfo(
identifiers={(DOMAIN, self.sock.serial)},
name="Owlet Baby Care Sock",
manufacturer=MANUFACTURER,
model=self.sock.model,
sw_version=self.sock.sw_version,
hw_version=f"{self.sock.version}r{self.sock.revision}",
)

View File

@@ -1,10 +1,15 @@
{ {
"domain": "owlet", "domain": "owlet",
"name": "Owlet Smart Sock", "name": "Owlet Smart Sock",
"codeowners": ["@ryanbdclark"], "codeowners": [
"@ryanbdclark"
],
"config_flow": true, "config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/owlet", "documentation": "https://www.home-assistant.io/integrations/owlet",
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"requirements": ["pyowletapi==2023.5.30"], "issue_tracker": "https://github.com/ryanbdclark/owlet/issues",
"version":"2023.5.7" "requirements": [
"pyowletapi==2023.11.1"
],
"version": "2023.11.1"
} }

View File

@@ -26,75 +26,78 @@ from .entity import OwletBaseEntity
@dataclass @dataclass
class OwletSensorEntityDescriptionMixin: class OwletSensorEntityDescription(SensorEntityDescription):
"""Owlet sensor description mix in."""
element: str
@dataclass
class OwletSensorEntityDescription(
SensorEntityDescription, OwletSensorEntityDescriptionMixin
):
"""Represent the owlet sensor entity description.""" """Represent the owlet sensor entity description."""
SENSORS: tuple[OwletSensorEntityDescription, ...] = ( SENSORS: tuple[OwletSensorEntityDescription, ...] = (
OwletSensorEntityDescription( OwletSensorEntityDescription(
key="batterypercentage", key="battery_percentage",
translation_key="batterypercent", translation_key="batterypercent",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
device_class=SensorDeviceClass.BATTERY, device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
element="battery_percentage",
), ),
OwletSensorEntityDescription( OwletSensorEntityDescription(
key="oxygensaturation", key="oxygen_saturation",
translation_key="o2saturation", translation_key="o2saturation",
native_unit_of_measurement=PERCENTAGE, native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
element="oxygen_saturation",
icon="mdi:leaf", icon="mdi:leaf",
), ),
OwletSensorEntityDescription( OwletSensorEntityDescription(
key="oxygensaturation10a", key="heart_rate",
translation_key="o2saturation10a",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
element="oxygen_10_av",
icon="mdi:leaf",
),
OwletSensorEntityDescription(
key="heartrate",
translation_key="heartrate", translation_key="heartrate",
native_unit_of_measurement="bpm", native_unit_of_measurement="bpm",
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
element="heart_rate",
icon="mdi:heart-pulse", icon="mdi:heart-pulse",
), ),
OwletSensorEntityDescription( OwletSensorEntityDescription(
key="batteryminutes", key="battery_minutes",
translation_key="batterymin", translation_key="batterymin",
native_unit_of_measurement=UnitOfTime.MINUTES, native_unit_of_measurement=UnitOfTime.MINUTES,
device_class=SensorDeviceClass.DURATION, device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
element="battery_minutes",
), ),
OwletSensorEntityDescription( OwletSensorEntityDescription(
key="signalstrength", key="signal_strength",
translation_key="signalstrength", translation_key="signalstrength",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH, device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
element="signal_strength",
), ),
OwletSensorEntityDescription( OwletSensorEntityDescription(
key="skintemp", key="skin_temperature",
translation_key="skintemp", translation_key="skintemp",
native_unit_of_measurement=UnitOfTemperature.CELSIUS, native_unit_of_measurement=UnitOfTemperature.CELSIUS,
device_class=SensorDeviceClass.TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT, state_class=SensorStateClass.MEASUREMENT,
element="skin_temperature", ),
OwletSensorEntityDescription(
key="sleep_state",
translation_key="sleepstate",
device_class=SensorDeviceClass.ENUM,
),
OwletSensorEntityDescription(
key="movement",
translation_key="movement",
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",
),
OwletSensorEntityDescription(
key="movement_bucket",
translation_key="movementbucket",
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:bucket-outline",
entity_registry_enabled_default=False,
), ),
) )
@@ -110,15 +113,14 @@ async def async_setup_entry(
hass.data[DOMAIN][config_entry.entry_id].values() hass.data[DOMAIN][config_entry.entry_id].values()
) )
async_add_entities( sensors = []
OwletSensor(coordinator, sensor)
for coordinator in coordinators
for sensor in SENSORS
)
async_add_entities( for coordinator in coordinators:
OwletSleepStateSensor(coordinator) for coordinator in coordinators for sensor in SENSORS:
) if sensor.key in coordinator.sock.properties:
sensors.append(OwletSensor(coordinator, sensor))
async_add_entities(sensors)
class OwletSensor(OwletBaseEntity, SensorEntity): class OwletSensor(OwletBaseEntity, SensorEntity):
@@ -127,60 +129,39 @@ class OwletSensor(OwletBaseEntity, SensorEntity):
def __init__( def __init__(
self, self,
coordinator: OwletCoordinator, coordinator: OwletCoordinator,
sensor_description: OwletSensorEntityDescription, description: OwletSensorEntityDescription,
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
super().__init__(coordinator) super().__init__(coordinator)
self.entity_description: OwletSensorEntityDescription = sensor_description self.entity_description: OwletSensorEntityDescription = description
self._attr_unique_id = ( self._attr_unique_id = f"{self.sock.serial}-{description.key}"
f"{self.sock.serial}-{self.entity_description.translation_key}"
)
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return sensor value.""" """Return sensor value."""
if ( if (
self.entity_description.element self.entity_description.key
in [ in [
"heart_rate", "heart_rate",
"battery_minutes", "battery_minutes",
"oxygen_saturation", "oxygen_saturation",
"skin_temperature", "skin_temperature",
"oxygen_10_av", "oxygen_10_av",
"sleep_state",
] ]
and self.sock.properties["charging"] and self.sock.properties["charging"]
): ):
return None return None
properties = self.sock.properties if self.entity_description.key == "sleep_state":
return properties[self.entity_description.element]
class OwletSleepStateSensor(OwletBaseEntity, SensorEntity):
"""Representation of an Owlet sleep state sensor."""
def __init__(
self,
coordinator: OwletCoordinator,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._attr_unique_id = f"{self.sock.serial}-sleepstate"
self._attr_icon = "mdi:sleep"
self._attr_device_class = SensorDeviceClass.ENUM
self._attr_translation_key = "sleepstate"
@property
def native_value(self) -> str:
"""Return sensor value."""
if self.sock.properties["charging"]:
return "unknown"
return SLEEP_STATES[self.sock.properties["sleep_state"]] return SLEEP_STATES[self.sock.properties["sleep_state"]]
return self.sock.properties[self.entity_description.key]
@property @property
def options(self) -> list[str]: def options(self) -> list[str]:
"""Set options for sleep state.""" """Set options for sleep state."""
if self.entity_description.key != "sleep_state":
return None
return list(SLEEP_STATES.values()) return list(SLEEP_STATES.values())

View File

@@ -2,7 +2,6 @@
"config": { "config": {
"step": { "step": {
"user": { "user": {
"title": "Enter login details",
"data": { "data": {
"region": "Region", "region": "Region",
"username": "Email", "username": "Email",
@@ -44,28 +43,28 @@
"name": "Charging" "name": "Charging"
}, },
"high_hr_alrt": { "high_hr_alrt": {
"name": "High Heart Rate Alert" "name": "High heart rate alert"
}, },
"low_hr_alrt": { "low_hr_alrt": {
"name":"Low Heart Rate Alert" "name": "Low heart rate alert"
}, },
"high_ox_alrt": { "high_ox_alrt": {
"name":"High Oxygen Alert" "name": "High oxygen alert"
}, },
"low_ox_alrt": { "low_ox_alrt": {
"name":"Low Oxygen Alert" "name": "Low oxygen alert"
}, },
"low_batt_alrt": { "low_batt_alrt": {
"name": "Low Battery Alert" "name": "Low battery alert"
}, },
"lost_pwr_alrt": { "lost_pwr_alrt": {
"name": "Lost Power Alert" "name": "Lost power alert"
}, },
"sock_discon_alrt": { "sock_discon_alrt": {
"name": "Sock Diconnected Alert" "name": "Sock diconnected alert"
}, },
"sock_off": { "sock_off": {
"name":"sock_off" "name": "Sock off"
}, },
"awake": { "awake": {
"name": "Awake" "name": "Awake"
@@ -73,34 +72,40 @@
}, },
"sensor": { "sensor": {
"batterypercent": { "batterypercent": {
"name": "Battery Percentage" "name": "Battery percentage"
},
"o2saturation": {
"name": "O2 Saturation"
},
"o2saturation10a": {
"name": "O2 Saturation 10 Minute Average"
},
"heartrate": {
"name": "Heart Rate"
},
"batterymin": {
"name": "Battery Remaining"
}, },
"signalstrength": { "signalstrength": {
"name": "Signal Strength" "name": "Signal strength"
},
"o2saturation": {
"name": "O2 saturation"
},
"o2saturation10a": {
"name": "O2 saturation 10 minute average"
},
"heartrate": {
"name": "Heart rate"
},
"batterymin": {
"name": "Battery remaining"
}, },
"skintemp": { "skintemp": {
"name": "Skin Temperature" "name": "Skin temperature"
}, },
"sleepstate": { "sleepstate": {
"name": "Sleep State", "name": "Sleep state",
"state": { "state": {
"unknown": "Unknown", "unknown": "Unknown",
"awake": "Awake", "awake": "Awake",
"light_sleep": "Light Sleep", "light_sleep": "Light sleep",
"deep_sleep": "Deep Sleep" "deep_sleep": "Deep sleep"
} }
},
"movement": {
"name": "Movement"
},
"movementbucket": {
"name": "Movement bucket"
} }
} }
} }

View File

@@ -2,7 +2,6 @@
"config": { "config": {
"step": { "step": {
"user": { "user": {
"title": "Enter login details",
"data": { "data": {
"region": "Region", "region": "Region",
"username": "Email", "username": "Email",
@@ -44,28 +43,28 @@
"name": "Charging" "name": "Charging"
}, },
"high_hr_alrt": { "high_hr_alrt": {
"name": "High Heart Rate Alert" "name": "High heart rate alert"
}, },
"low_hr_alrt": { "low_hr_alrt": {
"name":"Low Heart Rate Alert" "name": "Low heart rate alert"
}, },
"high_ox_alrt": { "high_ox_alrt": {
"name":"High Oxygen Alert" "name": "High oxygen alert"
}, },
"low_ox_alrt": { "low_ox_alrt": {
"name":"Low Oxygen Alert" "name": "Low oxygen alert"
}, },
"low_batt_alrt": { "low_batt_alrt": {
"name": "Low Battery Alert" "name": "Low battery alert"
}, },
"lost_pwr_alrt": { "lost_pwr_alrt": {
"name": "Lost Power Alert" "name": "Lost power alert"
}, },
"sock_discon_alrt": { "sock_discon_alrt": {
"name": "Sock Diconnected Alert" "name": "Sock diconnected alert"
}, },
"sock_off": { "sock_off": {
"name":"sock_off" "name": "Sock off"
}, },
"awake": { "awake": {
"name": "Awake" "name": "Awake"
@@ -73,34 +72,40 @@
}, },
"sensor": { "sensor": {
"batterypercent": { "batterypercent": {
"name": "Battery Percentage" "name": "Battery percentage"
},
"o2saturation": {
"name": "O2 Saturation"
},
"o2saturation10a": {
"name": "O2 Saturation 10 Minute Average"
},
"heartrate": {
"name": "Heart Rate"
},
"batterymin": {
"name": "Battery Remaining"
}, },
"signalstrength": { "signalstrength": {
"name": "Signal Strength" "name": "Signal strength"
},
"o2saturation": {
"name": "O2 saturation"
},
"o2saturation10a": {
"name": "O2 saturation 10 minute average"
},
"heartrate": {
"name": "Heart rate"
},
"batterymin": {
"name": "Battery remaining"
}, },
"skintemp": { "skintemp": {
"name": "Skin Temperature" "name": "Skin temperature"
}, },
"sleepstate": { "sleepstate": {
"name": "Sleep State", "name": "Sleep state",
"state": { "state": {
"unknown": "Unknown", "unknown": "Unknown",
"awake": "Awake", "awake": "Awake",
"light_sleep": "Light Sleep", "light_sleep": "Light sleep",
"deep_sleep": "Deep Sleep" "deep_sleep": "Deep sleep"
} }
},
"movement": {
"name": "Movement"
},
"movementbucket": {
"name": "Movement bucket"
} }
} }
} }

View File

@@ -2,7 +2,6 @@
"config": { "config": {
"step": { "step": {
"user": { "user": {
"title": "Enter login details",
"data": { "data": {
"region": "Region", "region": "Region",
"username": "Email", "username": "Email",
@@ -44,28 +43,28 @@
"name": "Charging" "name": "Charging"
}, },
"high_hr_alrt": { "high_hr_alrt": {
"name": "High Heart Rate Alert" "name": "High heart rate alert"
}, },
"low_hr_alrt": { "low_hr_alrt": {
"name":"Low Heart Rate Alert" "name": "Low heart rate alert"
}, },
"high_ox_alrt": { "high_ox_alrt": {
"name":"High Oxygen Alert" "name": "High oxygen alert"
}, },
"low_ox_alrt": { "low_ox_alrt": {
"name":"Low Oxygen Alert" "name": "Low oxygen alert"
}, },
"low_batt_alrt": { "low_batt_alrt": {
"name": "Low Battery Alert" "name": "Low battery alert"
}, },
"lost_pwr_alrt": { "lost_pwr_alrt": {
"name": "Lost Power Alert" "name": "Lost power alert"
}, },
"sock_discon_alrt": { "sock_discon_alrt": {
"name": "Sock Diconnected Alert" "name": "Sock diconnected alert"
}, },
"sock_off": { "sock_off": {
"name":"Sock Off" "name": "Sock off"
}, },
"awake": { "awake": {
"name": "Awake" "name": "Awake"
@@ -73,34 +72,40 @@
}, },
"sensor": { "sensor": {
"batterypercent": { "batterypercent": {
"name": "Battery Percentage" "name": "Battery percentage"
},
"o2saturation": {
"name": "O2 Saturation"
},
"o2saturation10a": {
"name": "O2 Saturation 10 Minute Average"
},
"heartrate": {
"name": "Heart Rate"
},
"batterymin": {
"name": "Battery Remaining"
}, },
"signalstrength": { "signalstrength": {
"name": "Signal Strength" "name": "Signal strength"
},
"o2saturation": {
"name": "O2 saturation"
},
"o2saturation10a": {
"name": "O2 saturation 10 minute average"
},
"heartrate": {
"name": "Heart rate"
},
"batterymin": {
"name": "Battery remaining"
}, },
"skintemp": { "skintemp": {
"name": "Skin Temperature" "name": "Skin temperature"
}, },
"sleepstate": { "sleepstate": {
"name": "Sleep State", "name": "Sleep state",
"state": { "state": {
"unknown": "Unknown", "unknown": "Unknown",
"awake": "Awake", "awake": "Awake",
"light_sleep": "Light Sleep", "light_sleep": "Light sleep",
"deep_sleep": "Deep Sleep" "deep_sleep": "Deep sleep"
} }
},
"movement": {
"name": "Movement"
},
"movementbucket": {
"name": "Movement bucket"
} }
} }
} }