4 Commits

Author SHA1 Message Date
ryanbdclark
faefd0b18b Update info.md 2023-11-23 15:42:11 +00:00
ryanbdclark
1192b833ca Update README.md 2023-11-23 15:41:37 +00:00
ryanbdclark
d440fed621 Update CHANGELOG.md 2023-11-23 15:41:03 +00:00
RyanClark123
50fe1a8765 Support for V2 sock added
### Feature
* Support added for V2 sock
* Added tests for binary sensors
### Fix
* Bumping pyowletapi to 2023.11.4 to allow V2 support
* Refactoring
* Corrected spelling of sock disconnected sensor
2023-11-23 15:38:35 +00:00
19 changed files with 1581 additions and 133 deletions

View File

@@ -1,6 +1,15 @@
# Changelog # Changelog
<!--next-version-placeholder--> <!--next-version-placeholder-->
##2023.11.2 (2023-11-23)
### Feature
* Support added for V2 sock ([`50fe1a8`](https://github.com/ryanbdclark/owlet/commit/50fe1a87656b7d6413d06f06f3650fd0bfb48e02))
* Added tests for binary sensors ([`50fe1a8`](https://github.com/ryanbdclark/owlet/commit/50fe1a87656b7d6413d06f06f3650fd0bfb48e02))
### Fix
* Bumping pyowletapi to 2023.11.4 to allow V2 support ([`50fe1a8`](https://github.com/ryanbdclark/owlet/commit/50fe1a87656b7d6413d06f06f3650fd0bfb48e02))
* Refactoring ([`50fe1a8`](https://github.com/ryanbdclark/owlet/commit/50fe1a87656b7d6413d06f06f3650fd0bfb48e02))
* Corrected spelling of sock disconnected sensor ([`50fe1a8`](https://github.com/ryanbdclark/owlet/commit/50fe1a87656b7d6413d06f06f3650fd0bfb48e02))
## 2023.11.1 (2023-11-16) ## 2023.11.1 (2023-11-16)
### Fix ### Fix
* Bumping pyowletapi to 2023.11.1 ([`3acf847`](https://github.com/ryanbdclark/owlet/commit/3acf8473526665382b44ef6325d708a6c62fff45)) * Bumping pyowletapi to 2023.11.1 ([`3acf847`](https://github.com/ryanbdclark/owlet/commit/3acf8473526665382b44ef6325d708a6c62fff45))

View File

@@ -8,13 +8,11 @@
[![hacs][hacsbadge]][hacs] [![hacs][hacsbadge]][hacs]
[![Project Maintenance][maintenance-shield]][user_profile] [![Project Maintenance][maintenance-shield]][user_profile]
A custom component for the Owlet smart sock, currently this only supports the owlet smart sock 3. A custom component for the Owlet smart sock
If you have a smart sock 2 and would like to contribute then please do so.
## Installation ## Installation
1. Use [HACS](https://hacs.xyz/docs/setup/download), in `HACS > Integrations > Explore & Add Repositories` search for "Owlet". After adding this `https://github.com/ryanbdclark/owlet` as a custom repository. 1. Use [HACS](https://hacs.xyz/docs/setup/download), in `HACS > Integrations > Explore & Add Repositories` search for "Owlet".
2. Restart Home Assistant. 2. Restart Home Assistant.
3. [![Add Integration][add-integration-badge]][add-integration] or in the HA UI go to "Settings" -> "Devices & Services" then click "+" and search for "Owlet Smart Sock". 3. [![Add Integration][add-integration-badge]][add-integration] or in the HA UI go to "Settings" -> "Devices & Services" then click "+" and search for "Owlet Smart Sock".

View File

@@ -48,11 +48,21 @@ SENSORS: tuple[OwletBinarySensorEntityDescription, ...] = (
translation_key="low_ox_alrt", translation_key="low_ox_alrt",
device_class=BinarySensorDeviceClass.SOUND, device_class=BinarySensorDeviceClass.SOUND,
), ),
OwletBinarySensorEntityDescription(
key="critical_oxygen_alert",
translation_key="crit_ox_alrt",
device_class=BinarySensorDeviceClass.SOUND,
),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="low_battery_alert", key="low_battery_alert",
translation_key="low_batt_alrt", translation_key="low_batt_alrt",
device_class=BinarySensorDeviceClass.SOUND, device_class=BinarySensorDeviceClass.SOUND,
), ),
OwletBinarySensorEntityDescription(
key="critical_battery_alert",
translation_key="crit_batt_alrt",
device_class=BinarySensorDeviceClass.SOUND,
),
OwletBinarySensorEntityDescription( OwletBinarySensorEntityDescription(
key="lost_power_alert", key="lost_power_alert",
translation_key="lost_pwr_alrt", translation_key="lost_pwr_alrt",
@@ -87,12 +97,14 @@ async def async_setup_entry(
sensors = [] sensors = []
for coordinator in coordinators: for coordinator in coordinators:
print(coordinator.sock.properties)
for sensor in SENSORS: for sensor in SENSORS:
if sensor.key in coordinator.sock.properties: if sensor.key in coordinator.sock.properties:
sensors.append(OwletBinarySensor(coordinator, sensor)) sensors.append(OwletBinarySensor(coordinator, sensor))
async_add_entities(sensors) async_add_entities(sensors)
class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity): class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
"""Representation of an Owlet binary sensor.""" """Representation of an Owlet binary sensor."""

View File

@@ -12,7 +12,6 @@ from pyowletapi.exceptions import (
OwletEmailError, OwletEmailError,
OwletPasswordError, OwletPasswordError,
) )
from pyowletapi.sock import Sock
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, exceptions from homeassistant import config_entries, exceptions
@@ -86,10 +85,8 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
title=user_input[CONF_USERNAME], title=user_input[CONF_USERNAME],
data={ data={
CONF_REGION: user_input[CONF_REGION], CONF_REGION: user_input[CONF_REGION],
CONF_USERNAME: user_input[CONF_PASSWORD], CONF_USERNAME: user_input[CONF_USERNAME],
CONF_API_TOKEN: token[CONF_API_TOKEN], **token,
CONF_OWLET_EXPIRY: token[CONF_OWLET_EXPIRY],
CONF_OWLET_REFRESH: token[CONF_OWLET_REFRESH],
}, },
options={CONF_SCAN_INTERVAL: POLLING_INTERVAL}, options={CONF_SCAN_INTERVAL: POLLING_INTERVAL},
) )

View File

@@ -5,7 +5,7 @@ DOMAIN = "owlet"
CONF_OWLET_EXPIRY = "expiry" CONF_OWLET_EXPIRY = "expiry"
CONF_OWLET_REFRESH = "refresh" CONF_OWLET_REFRESH = "refresh"
SUPPORTED_VERSIONS = [3] SUPPORTED_VERSIONS = [2, 3]
POLLING_INTERVAL = 5 POLLING_INTERVAL = 5
MANUFACTURER = "Owlet Baby Care" MANUFACTURER = "Owlet Baby Care"
SLEEP_STATES = {0: "unknown", 1: "awake", 8: "light_sleep", 15: "deep_sleep"} SLEEP_STATES = {0: "unknown", 1: "awake", 8: "light_sleep", 15: "deep_sleep"}

View File

@@ -36,7 +36,7 @@ class OwletCoordinator(DataUpdateCoordinator):
update_interval=timedelta(seconds=interval), update_interval=timedelta(seconds=interval),
) )
self.sock = sock self.sock = sock
self.config_entry = entry self.config_entry: ConfigEntry = entry
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,7 +2,7 @@
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 homeassistant.helpers.device_registry import DeviceInfo
from .coordinator import OwletCoordinator from .coordinator import OwletCoordinator
from .const import DOMAIN, MANUFACTURER from .const import DOMAIN, MANUFACTURER
@@ -23,7 +23,7 @@ class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
@property @property
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device info of the device""" """Return the device info of the device."""
return DeviceInfo( return DeviceInfo(
identifiers={(DOMAIN, self.sock.serial)}, identifiers={(DOMAIN, self.sock.serial)},
name="Owlet Baby Care Sock", name="Owlet Baby Care Sock",

View File

@@ -9,7 +9,7 @@
"iot_class": "cloud_polling", "iot_class": "cloud_polling",
"issue_tracker": "https://github.com/ryanbdclark/owlet/issues", "issue_tracker": "https://github.com/ryanbdclark/owlet/issues",
"requirements": [ "requirements": [
"pyowletapi==2023.11.1" "pyowletapi==2023.11.4"
], ],
"version": "2023.11.1" "version": "2023.11.2"
} }

View File

@@ -160,7 +160,7 @@ class OwletSensor(OwletBaseEntity, SensorEntity):
return self.sock.properties[self.entity_description.key] return self.sock.properties[self.entity_description.key]
@property @property
def options(self) -> list[str]: def options(self) -> list[str] | None:
"""Set options for sleep state.""" """Set options for sleep state."""
if self.entity_description.key != "sleep_state": if self.entity_description.key != "sleep_state":
return None return None

View File

@@ -54,14 +54,20 @@
"low_ox_alrt": { "low_ox_alrt": {
"name": "Low oxygen alert" "name": "Low oxygen alert"
}, },
"crit_ox_alrt": {
"name": "Critical oxygen alert"
},
"low_batt_alrt": { "low_batt_alrt": {
"name": "Low battery alert" "name": "Low battery alert"
}, },
"crit_batt_alrt": {
"name": "Critical 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 disconnected alert"
}, },
"sock_off": { "sock_off": {
"name": "Sock off" "name": "Sock off"

View File

@@ -54,14 +54,20 @@
"low_ox_alrt": { "low_ox_alrt": {
"name": "Low oxygen alert" "name": "Low oxygen alert"
}, },
"crit_ox_alrt": {
"name": "Critical oxygen alert"
},
"low_batt_alrt": { "low_batt_alrt": {
"name": "Low battery alert" "name": "Low battery alert"
}, },
"crit_batt_alrt": {
"name": "Critical 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 disconnected alert"
}, },
"sock_off": { "sock_off": {
"name": "Sock off" "name": "Sock off"

View File

@@ -54,14 +54,20 @@
"low_ox_alrt": { "low_ox_alrt": {
"name": "Low oxygen alert" "name": "Low oxygen alert"
}, },
"crit_ox_alrt": {
"name": "Critical oxygen alert"
},
"low_batt_alrt": { "low_batt_alrt": {
"name": "Low battery alert" "name": "Low battery alert"
}, },
"crit_batt_alrt": {
"name": "Critical 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 disconnected alert"
}, },
"sock_off": { "sock_off": {
"name": "Sock off" "name": "Sock off"

View File

@@ -8,9 +8,7 @@
[![hacs][hacsbadge]][hacs] [![hacs][hacsbadge]][hacs]
[![Project Maintenance][maintenance-shield]][user_profile] [![Project Maintenance][maintenance-shield]][user_profile]
A custom component for the Owlet smart sock, currently this only supports the owlet smart sock 3. A custom component for the Owlet smart sock
If you have a smart sock 2 and would like to contribute then please do so.
## Installation ## Installation

1211
tests/fixtures/update_properties_v2.json vendored Normal file

File diff suppressed because it is too large Load Diff

188
tests/test_binary_sensor.py Normal file
View File

@@ -0,0 +1,188 @@
"""Test Owlet Sensor."""
from __future__ import annotations
from homeassistant.core import HomeAssistant
from . import async_init_integration
async def test_sensors_asleep(hass: HomeAssistant) -> None:
"""Test sensor values."""
await async_init_integration(
hass, properties_fixture="update_properties_asleep.json"
)
assert len(hass.states.async_all("binary_sensor")) == 10
assert hass.states.get("binary_sensor.owlet_baby_care_sock_charging").state == "off"
assert (
hass.states.get(
"binary_sensor.owlet_baby_care_sock_high_heart_rate_alert"
).state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_heart_rate_alert").state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_high_oxygen_alert").state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_oxygen_alert").state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_battery_alert").state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_lost_power_alert").state
== "off"
)
assert (
hass.states.get(
"binary_sensor.owlet_baby_care_sock_sock_disconnected_alert"
).state
== "off"
)
assert hass.states.get("binary_sensor.owlet_baby_care_sock_sock_off").state == "off"
assert hass.states.get("binary_sensor.owlet_baby_care_sock_awake").state == "off"
async def test_sensors_awake(hass: HomeAssistant) -> None:
"""Test sensor values."""
await async_init_integration(
hass, properties_fixture="update_properties_awake.json"
)
assert len(hass.states.async_all("binary_sensor")) == 10
assert hass.states.get("binary_sensor.owlet_baby_care_sock_charging").state == "off"
assert (
hass.states.get(
"binary_sensor.owlet_baby_care_sock_high_heart_rate_alert"
).state
== "on"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_heart_rate_alert").state
== "on"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_high_oxygen_alert").state
== "on"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_oxygen_alert").state
== "on"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_battery_alert").state
== "on"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_lost_power_alert").state
== "on"
)
assert (
hass.states.get(
"binary_sensor.owlet_baby_care_sock_sock_disconnected_alert"
).state
== "off"
)
assert hass.states.get("binary_sensor.owlet_baby_care_sock_sock_off").state == "off"
assert hass.states.get("binary_sensor.owlet_baby_care_sock_awake").state == "on"
async def test_sensors_charging(hass: HomeAssistant) -> None:
"""Test sensor values."""
await async_init_integration(
hass, properties_fixture="update_properties_charging.json"
)
assert len(hass.states.async_all("binary_sensor")) == 10
assert hass.states.get("binary_sensor.owlet_baby_care_sock_charging").state == "on"
assert (
hass.states.get(
"binary_sensor.owlet_baby_care_sock_high_heart_rate_alert"
).state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_heart_rate_alert").state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_high_oxygen_alert").state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_oxygen_alert").state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_battery_alert").state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_lost_power_alert").state
== "off"
)
assert (
hass.states.get(
"binary_sensor.owlet_baby_care_sock_sock_disconnected_alert"
).state
== "off"
)
assert hass.states.get("binary_sensor.owlet_baby_care_sock_sock_off").state == "off"
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_awake").state == "unknown"
)
async def test_sensors_v2(hass: HomeAssistant) -> None:
"""Test sensor values."""
await async_init_integration(hass, properties_fixture="update_properties_v2.json")
assert len(hass.states.async_all("binary_sensor")) == 9
assert hass.states.get("binary_sensor.owlet_baby_care_sock_charging").state == "off"
assert (
hass.states.get(
"binary_sensor.owlet_baby_care_sock_high_heart_rate_alert"
).state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_heart_rate_alert").state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_oxygen_alert").state
== "off"
)
assert (
hass.states.get("binary_sensor.owlet_baby_care_sock_low_battery_alert").state
== "off"
)
assert (
hass.states.get(
"binary_sensor.owlet_baby_care_sock_critical_battery_alert"
).state
== "off"
)
assert (
hass.states.get(
"binary_sensor.owlet_baby_care_sock_critical_oxygen_alert"
).state
== "off"
)
assert (
hass.states.get(
"binary_sensor.owlet_baby_care_sock_sock_disconnected_alert"
).state
== "off"
)
assert hass.states.get("binary_sensor.owlet_baby_care_sock_sock_off").state == "off"

View File

@@ -70,7 +70,7 @@ async def test_flow_wrong_password(hass: HomeAssistant) -> None:
user_input=CONF_INPUT, user_input=CONF_INPUT,
) )
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["errors"] == {"base": "invalid_password"} assert result["errors"] == {"password": "invalid_password"}
async def test_flow_wrong_email(hass: HomeAssistant) -> None: async def test_flow_wrong_email(hass: HomeAssistant) -> None:
@@ -88,7 +88,7 @@ async def test_flow_wrong_email(hass: HomeAssistant) -> None:
user_input=CONF_INPUT, user_input=CONF_INPUT,
) )
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["errors"] == {"base": "invalid_email"} assert result["errors"] == {"username": "invalid_email"}
async def test_flow_credentials_error(hass: HomeAssistant) -> None: async def test_flow_credentials_error(hass: HomeAssistant) -> None:
@@ -193,7 +193,7 @@ async def test_reauth_invalid_password(hass: HomeAssistant) -> None:
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "reauth_confirm" assert result["step_id"] == "reauth_confirm"
assert result["errors"] == {"base": "invalid_password"} assert result["errors"] == {"password": "invalid_password"}
async def test_reauth_unknown_error(hass: HomeAssistant) -> None: async def test_reauth_unknown_error(hass: HomeAssistant) -> None:

View File

@@ -43,7 +43,7 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None:
entities = er.async_entries_for_device(entity_registry, device_entry.id) entities = er.async_entries_for_device(entity_registry, device_entry.id)
assert len(entities) == 8 assert len(entities) == 18
await entry.async_unload(hass) await entry.async_unload(hass)

View File

@@ -107,3 +107,20 @@ async def test_sensors_charging(hass: HomeAssistant) -> None:
== "unknown" == "unknown"
) )
assert hass.states.get("sensor.owlet_baby_care_sock_sleep_state").state == "unknown" assert hass.states.get("sensor.owlet_baby_care_sock_sleep_state").state == "unknown"
async def test_sensors_v2(hass: HomeAssistant) -> None:
"""Test sensor values."""
await async_init_integration(hass, properties_fixture="update_properties_v2.json")
assert len(hass.states.async_all("sensor")) == 4
assert (
hass.states.get("sensor.owlet_baby_care_sock_battery_percentage").state == "29"
)
assert hass.states.get("sensor.owlet_baby_care_sock_heart_rate").state == "145"
assert hass.states.get("sensor.owlet_baby_care_sock_o2_saturation").state == "99"
assert (
hass.states.get("sensor.owlet_baby_care_sock_signal_strength").state == "98.0"
)