Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c8411fab7 | ||
|
|
c45959b123 | ||
|
|
4e30d4652f | ||
|
|
02f8679ed1 | ||
|
|
534ad8a351 | ||
|
|
c693fefbf3 | ||
|
|
523ba949dd | ||
|
|
3c35d87fd2 | ||
|
|
6c2c531a19 | ||
|
|
f4e38ec521 | ||
|
|
ecb950da8a | ||
|
|
a7d4276671 | ||
|
|
ef0a3c3ddb | ||
|
|
7844b5bd87 | ||
|
|
3d31de0205 | ||
|
|
8d173174e2 | ||
|
|
1977df6be0 | ||
|
|
ea4c543ec5 | ||
|
|
26f81c14bf | ||
|
|
c6f37493ca | ||
|
|
5328933de3 |
25
.github/workflows/cron.yml
vendored
Normal file
25
.github/workflows/cron.yml
vendored
Normal 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"
|
||||
|
||||
|
||||
24
CHANGELOG.md
24
CHANGELOG.md
@@ -1,9 +1,31 @@
|
||||
# Changelog
|
||||
|
||||
<!--next-version-placeholder-->
|
||||
## 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.05.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)
|
||||
### 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
|
||||
* Coordinator now properly handles multiple devices
|
||||
* Spelling of signal strength sensor corrected
|
||||
|
||||
### Feature
|
||||
* Tests added
|
||||
|
||||
## 2023.05.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
|
||||
* 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)
|
||||
#### Fix
|
||||
|
||||
@@ -1,29 +1,32 @@
|
||||
"""The Owlet Smart Sock integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from pyowletapi.api import OwletAPI
|
||||
from pyowletapi.exceptions import (
|
||||
OwletAuthenticationError,
|
||||
OwletConnectionError,
|
||||
OwletDevicesError,
|
||||
OwletEmailError,
|
||||
OwletPasswordError,
|
||||
)
|
||||
from pyowletapi.sock import Sock
|
||||
from pyowletapi.exceptions import OwletAuthenticationError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryAuthFailed
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
Platform,
|
||||
CONF_REGION,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_API_TOKEN,
|
||||
CONF_REGION,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
CONF_OWLET_EXPIRY,
|
||||
CONF_OWLET_REFRESH,
|
||||
SUPPORTED_VERSIONS,
|
||||
)
|
||||
|
||||
from .const import CONF_OWLET_EXPIRY, CONF_OWLET_REFRESH, DOMAIN, SUPPORTED_VERSIONS
|
||||
from .coordinator import OwletCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
@@ -44,37 +47,50 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
)
|
||||
|
||||
try:
|
||||
token = await owlet_api.authenticate()
|
||||
|
||||
if token:
|
||||
if token := await owlet_api.authenticate():
|
||||
hass.config_entries.async_update_entry(entry, data={**entry.data, **token})
|
||||
|
||||
devices = await owlet_api.get_devices(SUPPORTED_VERSIONS)
|
||||
|
||||
if devices["tokens"]:
|
||||
hass.config_entries.async_update_entry(
|
||||
entry, data={**entry.data, **devices["tokens"]}
|
||||
)
|
||||
|
||||
socks = {
|
||||
device["device"]["dsn"]: Sock(owlet_api, device["device"])
|
||||
for device in devices["response"]
|
||||
}
|
||||
|
||||
except OwletAuthenticationError as err:
|
||||
except (OwletAuthenticationError, OwletEmailError, OwletPasswordError) as err:
|
||||
_LOGGER.error("Credentials no longer valid, please setup owlet again")
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Credentials expired for {entry.data[CONF_USERNAME]}"
|
||||
) from err
|
||||
|
||||
coordinators = [
|
||||
OwletCoordinator(hass, sock, entry.options.get(CONF_SCAN_INTERVAL))
|
||||
for sock in socks.values()
|
||||
]
|
||||
except OwletConnectionError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Error connecting to {entry.data[CONF_USERNAME]}"
|
||||
) from err
|
||||
|
||||
for coordinator in coordinators:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||
except OwletDevicesError:
|
||||
_LOGGER.error("No owlet devices found to set up")
|
||||
return False
|
||||
|
||||
if devices["tokens"]:
|
||||
hass.config_entries.async_update_entry(
|
||||
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()
|
||||
}
|
||||
|
||||
await asyncio.gather(
|
||||
*(
|
||||
coordinator.async_config_entry_first_refresh()
|
||||
for coordinator in list(coordinators.values())
|
||||
)
|
||||
)
|
||||
|
||||
hass.data[DOMAIN][entry.entry_id] = coordinators
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
|
||||
@@ -34,61 +34,61 @@ class OwletBinarySensorEntityDescription(
|
||||
SENSORS: tuple[OwletBinarySensorEntityDescription, ...] = (
|
||||
OwletBinarySensorEntityDescription(
|
||||
key="charging",
|
||||
name="Charging",
|
||||
translation_key="charging",
|
||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||
element="charging",
|
||||
),
|
||||
OwletBinarySensorEntityDescription(
|
||||
key="highhr",
|
||||
name="High heart rate alert",
|
||||
translation_key="high_hr_alrt",
|
||||
device_class=BinarySensorDeviceClass.SOUND,
|
||||
element="high_heart_rate_alert",
|
||||
),
|
||||
OwletBinarySensorEntityDescription(
|
||||
key="lowhr",
|
||||
name="Low Heart Rate Alert",
|
||||
translation_key="low_hr_alrt",
|
||||
device_class=BinarySensorDeviceClass.SOUND,
|
||||
element="low_heart_rate_alert",
|
||||
),
|
||||
OwletBinarySensorEntityDescription(
|
||||
key="higho2",
|
||||
name="High oxygen alert",
|
||||
translation_key="high_ox_alrt",
|
||||
device_class=BinarySensorDeviceClass.SOUND,
|
||||
element="high_oxygen_alert",
|
||||
),
|
||||
OwletBinarySensorEntityDescription(
|
||||
key="lowo2",
|
||||
name="Low oxygen alert",
|
||||
translation_key="low_ox_alrt",
|
||||
device_class=BinarySensorDeviceClass.SOUND,
|
||||
element="low_oxygen_alert",
|
||||
),
|
||||
OwletBinarySensorEntityDescription(
|
||||
key="lowbattery",
|
||||
name="Low Battery alert",
|
||||
translation_key="low_batt_alrt",
|
||||
device_class=BinarySensorDeviceClass.SOUND,
|
||||
element="low_battery_alert",
|
||||
),
|
||||
OwletBinarySensorEntityDescription(
|
||||
key="lostpower",
|
||||
name="Lost power alert",
|
||||
translation_key="lost_pwr_alrt",
|
||||
device_class=BinarySensorDeviceClass.SOUND,
|
||||
element="lost_power_alert",
|
||||
),
|
||||
OwletBinarySensorEntityDescription(
|
||||
key="sockdisconnected",
|
||||
name="Sock disconnected alert",
|
||||
translation_key="sock_discon_alrt",
|
||||
device_class=BinarySensorDeviceClass.SOUND,
|
||||
element="sock_disconnected",
|
||||
),
|
||||
OwletBinarySensorEntityDescription(
|
||||
key="sock_off",
|
||||
name="Sock off",
|
||||
translation_key="sock_off",
|
||||
device_class=BinarySensorDeviceClass.POWER,
|
||||
element="sock_off",
|
||||
),
|
||||
OwletBinarySensorEntityDescription(
|
||||
key="awake",
|
||||
name="Awake",
|
||||
translation_key="awake",
|
||||
element="sleep_state",
|
||||
icon="mdi:sleep",
|
||||
),
|
||||
@@ -102,11 +102,13 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the owlet sensors from config entry."""
|
||||
|
||||
coordinator: OwletCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinators: OwletCoordinator = hass.data[DOMAIN][config_entry.entry_id].values()
|
||||
|
||||
entities = [OwletBinarySensor(coordinator, sensor) for sensor in SENSORS]
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
OwletBinarySensor(coordinator, sensor)
|
||||
for coordinator in coordinators
|
||||
for sensor in SENSORS
|
||||
)
|
||||
|
||||
|
||||
class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
|
||||
@@ -120,7 +122,7 @@ class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
|
||||
"""Initialize the binary sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = sensor_description
|
||||
self._attr_unique_id = f"{self.sock.serial}-{self.entity_description.name}"
|
||||
self._attr_unique_id = f"{self.sock.serial}-{self.entity_description.translation_key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
|
||||
@@ -1,38 +1,34 @@
|
||||
"""Config flow for Owlet Smart Sock integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from pyowletapi.api import OwletAPI
|
||||
from pyowletapi.sock import Sock
|
||||
from pyowletapi.exceptions import (
|
||||
OwletCredentialsError,
|
||||
OwletDevicesError,
|
||||
OwletEmailError,
|
||||
OwletPasswordError,
|
||||
)
|
||||
|
||||
from pyowletapi.sock import Sock
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries, exceptions
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_REGION,
|
||||
CONF_USERNAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_API_TOKEN,
|
||||
CONF_PASSWORD,
|
||||
CONF_REGION,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
CONF_OWLET_EXPIRY,
|
||||
POLLING_INTERVAL,
|
||||
CONF_OWLET_REFRESH,
|
||||
)
|
||||
from .const import CONF_OWLET_EXPIRY, CONF_OWLET_REFRESH, DOMAIN, POLLING_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,6 +47,7 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialise config flow."""
|
||||
self._entry: ConfigEntry
|
||||
self._region: str
|
||||
self._username: str
|
||||
@@ -80,29 +77,31 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
try:
|
||||
token = await owlet_api.authenticate()
|
||||
try:
|
||||
await owlet_api.validate_authentication()
|
||||
return self.async_create_entry(
|
||||
title=self._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],
|
||||
},
|
||||
options={CONF_SCAN_INTERVAL: POLLING_INTERVAL},
|
||||
)
|
||||
except OwletDevicesError:
|
||||
errors["base"] = "no_devices"
|
||||
await owlet_api.validate_authentication()
|
||||
|
||||
except OwletDevicesError:
|
||||
errors["base"] = "no_devices"
|
||||
except OwletEmailError:
|
||||
errors["base"] = "invalid_email"
|
||||
except OwletPasswordError:
|
||||
errors["base"] = "invalid_password"
|
||||
except OwletCredentialsError:
|
||||
errors["base"] = "invalid_credentials"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=self._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],
|
||||
},
|
||||
options={CONF_SCAN_INTERVAL: POLLING_INTERVAL},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
@@ -110,19 +109,21 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_reauth(self, user_input=None):
|
||||
"""Handle reauth"""
|
||||
async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult:
|
||||
"""Handle reauth."""
|
||||
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||
self.context["entry_id"]
|
||||
)
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(self, user_input=None):
|
||||
"""Dialog that informs the user that reauth is required"""
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Dialog that informs the user that reauth is required."""
|
||||
assert self.reauth_entry is not None
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
@@ -145,12 +146,10 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
except OwletEmailError:
|
||||
errors["base"] = "invalid_email"
|
||||
except OwletPasswordError:
|
||||
errors["base"] = "invalid_password"
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("error reauthing")
|
||||
_LOGGER.exception("Error reauthenticating")
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
@@ -160,14 +159,16 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
|
||||
|
||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle a options flow for owlet"""
|
||||
"""Handle a options flow for owlet."""
|
||||
|
||||
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||
"""Initialise options flow"""
|
||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
||||
"""Initialise options flow."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Handle options flow"""
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle options flow."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
@@ -184,4 +185,4 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||
|
||||
|
||||
class InvalidAuth(exceptions.HomeAssistantError):
|
||||
"""Error to indiciate there is invalud auth"""
|
||||
"""Error to indicate there is invalid auth."""
|
||||
|
||||
@@ -8,4 +8,4 @@ CONF_OWLET_REFRESH = "refresh"
|
||||
SUPPORTED_VERSIONS = [3]
|
||||
POLLING_INTERVAL = 5
|
||||
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"}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
"""Owlet integration."""
|
||||
"""Owlet integration coordinator class."""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pyowletapi.sock import Sock
|
||||
from pyowletapi.exceptions import (
|
||||
OwletError,
|
||||
OwletConnectionError,
|
||||
OwletAuthenticationError,
|
||||
OwletConnectionError,
|
||||
OwletError,
|
||||
)
|
||||
from pyowletapi.sock import Sock
|
||||
|
||||
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
|
||||
|
||||
@@ -23,7 +26,7 @@ _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: int) -> None:
|
||||
def __init__(self, hass: HomeAssistant, sock: Sock, interval) -> None:
|
||||
"""Initialise a custom coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
@@ -32,18 +35,15 @@ class OwletCoordinator(DataUpdateCoordinator):
|
||||
update_interval=timedelta(seconds=interval),
|
||||
)
|
||||
assert self.config_entry is not None
|
||||
self._device_unique_id = sock.serial
|
||||
self._model = sock.model
|
||||
self._sw_version = sock.sw_version
|
||||
self._hw_version = sock.version
|
||||
self.config_entry: ConfigEntry
|
||||
self.sock = sock
|
||||
self.device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device_unique_id)},
|
||||
identifiers={(DOMAIN, sock.serial)},
|
||||
name="Owlet Baby Care Sock",
|
||||
manufacturer=MANUFACTURER,
|
||||
model=self._model,
|
||||
sw_version=self._sw_version,
|
||||
hw_version=self._hw_version,
|
||||
model=sock.model,
|
||||
sw_version=sock.sw_version,
|
||||
hw_version=sock.version,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> None:
|
||||
@@ -55,5 +55,9 @@ class OwletCoordinator(DataUpdateCoordinator):
|
||||
self.config_entry,
|
||||
data={**self.config_entry.data, **properties["tokens"]},
|
||||
)
|
||||
except (OwletError, OwletConnectionError, OwletAuthenticationError) as err:
|
||||
except OwletAuthenticationError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
f"Authentication failed for {self.config_entry.data[CONF_EMAIL]}"
|
||||
) from err
|
||||
except (OwletError, OwletConnectionError) as err:
|
||||
raise UpdateFailed(err) from err
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
{
|
||||
"domain": "owlet",
|
||||
"name": "Owlet Smart Sock",
|
||||
"codeowners": [
|
||||
"@ryanbdclark"
|
||||
],
|
||||
"codeowners": ["@ryanbdclark"],
|
||||
"config_flow": true,
|
||||
"dependencies": [],
|
||||
"documentation": "https://www.home-assistant.io/integrations/owlet",
|
||||
"homekit": {},
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": [
|
||||
"pyowletapi==2023.5.28"
|
||||
],
|
||||
"version":"2023.5.5"
|
||||
"issue_tracker": "https://github.com/ryanbdclark/owlet/issues",
|
||||
"requirements": ["pyowletapi==2023.7.2"],
|
||||
"version":"2023.7.2"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Support for Android IP Webcam binary sensors."""
|
||||
"""Support for Owlet sensors."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
@@ -9,14 +10,15 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.const import (
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
UnitOfTime,
|
||||
UnitOfTemperature,
|
||||
UnitOfTime,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .const import DOMAIN, SLEEP_STATES
|
||||
from .coordinator import OwletCoordinator
|
||||
@@ -25,7 +27,7 @@ from .entity import OwletBaseEntity
|
||||
|
||||
@dataclass
|
||||
class OwletSensorEntityDescriptionMixin:
|
||||
"""Owlet sensor description mix in"""
|
||||
"""Owlet sensor description mix in."""
|
||||
|
||||
element: str
|
||||
|
||||
@@ -40,7 +42,7 @@ class OwletSensorEntityDescription(
|
||||
SENSORS: tuple[OwletSensorEntityDescription, ...] = (
|
||||
OwletSensorEntityDescription(
|
||||
key="batterypercentage",
|
||||
name="Battery",
|
||||
translation_key="batterypercent",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -48,7 +50,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
|
||||
),
|
||||
OwletSensorEntityDescription(
|
||||
key="oxygensaturation",
|
||||
name="O2 Saturation",
|
||||
translation_key="o2saturation",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
element="oxygen_saturation",
|
||||
@@ -56,7 +58,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
|
||||
),
|
||||
OwletSensorEntityDescription(
|
||||
key="oxygensaturation10a",
|
||||
name="O2 Saturation 10 Minute Average",
|
||||
translation_key="o2saturation10a",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
element="oxygen_10_av",
|
||||
@@ -64,7 +66,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
|
||||
),
|
||||
OwletSensorEntityDescription(
|
||||
key="heartrate",
|
||||
name="Heart rate",
|
||||
translation_key="heartrate",
|
||||
native_unit_of_measurement="bpm",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
element="heart_rate",
|
||||
@@ -72,7 +74,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
|
||||
),
|
||||
OwletSensorEntityDescription(
|
||||
key="batteryminutes",
|
||||
name="Battery Remaining",
|
||||
translation_key="batterymin",
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
device_class=SensorDeviceClass.DURATION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -80,7 +82,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
|
||||
),
|
||||
OwletSensorEntityDescription(
|
||||
key="signalstrength",
|
||||
name="Singal Strength",
|
||||
translation_key="signalstrength",
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -88,7 +90,7 @@ SENSORS: tuple[OwletSensorEntityDescription, ...] = (
|
||||
),
|
||||
OwletSensorEntityDescription(
|
||||
key="skintemp",
|
||||
name="Skin Temperature",
|
||||
translation_key="skintemp",
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
@@ -104,12 +106,19 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the owlet sensors from config entry."""
|
||||
|
||||
coordinator: OwletCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||
coordinators: list[OwletCoordinator] = list(
|
||||
hass.data[DOMAIN][config_entry.entry_id].values()
|
||||
)
|
||||
|
||||
entities = [OwletSensor(coordinator, sensor) for sensor in SENSORS]
|
||||
entities.append(OwletSleepStateSensor(coordinator))
|
||||
async_add_entities(
|
||||
OwletSensor(coordinator, sensor)
|
||||
for coordinator in coordinators
|
||||
for sensor in SENSORS
|
||||
)
|
||||
|
||||
async_add_entities(entities)
|
||||
async_add_entities(
|
||||
OwletSleepStateSensor(coordinator) for coordinator in coordinators
|
||||
)
|
||||
|
||||
|
||||
class OwletSensor(OwletBaseEntity, SensorEntity):
|
||||
@@ -122,12 +131,14 @@ class OwletSensor(OwletBaseEntity, SensorEntity):
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = sensor_description
|
||||
self._attr_unique_id = f"{self.sock.serial}-{self.entity_description.name}"
|
||||
self.entity_description: OwletSensorEntityDescription = sensor_description
|
||||
self._attr_unique_id = (
|
||||
f"{self.sock.serial}-{self.entity_description.translation_key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return sensor value"""
|
||||
def native_value(self) -> StateType:
|
||||
"""Return sensor value."""
|
||||
|
||||
if (
|
||||
self.entity_description.element
|
||||
@@ -142,7 +153,9 @@ class OwletSensor(OwletBaseEntity, SensorEntity):
|
||||
):
|
||||
return None
|
||||
|
||||
return self.sock.properties[self.entity_description.element]
|
||||
properties = self.sock.properties
|
||||
|
||||
return properties[self.entity_description.element]
|
||||
|
||||
|
||||
class OwletSleepStateSensor(OwletBaseEntity, SensorEntity):
|
||||
@@ -154,20 +167,20 @@ class OwletSleepStateSensor(OwletBaseEntity, SensorEntity):
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = f"{self.sock.serial}-Sleep State"
|
||||
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"
|
||||
self._attr_name = "Sleep State"
|
||||
|
||||
@property
|
||||
def native_value(self):
|
||||
"""Return sensor value"""
|
||||
def native_value(self) -> str:
|
||||
"""Return sensor value."""
|
||||
if self.sock.properties["charging"]:
|
||||
return None
|
||||
return "unknown"
|
||||
|
||||
return SLEEP_STATES[self.sock.properties["sleep_state"]]
|
||||
|
||||
@property
|
||||
def options(self) -> list[str]:
|
||||
"""Set options for sleep state."""
|
||||
return list(SLEEP_STATES.values())
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user":{
|
||||
"user": {
|
||||
"title": "Enter login details",
|
||||
"data":{
|
||||
"data": {
|
||||
"region": "Region",
|
||||
"username": "Email",
|
||||
"password": "Password"
|
||||
}
|
||||
},
|
||||
"reauth_confirm":{
|
||||
"reauth_confirm": {
|
||||
"title": "Reauthentiaction required for Owlet",
|
||||
"data":{
|
||||
"data": {
|
||||
"password": "Password"
|
||||
}
|
||||
}
|
||||
@@ -20,20 +20,88 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_email": "Entered email address is incorrect",
|
||||
"invalid_password": "Entered password is incorrect",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
"invalid_credentials": "Entered credentials are incorrect",
|
||||
"unknown": "Unknown error occured"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "Device already configured",
|
||||
"reauth_successful": "Reauthentication successful"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init":{
|
||||
"title":"Configure options for Owlet",
|
||||
"data":{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user":{
|
||||
"user": {
|
||||
"title": "Enter login details",
|
||||
"data":{
|
||||
"data": {
|
||||
"region": "Region",
|
||||
"username": "Email",
|
||||
"password": "Password"
|
||||
}
|
||||
},
|
||||
"reauth_confirm":{
|
||||
"reauth_confirm": {
|
||||
"title": "Reauthentiaction required for Owlet",
|
||||
"data":{
|
||||
"data": {
|
||||
"password": "Password"
|
||||
}
|
||||
}
|
||||
@@ -20,20 +20,88 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_email": "Entered email address is incorrect",
|
||||
"invalid_password": "Entered password is incorrect",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
"invalid_credentials": "Entered credentials are incorrect",
|
||||
"unknown": "Unknown error occured"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "Device already configured",
|
||||
"reauth_successful": "Reauthentication successful"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init":{
|
||||
"title":"Configure options for Owlet",
|
||||
"data":{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user":{
|
||||
"user": {
|
||||
"title": "Enter login details",
|
||||
"data":{
|
||||
"data": {
|
||||
"region": "Region",
|
||||
"username": "Email",
|
||||
"password": "Password"
|
||||
}
|
||||
},
|
||||
"reauth_confirm":{
|
||||
"reauth_confirm": {
|
||||
"title": "Reauthentiaction required for Owlet",
|
||||
"data":{
|
||||
"data": {
|
||||
"password": "Password"
|
||||
}
|
||||
}
|
||||
@@ -20,20 +20,88 @@
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_email": "Entered email address is incorrect",
|
||||
"invalid_password": "Entered password is incorrect",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
"invalid_credentials": "Entered credentials are incorrect",
|
||||
"unknown": "Unknown error occured"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "Device already configured",
|
||||
"reauth_successful": "Reauthentication successful"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init":{
|
||||
"title":"Configure options for Owlet",
|
||||
"data":{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
tests/__init__.py
Normal file
60
tests/__init__.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Tests for the Owlet integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.owlet.const import (
|
||||
CONF_OWLET_EXPIRY,
|
||||
CONF_OWLET_REFRESH,
|
||||
DOMAIN,
|
||||
POLLING_INTERVAL,
|
||||
)
|
||||
from homeassistant.const import (
|
||||
CONF_API_TOKEN,
|
||||
CONF_REGION,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
async def async_init_integration(
|
||||
hass: HomeAssistant,
|
||||
skip_setup: bool = False,
|
||||
properties_fixture: str = "update_properties_charging.json",
|
||||
devices_fixture: str = "get_devices.json",
|
||||
) -> MockConfigEntry:
|
||||
"""Set up integration entry."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="sample@gmail.com",
|
||||
unique_id="sample@gmail.com",
|
||||
data={
|
||||
CONF_REGION: "europe",
|
||||
CONF_USERNAME: "sample@gmail.com",
|
||||
CONF_API_TOKEN: "api_token",
|
||||
CONF_OWLET_EXPIRY: 100,
|
||||
CONF_OWLET_REFRESH: "refresh_token",
|
||||
},
|
||||
options={CONF_SCAN_INTERVAL: POLLING_INTERVAL},
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
|
||||
if not skip_setup:
|
||||
with patch(
|
||||
"homeassistant.components.owlet.OwletAPI.get_properties",
|
||||
return_value=json.loads(load_fixture(properties_fixture, "owlet")),
|
||||
), patch(
|
||||
"homeassistant.components.owlet.OwletAPI.authenticate", return_value=None
|
||||
), patch(
|
||||
"homeassistant.components.owlet.OwletAPI.get_devices",
|
||||
return_value=json.loads(load_fixture(devices_fixture, "owlet")),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return entry
|
||||
14
tests/const.py
Normal file
14
tests/const.py
Normal file
@@ -0,0 +1,14 @@
|
||||
"""Constants used for Owlet tests."""
|
||||
|
||||
AUTH_RETURN = {
|
||||
"api_token": "api_token",
|
||||
"expiry": 100,
|
||||
"refresh": "refresh_token",
|
||||
}
|
||||
|
||||
|
||||
CONF_INPUT = {
|
||||
"region": "europe",
|
||||
"username": "sample@gmail.com",
|
||||
"password": "sample",
|
||||
}
|
||||
32
tests/fixtures/get_devices.json
vendored
Normal file
32
tests/fixtures/get_devices.json
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"response": [
|
||||
{
|
||||
"device": {
|
||||
"product_name": "Owlet Baby Monitors",
|
||||
"model": "AY001MTL1",
|
||||
"dsn": "SERIAL_NUMBER",
|
||||
"oem_model": "SS3-OBL-EU",
|
||||
"sw_version": "bc 2.9.7-beta 05/01/19 16:15:18 ID beb2858",
|
||||
"template_id": 48775,
|
||||
"mac": "MAC",
|
||||
"unique_hardware_id": "None",
|
||||
"hwsig": "123-123-123",
|
||||
"lan_ip": "192.0.0.1",
|
||||
"connected_at": "2023-05-23T01:21:29Z",
|
||||
"key": 123456,
|
||||
"lan_enabled": false,
|
||||
"connection_priority": [],
|
||||
"has_properties": true,
|
||||
"product_class": "None",
|
||||
"connection_status": "Online",
|
||||
"lat": "0.0",
|
||||
"lng": "0.0",
|
||||
"locality": "QW12",
|
||||
"device_type": "Wifi",
|
||||
"dealer": "None",
|
||||
"manuf_model": "LBEE5PA1LD-222"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tokens": ""
|
||||
}
|
||||
36
tests/fixtures/get_devices_with_tokens.json
vendored
Normal file
36
tests/fixtures/get_devices_with_tokens.json
vendored
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"response": [
|
||||
{
|
||||
"device": {
|
||||
"product_name": "Owlet Baby Monitors",
|
||||
"model": "AY001MTL1",
|
||||
"dsn": "SERIAL_NUMBER",
|
||||
"oem_model": "SS3-OBL-EU",
|
||||
"sw_version": "bc 2.9.7-beta 05/01/19 16:15:18 ID beb2858",
|
||||
"template_id": 48775,
|
||||
"mac": "MAC",
|
||||
"unique_hardware_id": "None",
|
||||
"hwsig": "123-123-123",
|
||||
"lan_ip": "192.0.0.1",
|
||||
"connected_at": "2023-05-23T01:21:29Z",
|
||||
"key": 123456,
|
||||
"lan_enabled": false,
|
||||
"connection_priority": [],
|
||||
"has_properties": true,
|
||||
"product_class": "None",
|
||||
"connection_status": "Online",
|
||||
"lat": "0.0",
|
||||
"lng": "0.0",
|
||||
"locality": "QW12",
|
||||
"device_type": "Wifi",
|
||||
"dealer": "None",
|
||||
"manuf_model": "LBEE5PA1LD-222"
|
||||
}
|
||||
}
|
||||
],
|
||||
"tokens": {
|
||||
"api_token": "new_api_token",
|
||||
"expiry": 200,
|
||||
"refresh": "new_refresh_token"
|
||||
}
|
||||
}
|
||||
1051
tests/fixtures/update_properties_asleep.json
vendored
Normal file
1051
tests/fixtures/update_properties_asleep.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1051
tests/fixtures/update_properties_awake.json
vendored
Normal file
1051
tests/fixtures/update_properties_awake.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1055
tests/fixtures/update_properties_charging.json
vendored
Normal file
1055
tests/fixtures/update_properties_charging.json
vendored
Normal file
File diff suppressed because it is too large
Load Diff
237
tests/test_config_flow.py
Normal file
237
tests/test_config_flow.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""Test Owlet config flow."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from pyowletapi.exceptions import (
|
||||
OwletCredentialsError,
|
||||
OwletDevicesError,
|
||||
OwletEmailError,
|
||||
OwletPasswordError,
|
||||
)
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.owlet.const import DOMAIN, POLLING_INTERVAL
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from . import async_init_integration
|
||||
from .const import AUTH_RETURN, CONF_INPUT
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
"""Test that the form is served with no input."""
|
||||
# await async_init_integration(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
|
||||
return_value=AUTH_RETURN,
|
||||
), patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.validate_authentication"
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == "sample@gmail.com"
|
||||
assert result["data"] == {
|
||||
"region": "europe",
|
||||
"username": "sample@gmail.com",
|
||||
"api_token": "api_token",
|
||||
"expiry": 100,
|
||||
"refresh": "refresh_token",
|
||||
}
|
||||
assert result["options"] == {"scan_interval": POLLING_INTERVAL}
|
||||
|
||||
|
||||
async def test_flow_wrong_password(hass: HomeAssistant) -> None:
|
||||
"""Test incorrect login throwing error."""
|
||||
with patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
|
||||
side_effect=OwletPasswordError(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_password"}
|
||||
|
||||
|
||||
async def test_flow_wrong_email(hass: HomeAssistant) -> None:
|
||||
"""Test incorrect login throwing error."""
|
||||
with patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
|
||||
side_effect=OwletEmailError(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_email"}
|
||||
|
||||
|
||||
async def test_flow_credentials_error(hass: HomeAssistant) -> None:
|
||||
"""Test incorrect login throwing error."""
|
||||
with patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
|
||||
side_effect=OwletCredentialsError(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_credentials"}
|
||||
|
||||
|
||||
async def test_flow_unknown_error(hass: HomeAssistant) -> None:
|
||||
"""Test unknown error throwing error."""
|
||||
with patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
|
||||
side_effect=Exception(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_flow_no_devices(hass: HomeAssistant) -> None:
|
||||
"""Test unknown error throwing error."""
|
||||
with patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate"
|
||||
), patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.validate_authentication",
|
||||
side_effect=OwletDevicesError(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_INPUT,
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "no_devices"}
|
||||
|
||||
|
||||
async def test_reauth_success(hass: HomeAssistant) -> None:
|
||||
"""Test reauth form."""
|
||||
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
|
||||
return_value=AUTH_RETURN,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_PASSWORD: "sample"},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
|
||||
await hass.config_entries.async_unload(entry.entry_id)
|
||||
|
||||
|
||||
async def test_reauth_invalid_password(hass: HomeAssistant) -> None:
|
||||
"""Test reauth with invalid password errir."""
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
|
||||
side_effect=OwletPasswordError(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_PASSWORD: "sample"}
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
assert result["errors"] == {"base": "invalid_password"}
|
||||
|
||||
|
||||
async def test_reauth_unknown_error(hass: HomeAssistant) -> None:
|
||||
"""Test reauthing with an unknown error."""
|
||||
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_REAUTH, "entry_id": entry.entry_id}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.config_flow.OwletAPI.authenticate",
|
||||
side_effect=Exception(),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_PASSWORD: "sample"}
|
||||
)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
|
||||
async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
"""Test that the form is served with no input."""
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_SCAN_INTERVAL: 10},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ""
|
||||
assert result["data"] == {CONF_SCAN_INTERVAL: 10}
|
||||
74
tests/test_coordinator.py
Normal file
74
tests/test_coordinator.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""Test owlet coordinator."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
from pyowletapi.exceptions import OwletAuthenticationError, OwletConnectionError
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import async_init_integration
|
||||
|
||||
from tests.common import load_fixture
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
|
||||
async def test_coordinator_auth_error(hass: HomeAssistant) -> None:
|
||||
"""Test coordinator setup authentication error."""
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.Sock.update_properties",
|
||||
side_effect=OwletAuthenticationError(),
|
||||
), patch(
|
||||
"homeassistant.components.owlet.OwletAPI.authenticate", return_value=None
|
||||
), patch(
|
||||
"homeassistant.components.owlet.OwletAPI.get_devices",
|
||||
return_value=json.loads(load_fixture("get_devices.json", "owlet")),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_coordinator_connection_error(hass: HomeAssistant) -> None:
|
||||
"""Test coordinator setup connection error error."""
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.Sock.update_properties",
|
||||
side_effect=OwletConnectionError(),
|
||||
), patch(
|
||||
"homeassistant.components.owlet.OwletAPI.authenticate", return_value=None
|
||||
), patch(
|
||||
"homeassistant.components.owlet.OwletAPI.get_devices",
|
||||
return_value=json.loads(load_fixture("get_devices.json", "owlet")),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_coordinator_error(hass: HomeAssistant) -> None:
|
||||
"""Test coordinator setup generic error."""
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.Sock.update_properties",
|
||||
side_effect=Exception(),
|
||||
), patch(
|
||||
"homeassistant.components.owlet.OwletAPI.authenticate", return_value=None
|
||||
), patch(
|
||||
"homeassistant.components.owlet.OwletAPI.get_devices",
|
||||
return_value=json.loads(load_fixture("get_devices.json", "owlet")),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ConfigEntryState.SETUP_RETRY
|
||||
136
tests/test_init.py
Normal file
136
tests/test_init.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Test Owlet init."""
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from pyowletapi.exceptions import (
|
||||
OwletAuthenticationError,
|
||||
OwletConnectionError,
|
||||
OwletDevicesError,
|
||||
OwletError,
|
||||
)
|
||||
|
||||
from homeassistant.components.owlet.const import (
|
||||
CONF_OWLET_EXPIRY,
|
||||
CONF_OWLET_REFRESH,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_REGION, CONF_USERNAME, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
|
||||
from . import async_init_integration
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||
|
||||
|
||||
async def test_async_setup_entry(hass: HomeAssistant) -> None:
|
||||
"""Test setting up entry."""
|
||||
entry = await async_init_integration(hass)
|
||||
|
||||
assert entry.state == ConfigEntryState.LOADED
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
|
||||
device_entry = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id, identifiers={(DOMAIN, "SERIAL_NUMBER")}
|
||||
)
|
||||
|
||||
assert device_entry.name == "Owlet Baby Care Sock"
|
||||
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
entities = er.async_entries_for_device(entity_registry, device_entry.id)
|
||||
|
||||
assert len(entities) == 8
|
||||
|
||||
await entry.async_unload(hass)
|
||||
|
||||
assert entry.state == ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_async_setup_entry_new_tokens(hass: HomeAssistant) -> None:
|
||||
"""Test setting up entry and getting new tokens."""
|
||||
entry = await async_init_integration(
|
||||
hass, devices_fixture="get_devices_with_tokens.json"
|
||||
)
|
||||
|
||||
assert entry.data == {
|
||||
CONF_REGION: "europe",
|
||||
CONF_USERNAME: "sample@gmail.com",
|
||||
CONF_API_TOKEN: "new_api_token",
|
||||
CONF_OWLET_EXPIRY: 200,
|
||||
CONF_OWLET_REFRESH: "new_refresh_token",
|
||||
}
|
||||
|
||||
assert entry.state == ConfigEntryState.LOADED
|
||||
|
||||
await entry.async_unload(hass)
|
||||
|
||||
assert entry.state == ConfigEntryState.NOT_LOADED
|
||||
|
||||
|
||||
async def test_async_setup_entry_auth_error(hass: HomeAssistant) -> None:
|
||||
"""Test setting up entry with auth error."""
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.OwletAPI.authenticate",
|
||||
side_effect=OwletAuthenticationError(),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ConfigEntryState.SETUP_ERROR
|
||||
|
||||
await entry.async_unload(hass)
|
||||
|
||||
|
||||
async def test_async_setup_entry_connection_error(hass: HomeAssistant) -> None:
|
||||
"""Test setting up entry with connection error."""
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.OwletAPI.authenticate",
|
||||
side_effect=OwletConnectionError(),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ConfigEntryState.SETUP_RETRY
|
||||
|
||||
await entry.async_unload(hass)
|
||||
|
||||
|
||||
async def test_async_setup_entry_devices_error(hass: HomeAssistant) -> None:
|
||||
"""Test setting up entry with device error."""
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.OwletAPI.authenticate", return_value=None
|
||||
), patch(
|
||||
"homeassistant.components.owlet.OwletAPI.get_devices",
|
||||
side_effect=OwletDevicesError(),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ConfigEntryState.SETUP_ERROR
|
||||
await entry.async_unload(hass)
|
||||
|
||||
|
||||
async def test_async_setup_entry_error(hass: HomeAssistant) -> None:
|
||||
"""Test setting up entry with unknown error."""
|
||||
entry = await async_init_integration(hass, skip_setup=True)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.owlet.OwletAPI.authenticate",
|
||||
side_effect=OwletError(),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state == ConfigEntryState.SETUP_ERROR
|
||||
|
||||
await entry.async_unload(hass)
|
||||
109
tests/test_sensor.py
Normal file
109
tests/test_sensor.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""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("sensor")) == 8
|
||||
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_battery_percentage").state
|
||||
== "50.0"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_battery_remaining").state
|
||||
== "400.0"
|
||||
)
|
||||
assert hass.states.get("sensor.owlet_baby_care_sock_heart_rate").state == "97.0"
|
||||
assert hass.states.get("sensor.owlet_baby_care_sock_o2_saturation").state == "99.0"
|
||||
assert (
|
||||
hass.states.get(
|
||||
"sensor.owlet_baby_care_sock_o2_saturation_10_minute_average"
|
||||
).state
|
||||
== "97.0"
|
||||
)
|
||||
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_signal_strength").state == "30.0"
|
||||
)
|
||||
assert hass.states.get("sensor.owlet_baby_care_sock_skin_temperature").state == "34"
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_sleep_state").state
|
||||
== "light_sleep"
|
||||
)
|
||||
|
||||
|
||||
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("sensor")) == 8
|
||||
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_battery_percentage").state
|
||||
== "80.0"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_battery_remaining").state
|
||||
== "600.0"
|
||||
)
|
||||
assert hass.states.get("sensor.owlet_baby_care_sock_heart_rate").state == "110.0"
|
||||
assert hass.states.get("sensor.owlet_baby_care_sock_o2_saturation").state == "98.0"
|
||||
assert (
|
||||
hass.states.get(
|
||||
"sensor.owlet_baby_care_sock_o2_saturation_10_minute_average"
|
||||
).state
|
||||
== "98.0"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_signal_strength").state == "34.0"
|
||||
)
|
||||
assert hass.states.get("sensor.owlet_baby_care_sock_skin_temperature").state == "35"
|
||||
assert hass.states.get("sensor.owlet_baby_care_sock_sleep_state").state == "awake"
|
||||
|
||||
|
||||
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("sensor")) == 8
|
||||
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_battery_percentage").state
|
||||
== "100.0"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_battery_remaining").state
|
||||
== "unknown"
|
||||
)
|
||||
assert hass.states.get("sensor.owlet_baby_care_sock_heart_rate").state == "unknown"
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_o2_saturation").state == "unknown"
|
||||
)
|
||||
assert (
|
||||
hass.states.get(
|
||||
"sensor.owlet_baby_care_sock_o2_saturation_10_minute_average"
|
||||
).state
|
||||
== "unknown"
|
||||
)
|
||||
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_signal_strength").state == "34.0"
|
||||
)
|
||||
assert (
|
||||
hass.states.get("sensor.owlet_baby_care_sock_skin_temperature").state
|
||||
== "unknown"
|
||||
)
|
||||
assert hass.states.get("sensor.owlet_baby_care_sock_sleep_state").state == "unknown"
|
||||
Reference in New Issue
Block a user