29 Commits

Author SHA1 Message Date
ryanbdclark
dc28ebb02f Update CHANGELOG.md 2024-06-17 09:31:30 +01:00
RyanClark123
52710ba7de Bumping pyowletapi
* Fix
Bump pyowletapi to 2024.6.1
2024-06-17 09:28:22 +01:00
ryanbdclark
d8449c14a4 Update CHANGELOG.md 2024-05-13 13:49:23 +01:00
ryanbdclark
3610262855 Update CHANGELOG.md 2024-05-13 13:48:02 +01:00
ryanbdclark
904023e93a Update CHANGELOG.md 2024-05-13 13:47:23 +01:00
RyanClark123
ceade24851 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
2024-05-13 13:41:51 +01:00
ryanbdclark
ab486d5519 Merge pull request #14 from coreywillwhat/patch-1
fix: O2 Sat 10m Avg reporting `255%`
2024-05-13 09:52:30 +01:00
coreywillwhat
5e17ecdeb2 fix: O2 Sat 10m Avg reporting 255%
Just a suggestion!
The O2 Sat 10m average reports `255%` when not charging, and before it can calculate the 10m average. Suggest changing to report `None` until the average can be displayed. This way the graphs/data aren't skewed by the 255 value.
2024-05-07 11:32:15 -06:00
ryanbdclark
4b90ce0d61 Update CHANGELOG.md 2024-03-27 15:53:56 +00:00
RyanClark123
50c55dcfd3 Add binary sensor, correct interval input
### Feature
*Base station on added as binary sensor

### Fix
* Bumping pyowletapi to 2024.3.2
* UI config now allows you to set interval to 5 seconds, previously the minimum was 10
2024-03-27 15:51:12 +00:00
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
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
21 changed files with 1954 additions and 384 deletions

View File

@@ -7,15 +7,19 @@ on:
- cron: '0 0 * * *'
jobs:
validate:
hassfest:
name: "Hassfest validation"
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v3"
- name: HACS validation
uses: "hacs/action@main"
with:
- 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"
- name: Hassfest validation
uses: "home-assistant/actions/hassfest@master"

View File

@@ -1,11 +1,67 @@
# Changelog
<!--next-version-placeholder-->
## 2023.05.7 (2023-05-30)
## 2024.6.1 (2024-06-17)
### Fix
* Bumping pyowletapi to 2024.6.1 to resolve setup errors ([`52710ba`](https://github.com/ryanbdclark/owlet/commit/52710ba7de53fd07195537c2a5fd2f95bc7dfd1a))
## 2024.5.1 (2024-05-13)
### Feature
* As per HA core patterns, certain sensors will now show as unavailable when sock is charging ([`ceade24`](https://github.com/ryanbdclark/owlet/commit/ceade24851479b8c9bc60b7b8bed74a7bdb927e9))
* Oxygen 10 minute average now only shows a figure if it is between 0 and 100 this avoids skewing by 255 values before the 10 minutes is reached, thanks @coreywillwhat ([`5e17ecd`](https://github.com/ryanbdclark/owlet/commit/5e17ecdeb2aca5bbb35f19ca5795a2c5e0f776ab))
### Fix
* Refactoring as per core maintainers suggestions ([`ceade24`](https://github.com/ryanbdclark/owlet/commit/ceade24851479b8c9bc60b7b8bed74a7bdb927e9))
## 2024.3.1
### Feature
* Base station on added as binary sensor ([`50c55dc`](https://github.com/ryanbdclark/owlet/commit/50c55dcfd30d15027155a8f1d05340238501522d))
### Fix
* Bumping pyowletapi to 2024.3.2 ([`50c55dc`](https://github.com/ryanbdclark/owlet/commit/50c55dcfd30d15027155a8f1d05340238501522d))
* UI config now allows you to set interval to 5 seconds, previously the minimum was 10 ([`50c55dc`](https://github.com/ryanbdclark/owlet/commit/50c55dcfd30d15027155a8f1d05340238501522d))
## 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)
### 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
* 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
* 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
@@ -15,25 +71,25 @@
### Feature
* Tests added
## 2023.05.5 (2023-05-19)
## 2023.5.5 (2023-05-19)
#### 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))
## 2023.05.4 (2023-05-17)
## 2023.5.4 (2023-05-17)
#### Fix
* Bumping to pyowletapi 2023.5.25
## 2023.05.3 (2023-05-17)
## 2023.5.3 (2023-05-17)
#### Fix
* Bumping to pyowletapi 2023.5.24
* Reauthing now no longer re adds users' password to config entry
## 2023.05.2 (2023-05-16)
## 2023.5.2 (2023-05-16)
#### 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))
* 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
* Changed versioning to date based
### Fix

View File

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

@@ -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)
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(

View File

@@ -1,4 +1,5 @@
"""Support for Owlet binary sensors."""
from __future__ import annotations
from dataclasses import dataclass
@@ -17,80 +18,85 @@ from .coordinator import OwletCoordinator
from .entity import OwletBaseEntity
@dataclass
class OwletBinarySensorEntityMixin:
"""Owlet binary sensor element mixin"""
element: str
@dataclass
class OwletBinarySensorEntityDescription(
BinarySensorEntityDescription, OwletBinarySensorEntityMixin
):
@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,
element="charging",
available_during_charging=True,
),
OwletBinarySensorEntityDescription(
key="highhr",
key="high_heart_rate_alert",
translation_key="high_hr_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="high_heart_rate_alert",
available_during_charging=True,
),
OwletBinarySensorEntityDescription(
key="lowhr",
key="low_heart_rate_alert",
translation_key="low_hr_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="low_heart_rate_alert",
available_during_charging=True,
),
OwletBinarySensorEntityDescription(
key="higho2",
key="high_oxygen_alert",
translation_key="high_ox_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="high_oxygen_alert",
available_during_charging=True,
),
OwletBinarySensorEntityDescription(
key="lowo2",
key="low_oxygen_alert",
translation_key="low_ox_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="low_oxygen_alert",
available_during_charging=True,
),
OwletBinarySensorEntityDescription(
key="lowbattery",
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,
element="low_battery_alert",
available_during_charging=True,
),
OwletBinarySensorEntityDescription(
key="lostpower",
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,
element="lost_power_alert",
available_during_charging=True,
),
OwletBinarySensorEntityDescription(
key="sockdisconnected",
key="sock_disconnected",
translation_key="sock_discon_alrt",
device_class=BinarySensorDeviceClass.SOUND,
element="sock_disconnected",
available_during_charging=True,
),
OwletBinarySensorEntityDescription(
key="sock_off",
translation_key="sock_off",
device_class=BinarySensorDeviceClass.POWER,
element="sock_off",
available_during_charging=True,
),
OwletBinarySensorEntityDescription(
key="awake",
translation_key="awake",
element="sleep_state",
icon="mdi:sleep",
key="base_station_on",
translation_key="base_on",
device_class=BinarySensorDeviceClass.POWER,
available_during_charging=True,
),
)
@@ -104,11 +110,16 @@ async def async_setup_entry(
coordinators: OwletCoordinator = hass.data[DOMAIN][config_entry.entry_id].values()
async_add_entities(
OwletBinarySensor(coordinator, sensor)
for coordinator in coordinators
for sensor in SENSORS
)
sensors = []
for coordinator in coordinators:
for sensor in SENSORS:
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)
class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
@@ -117,19 +128,25 @@ class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
def __init__(
self,
coordinator: OwletCoordinator,
sensor_description: OwletBinarySensorEntityDescription,
description: OwletBinarySensorEntityDescription,
) -> None:
"""Initialize the binary sensor."""
super().__init__(coordinator)
self.entity_description = sensor_description
self._attr_unique_id = f"{self.sock.serial}-{self.entity_description.translation_key}"
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.element]
if self.entity_description.element == "sleep_state":
if self.entity_description.key == "sleep_state":
if self.sock.properties["charging"]:
return None
if state in [8, 15]:
@@ -137,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

View File

@@ -12,7 +12,6 @@ from pyowletapi.exceptions import (
OwletEmailError,
OwletPasswordError,
)
from pyowletapi.sock import Sock
import voluptuous as vol
from homeassistant import config_entries, exceptions
@@ -34,9 +33,9 @@ _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required("region"): vol.In(["europe", "world"]),
vol.Required("username"): str,
vol.Required("password"): str,
vol.Required(CONF_REGION): vol.In(["europe", "world"]),
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
}
)
@@ -45,15 +44,10 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Owlet Smart Sock."""
VERSION = 1
reauth_entry: ConfigEntry | None = None
def __init__(self) -> None:
"""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(
self, user_input: dict[str, Any] | None = None
@@ -61,18 +55,14 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle the initial step."""
errors: dict[str, str] = {}
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(
region=self._region,
user=self._username,
password=self._password,
region=user_input[CONF_REGION],
user=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
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()
try:
@@ -82,9 +72,9 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
except OwletDevicesError:
errors["base"] = "no_devices"
except OwletEmailError:
errors["base"] = "invalid_email"
errors[CONF_USERNAME] = "invalid_email"
except OwletPasswordError:
errors["base"] = "invalid_password"
errors[CONF_PASSWORD] = "invalid_password"
except OwletCredentialsError:
errors["base"] = "invalid_credentials"
except Exception: # pylint: disable=broad-except
@@ -92,13 +82,11 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
errors["base"] = "unknown"
else:
return self.async_create_entry(
title=self._username,
title=user_input[CONF_USERNAME],
data={
CONF_REGION: self._region,
CONF_USERNAME: self._username,
CONF_API_TOKEN: token[CONF_API_TOKEN],
CONF_OWLET_EXPIRY: token[CONF_OWLET_EXPIRY],
CONF_OWLET_REFRESH: token[CONF_OWLET_REFRESH],
CONF_REGION: user_input[CONF_REGION],
CONF_USERNAME: user_input[CONF_USERNAME],
**token,
},
options={CONF_SCAN_INTERVAL: POLLING_INTERVAL},
)
@@ -142,12 +130,12 @@ 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["base"] = "invalid_password"
errors[CONF_PASSWORD] = "invalid_password"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Error reauthenticating")
@@ -177,7 +165,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
vol.Required(
CONF_SCAN_INTERVAL,
default=self.config_entry.options.get(CONF_SCAN_INTERVAL),
): vol.All(vol.Coerce(int), vol.Range(min=10)),
): vol.All(vol.Coerce(int), vol.Range(min=5)),
}
)

View File

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

View File

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

View File

@@ -2,13 +2,17 @@
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
class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
"""Base class for Owlet Sock entities."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: OwletCoordinator,
@@ -16,5 +20,15 @@ class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
"""Initialize the base entity."""
super().__init__(coordinator)
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,11 +1,15 @@
{
"domain": "owlet",
"name": "Owlet Smart Sock",
"codeowners": ["@ryanbdclark"],
"codeowners": [
"@ryanbdclark"
],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/owlet",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/ryanbdclark/owlet/issues",
"requirements": ["pyowletapi==2023.07.01"],
"version":"2023.07.01"
"requirements": [
"pyowletapi==2024.6.1"
],
"version": "2024.6.1"
}

View File

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

View File

@@ -2,17 +2,16 @@
"config": {
"step": {
"user": {
"title": "Enter login details",
"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%]"
}
}
},
@@ -39,68 +38,83 @@
}
},
"entity": {
"binary_sensor":{
"binary_sensor": {
"charging": {
"name": "Charging"
},
"high_hr_alrt":{
"name": "High Heart Rate Alert"
"high_hr_alrt": {
"name": "High heart rate alert"
},
"low_hr_alrt":{
"name":"Low Heart Rate Alert"
"low_hr_alrt": {
"name": "Low heart rate alert"
},
"high_ox_alrt":{
"name":"High Oxygen Alert"
"high_ox_alrt": {
"name": "High oxygen alert"
},
"low_ox_alrt":{
"name":"Low Oxygen Alert"
"low_ox_alrt": {
"name": "Low oxygen alert"
},
"low_batt_alrt":{
"name": "Low Battery Alert"
"crit_ox_alrt": {
"name": "Critical oxygen alert"
},
"lost_pwr_alrt":{
"name": "Lost Power Alert"
"low_batt_alrt": {
"name": "Low battery alert"
},
"sock_discon_alrt":{
"name": "Sock Diconnected Alert"
"crit_batt_alrt": {
"name": "Critical battery alert"
},
"sock_off":{
"name":"Sock Off"
"lost_pwr_alrt": {
"name": "Lost power alert"
},
"awake":{
"name":"Awake"
"sock_discon_alrt": {
"name": "Sock disconnected alert"
},
"sock_off": {
"name": "Sock off"
},
"awake": {
"name": "Awake"
},
"base_on": {
"name": "Base station on"
}
},
"sensor": {
"batterypercent": {
"name": "Battery Percentage"
},
"o2saturation": {
"name": "O2 Saturation"
},
"o2saturation10a": {
"name": "O2 Saturation 10 Minute Average"
},
"heartrate": {
"name": "Heart Rate"
},
"batterymin": {
"name": "Battery Remaining"
"name": "Battery percentage"
},
"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": {
"name": "Skin Temperature"
"name": "Skin temperature"
},
"sleepstate": {
"name": "Sleep State",
"name": "Sleep state",
"state": {
"unknown": "Unknown",
"awake": "Awake",
"light_sleep": "Light Sleep",
"deep_sleep": "Deep Sleep"
"light_sleep": "Light sleep",
"deep_sleep": "Deep sleep"
}
},
"movement": {
"name": "Movement"
},
"movementbucket": {
"name": "Movement bucket"
}
}
}

View File

@@ -1,107 +1,121 @@
{
"config": {
"step": {
"user": {
"title": "Enter login details",
"data": {
"region": "Region",
"username": "Email",
"password": "Password"
"config": {
"step": {
"user": {
"data": {
"region": "Region",
"username": "Email",
"password": "[%key:common::config_flow::data::password%]"
}
},
"reauth_confirm": {
"title": "Reauthentiaction required for Owlet",
"data": {
"password": "[%key:common::config_flow::data::password%]"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_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"
}
},
"reauth_confirm": {
"title": "Reauthentiaction required for Owlet",
"data": {
"password": "Password"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_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": {
"step": {
"init": {
"title": "Configure options for Owlet",
"data": {
"pollinterval": "Polling interval in seconds, min 10"
"options": {
"step": {
"init": {
"title": "Configure options for Owlet",
"data": {
"pollinterval": "Polling interval in seconds, min 10"
}
}
}
}
}
},
"entity": {
"binary_sensor":{
"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"
},
"low_batt_alrt":{
"name": "Low Battery Alert"
},
"lost_pwr_alrt":{
"name": "Lost Power Alert"
},
"sock_discon_alrt":{
"name": "Sock Diconnected Alert"
},
"sock_off":{
"name":"Sock Off"
},
"awake":{
"name":"Awake"
}
},
"sensor": {
"batterypercent": {
"name": "Battery Percentage"
},
"o2saturation": {
"name": "O2 Saturation"
},
"o2saturation10a": {
"name": "O2 Saturation 10 Minute Average"
},
"heartrate": {
"name": "Heart Rate"
},
"batterymin": {
"name": "Battery Remaining"
},
"signalstrength": {
"name": "Signal Strength"
},
"skintemp": {
"name": "Skin Temperature"
},
"sleepstate": {
"name": "Sleep State",
"state": {
"unknown": "Unknown",
"awake": "Awake",
"light_sleep": "Light Sleep",
"deep_sleep": "Deep Sleep"
"entity": {
"binary_sensor": {
"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"
},
"lost_pwr_alrt": {
"name": "Lost power alert"
},
"sock_discon_alrt": {
"name": "Sock disconnected alert"
},
"sock_off": {
"name": "Sock off"
},
"awake": {
"name": "Awake"
},
"base_on": {
"name": "Base station on"
}
},
"sensor": {
"batterypercent": {
"name": "Battery percentage"
},
"signalstrength": {
"name": "Signal strength"
},
"o2saturation": {
"name": "O2 saturation"
},
"o2saturation10a": {
"name": "O2 saturation 10 minute average"
},
"heartrate": {
"name": "Heart rate"
},
"batterymin": {
"name": "Battery remaining"
},
"skintemp": {
"name": "Skin temperature"
},
"sleepstate": {
"name": "Sleep state",
"state": {
"unknown": "Unknown",
"awake": "Awake",
"light_sleep": "Light sleep",
"deep_sleep": "Deep sleep"
}
},
"movement": {
"name": "Movement"
},
"movementbucket": {
"name": "Movement bucket"
}
}
}
}
}
}

View File

@@ -2,17 +2,16 @@
"config": {
"step": {
"user": {
"title": "Enter login details",
"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%]"
}
}
},
@@ -39,68 +38,83 @@
}
},
"entity": {
"binary_sensor":{
"binary_sensor": {
"charging": {
"name": "Charging"
},
"high_hr_alrt":{
"name": "High Heart Rate Alert"
"high_hr_alrt": {
"name": "High heart rate alert"
},
"low_hr_alrt":{
"name":"Low Heart Rate Alert"
"low_hr_alrt": {
"name": "Low heart rate alert"
},
"high_ox_alrt":{
"name":"High Oxygen Alert"
"high_ox_alrt": {
"name": "High oxygen alert"
},
"low_ox_alrt":{
"name":"Low Oxygen Alert"
"low_ox_alrt": {
"name": "Low oxygen alert"
},
"low_batt_alrt":{
"name": "Low Battery Alert"
"crit_ox_alrt": {
"name": "Critical oxygen alert"
},
"lost_pwr_alrt":{
"name": "Lost Power Alert"
"low_batt_alrt": {
"name": "Low battery alert"
},
"sock_discon_alrt":{
"name": "Sock Diconnected Alert"
"crit_batt_alrt": {
"name": "Critical battery alert"
},
"sock_off":{
"name":"Sock Off"
"lost_pwr_alrt": {
"name": "Lost power alert"
},
"awake":{
"name":"Awake"
"sock_discon_alrt": {
"name": "Sock disconnected alert"
},
"sock_off": {
"name": "Sock off"
},
"awake": {
"name": "Awake"
},
"base_on": {
"name": "Base station on"
}
},
"sensor": {
"batterypercent": {
"name": "Battery Percentage"
},
"o2saturation": {
"name": "O2 Saturation"
},
"o2saturation10a": {
"name": "O2 Saturation 10 Minute Average"
},
"heartrate": {
"name": "Heart Rate"
},
"batterymin": {
"name": "Battery Remaining"
"name": "Battery percentage"
},
"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": {
"name": "Skin Temperature"
"name": "Skin temperature"
},
"sleepstate": {
"name": "Sleep State",
"name": "Sleep state",
"state": {
"unknown": "Unknown",
"awake": "Awake",
"light_sleep": "Light Sleep",
"deep_sleep": "Deep Sleep"
"light_sleep": "Light sleep",
"deep_sleep": "Deep sleep"
}
},
"movement": {
"name": "Movement"
},
"movementbucket": {
"name": "Movement bucket"
}
}
}

View File

@@ -8,9 +8,7 @@
[![hacs][hacsbadge]][hacs]
[![Project Maintenance][maintenance-shield]][user_profile]
A custom component for the Owlet smart sock, currently this only supports the owlet smart sock 3.
If you have a smart sock 2 and would like to contribute then please do so.
A custom component for the Owlet smart sock
## 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,
)
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:
@@ -88,7 +88,7 @@ async def test_flow_wrong_email(hass: HomeAssistant) -> None:
user_input=CONF_INPUT,
)
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:
@@ -193,7 +193,7 @@ async def test_reauth_invalid_password(hass: HomeAssistant) -> None:
assert result["type"] == FlowResultType.FORM
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:

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)
assert len(entities) == 8
assert len(entities) == 18
await entry.async_unload(hass)

View File

@@ -107,3 +107,20 @@ async def test_sensors_charging(hass: HomeAssistant) -> None:
== "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"
)