Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad699b47d7 |
25
.github/workflows/cron.yml
vendored
25
.github/workflows/cron.yml
vendored
@@ -1,25 +0,0 @@
|
|||||||
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"
|
|
||||||
|
|
||||||
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -152,4 +152,3 @@ cython_debug/
|
|||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
/custom_components/owlet.zip
|
/custom_components/owlet.zip
|
||||||
custom_components/owlet/owlet.zip
|
|
||||||
|
|||||||
60
CHANGELOG.md
60
CHANGELOG.md
@@ -1,66 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
<!--next-version-placeholder-->
|
<!--next-version-placeholder-->
|
||||||
## 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.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
|
|
||||||
* Coordinator now properly handles multiple devices
|
|
||||||
* Spelling of signal strength sensor corrected
|
|
||||||
|
|
||||||
### Feature
|
|
||||||
* Tests added
|
|
||||||
|
|
||||||
## 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.5.4 (2023-05-17)
|
|
||||||
#### Fix
|
|
||||||
* Bumping to pyowletapi 2023.5.25
|
|
||||||
|
|
||||||
## 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.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.5.1 (2023-05-15)
|
|
||||||
#### Feature
|
|
||||||
* Changed versioning to date based
|
|
||||||
### Fix
|
|
||||||
* Bumping to pyowletapi 2023.5.21 to fix issue with unawaited authentication call, should resolve issue with refreshing authentication ([`228d54b`](https://github.com/ryanbdclark/owlet/commit/228d54b6414e0b9171064254246d1f36c3af8f5b))
|
|
||||||
|
|
||||||
|
|
||||||
## v1.5.0 (2023-05-12)
|
## v1.5.0 (2023-05-12)
|
||||||
### Feature
|
### Feature
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -1,6 +1,8 @@
|
|||||||
# Owlet Custom Integration
|
# Owlet Custom Integration
|
||||||
|
|
||||||
[![GitHub Release][releases-shield]][releases]
|
[![GitHub Release][releases-shield]][releases]
|
||||||
|
![GitHub all releases][download-all]
|
||||||
|
![GitHub release (latest by RyanClark123)][download-latest]
|
||||||
[![GitHub Activity][commits-shield]][commits]
|
[![GitHub Activity][commits-shield]][commits]
|
||||||
|
|
||||||
[![License][license-shield]][license]
|
[![License][license-shield]][license]
|
||||||
@@ -14,7 +16,7 @@ If you have a smart sock 2 and would like to contribute then please do so.
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
1. Use [HACS](https://hacs.xyz/docs/setup/download), in `HACS > Integrations > Explore & Add Repositories` search for "Owlet". After adding this `https://github.com/ryanbdclark/owlet` as a custom repository.
|
1. Use [HACS](https://hacs.xyz/docs/setup/download), in `HACS > Integrations > Explore & Add Repositories` search for "Owlet". After adding this `https://github.com/RyanClark123/owlet` as a custom repository.
|
||||||
2. Restart Home Assistant.
|
2. Restart Home Assistant.
|
||||||
3. [![Add Integration][add-integration-badge]][add-integration] or in the HA UI go to "Settings" -> "Devices & Services" then click "+" and search for "Owlet Smart Sock".
|
3. [![Add Integration][add-integration-badge]][add-integration] or in the HA UI go to "Settings" -> "Devices & Services" then click "+" and search for "Owlet Smart Sock".
|
||||||
|
|
||||||
@@ -32,19 +34,21 @@ This integration provides the following entities:
|
|||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
- Seconds between polling - Number of seconds between each call for data from the owlet cloud service, default is 5 seconds.
|
- Seconds between polling - Number of seconds between each call for data from the owlet cloud service, default is 10 seconds.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[commits-shield]: https://img.shields.io/github/commit-activity/w/ryanbdclark/owlet?style=for-the-badge
|
[commits-shield]: https://img.shields.io/github/commit-activity/w/RyanClark123/owlet?style=for-the-badge
|
||||||
[commits]: https://github.com/ryanbdclark/owlet/commits/main
|
[commits]: https://github.com/RyanClark123/owlet/commits/main
|
||||||
[hacs]: https://github.com/hacs/integration
|
[hacs]: https://github.com/hacs/integration
|
||||||
[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge
|
[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge
|
||||||
[license]: LICENSE
|
[license]: LICENSE
|
||||||
[license-shield]: https://img.shields.io/github/license/ryanbdclark/owlet.svg?style=for-the-badge
|
[license-shield]: https://img.shields.io/github/license/RyanClark123/owlet.svg?style=for-the-badge
|
||||||
[maintenance-shield]: https://img.shields.io/badge/maintainer-Ryan%20Clark%20%40ryanbdclark-blue.svg?style=for-the-badge
|
[maintenance-shield]: https://img.shields.io/badge/maintainer-Ryan%20Clark%20%40RyanClark123-blue.svg?style=for-the-badge
|
||||||
[releases-shield]: https://img.shields.io/github/release/ryanbdclark/owlet.svg?style=for-the-badge
|
[releases-shield]: https://img.shields.io/github/release/RyanClark123/owlet.svg?style=for-the-badge
|
||||||
[releases]: https://github.com/ryanbdclark/owlet/releases
|
[releases]: https://github.com/RyanClark123/owlet/releases
|
||||||
[user_profile]: https://github.com/ryanbdclark
|
[user_profile]: https://github.com/RyanClark123
|
||||||
|
[download-all]: https://img.shields.io/github/downloads/RyanClark123/Owlet/total?style=for-the-badge
|
||||||
|
[download-latest]: https://img.shields.io/github/downloads/RyanClark123/Owlet/latest/total?style=for-the-badge
|
||||||
[add-integration]: https://my.home-assistant.io/redirect/config_flow_start?domain=owlet
|
[add-integration]: https://my.home-assistant.io/redirect/config_flow_start?domain=owlet
|
||||||
[add-integration-badge]: https://my.home-assistant.io/badges/config_flow_start.svg
|
[add-integration-badge]: https://my.home-assistant.io/badges/config_flow_start.svg
|
||||||
@@ -1,32 +1,28 @@
|
|||||||
"""The Owlet Smart Sock integration."""
|
"""The Owlet Smart Sock integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pyowletapi.api import OwletAPI
|
from pyowletapi.api import OwletAPI
|
||||||
from pyowletapi.exceptions import (
|
|
||||||
OwletAuthenticationError,
|
|
||||||
OwletConnectionError,
|
|
||||||
OwletDevicesError,
|
|
||||||
OwletEmailError,
|
|
||||||
OwletPasswordError,
|
|
||||||
)
|
|
||||||
from pyowletapi.sock import Sock
|
from pyowletapi.sock import Sock
|
||||||
|
from pyowletapi.exceptions import OwletConnectionError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry, ConfigEntryAuthFailed
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_API_TOKEN,
|
|
||||||
CONF_REGION,
|
|
||||||
CONF_SCAN_INTERVAL,
|
|
||||||
CONF_USERNAME,
|
|
||||||
Platform,
|
Platform,
|
||||||
|
CONF_REGION,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
|
CONF_API_TOKEN,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from .const import (
|
||||||
from .const import CONF_OWLET_EXPIRY, CONF_OWLET_REFRESH, DOMAIN, SUPPORTED_VERSIONS
|
DOMAIN,
|
||||||
|
CONF_OWLET_EXPIRY,
|
||||||
|
SUPPORTED_VERSIONS,
|
||||||
|
)
|
||||||
from .coordinator import OwletCoordinator
|
from .coordinator import OwletCoordinator
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
|
||||||
@@ -39,58 +35,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
hass.data.setdefault(DOMAIN, {})
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
owlet_api = OwletAPI(
|
owlet_api = OwletAPI(
|
||||||
region=entry.data[CONF_REGION],
|
entry.data[CONF_REGION],
|
||||||
token=entry.data[CONF_API_TOKEN],
|
entry.data[CONF_USERNAME],
|
||||||
expiry=entry.data[CONF_OWLET_EXPIRY],
|
entry.data[CONF_PASSWORD],
|
||||||
refresh=entry.data[CONF_OWLET_REFRESH],
|
entry.data[CONF_API_TOKEN],
|
||||||
session=async_get_clientsession(hass),
|
entry.data[CONF_OWLET_EXPIRY],
|
||||||
|
async_get_clientsession(hass),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if token := await owlet_api.authenticate():
|
token = await owlet_api.authenticate()
|
||||||
|
|
||||||
|
if token:
|
||||||
hass.config_entries.async_update_entry(entry, data={**entry.data, **token})
|
hass.config_entries.async_update_entry(entry, data={**entry.data, **token})
|
||||||
|
|
||||||
devices = await owlet_api.get_devices(SUPPORTED_VERSIONS)
|
socks = {
|
||||||
|
device["device"]["dsn"]: Sock(owlet_api, device["device"])
|
||||||
|
for device in await owlet_api.get_devices(SUPPORTED_VERSIONS)
|
||||||
|
}
|
||||||
|
|
||||||
except (OwletAuthenticationError, OwletEmailError, OwletPasswordError) as err:
|
except OwletConnectionError as err:
|
||||||
_LOGGER.error("Credentials no longer valid, please setup owlet again")
|
_LOGGER.error("Credentials no longer valid, please setup owlet again")
|
||||||
raise ConfigEntryAuthFailed(
|
raise ConfigEntryAuthFailed(
|
||||||
f"Credentials expired for {entry.data[CONF_USERNAME]}"
|
f"Credentials expired for {entry.data[CONF_USERNAME]}"
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
except OwletConnectionError as err:
|
coordinators = [
|
||||||
raise ConfigEntryNotReady(
|
OwletCoordinator(hass, sock, entry.options.get(CONF_SCAN_INTERVAL))
|
||||||
f"Error connecting to {entry.data[CONF_USERNAME]}"
|
for sock in socks.values()
|
||||||
) from err
|
]
|
||||||
|
|
||||||
except OwletDevicesError:
|
for coordinator in coordinators:
|
||||||
_LOGGER.error("No owlet devices found to set up")
|
await coordinator.async_config_entry_first_refresh()
|
||||||
return False
|
hass.data[DOMAIN][entry.entry_id] = coordinator
|
||||||
|
|
||||||
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, entry)
|
|
||||||
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)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
|||||||
@@ -18,60 +18,73 @@ from .entity import OwletBaseEntity
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class OwletBinarySensorEntityDescription(BinarySensorEntityDescription):
|
class OwletBinarySensorEntityMixin:
|
||||||
|
"""Owlet binary sensor element mixin"""
|
||||||
|
|
||||||
|
element: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OwletBinarySensorEntityDescription(
|
||||||
|
BinarySensorEntityDescription, OwletBinarySensorEntityMixin
|
||||||
|
):
|
||||||
"""Represent the owlet binary sensor entity description."""
|
"""Represent the owlet binary sensor entity description."""
|
||||||
|
|
||||||
|
|
||||||
SENSORS: tuple[OwletBinarySensorEntityDescription, ...] = (
|
SENSORS: tuple[OwletBinarySensorEntityDescription, ...] = (
|
||||||
OwletBinarySensorEntityDescription(
|
OwletBinarySensorEntityDescription(
|
||||||
key="charging",
|
key="charging",
|
||||||
translation_key="charging",
|
name="Charging",
|
||||||
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
device_class=BinarySensorDeviceClass.BATTERY_CHARGING,
|
||||||
|
element="charging",
|
||||||
),
|
),
|
||||||
OwletBinarySensorEntityDescription(
|
OwletBinarySensorEntityDescription(
|
||||||
key="high_heart_rate_alert",
|
key="highhr",
|
||||||
translation_key="high_hr_alrt",
|
name="High heart rate alert",
|
||||||
device_class=BinarySensorDeviceClass.SOUND,
|
device_class=BinarySensorDeviceClass.SOUND,
|
||||||
|
element="high_heart_rate_alert",
|
||||||
),
|
),
|
||||||
OwletBinarySensorEntityDescription(
|
OwletBinarySensorEntityDescription(
|
||||||
key="low_heart_rate_alert",
|
key="lowhr",
|
||||||
translation_key="low_hr_alrt",
|
name="Low Heart Rate Alert",
|
||||||
device_class=BinarySensorDeviceClass.SOUND,
|
device_class=BinarySensorDeviceClass.SOUND,
|
||||||
|
element="low_heart_rate_alert",
|
||||||
),
|
),
|
||||||
OwletBinarySensorEntityDescription(
|
OwletBinarySensorEntityDescription(
|
||||||
key="high_oxygen_alert",
|
key="higho2",
|
||||||
translation_key="high_ox_alrt",
|
name="High oxygen alert",
|
||||||
device_class=BinarySensorDeviceClass.SOUND,
|
device_class=BinarySensorDeviceClass.SOUND,
|
||||||
|
element="high_oxygen_alert",
|
||||||
),
|
),
|
||||||
OwletBinarySensorEntityDescription(
|
OwletBinarySensorEntityDescription(
|
||||||
key="low_oxygen_alert",
|
key="lowo2",
|
||||||
translation_key="low_ox_alrt",
|
name="Low oxygen alert",
|
||||||
device_class=BinarySensorDeviceClass.SOUND,
|
device_class=BinarySensorDeviceClass.SOUND,
|
||||||
|
element="low_oxygen_alert",
|
||||||
),
|
),
|
||||||
OwletBinarySensorEntityDescription(
|
OwletBinarySensorEntityDescription(
|
||||||
key="low_battery_alert",
|
key="lowbattery",
|
||||||
translation_key="low_batt_alrt",
|
name="Low Battery alert",
|
||||||
device_class=BinarySensorDeviceClass.SOUND,
|
device_class=BinarySensorDeviceClass.SOUND,
|
||||||
|
element="low_battery_alert",
|
||||||
),
|
),
|
||||||
OwletBinarySensorEntityDescription(
|
OwletBinarySensorEntityDescription(
|
||||||
key="lost_power_alert",
|
key="lostpower",
|
||||||
translation_key="lost_pwr_alrt",
|
name="Lost power alert",
|
||||||
device_class=BinarySensorDeviceClass.SOUND,
|
device_class=BinarySensorDeviceClass.SOUND,
|
||||||
|
element="lost_power_alert",
|
||||||
),
|
),
|
||||||
OwletBinarySensorEntityDescription(
|
OwletBinarySensorEntityDescription(
|
||||||
key="sock_disconnected",
|
key="sockdisconnected",
|
||||||
translation_key="sock_discon_alrt",
|
name="Sock disconnected alert",
|
||||||
device_class=BinarySensorDeviceClass.SOUND,
|
device_class=BinarySensorDeviceClass.SOUND,
|
||||||
|
element="sock_disconnected",
|
||||||
),
|
),
|
||||||
OwletBinarySensorEntityDescription(
|
OwletBinarySensorEntityDescription(
|
||||||
key="sock_off",
|
key="sock_off",
|
||||||
translation_key="sock_off",
|
name="Sock off",
|
||||||
device_class=BinarySensorDeviceClass.POWER,
|
device_class=BinarySensorDeviceClass.POWER,
|
||||||
),
|
element="sock_off",
|
||||||
OwletBinarySensorEntityDescription(
|
|
||||||
key="sleep_state",
|
|
||||||
translation_key="awake",
|
|
||||||
icon="mdi:sleep",
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -83,13 +96,11 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the owlet sensors from config entry."""
|
"""Set up the owlet sensors from config entry."""
|
||||||
|
|
||||||
coordinators: OwletCoordinator = hass.data[DOMAIN][config_entry.entry_id].values()
|
coordinator: OwletCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
|
|
||||||
async_add_entities(
|
entities = [OwletBinarySensor(coordinator, sensor) for sensor in SENSORS]
|
||||||
OwletBinarySensor(coordinator, sensor)
|
|
||||||
for coordinator in coordinators
|
async_add_entities(entities)
|
||||||
for sensor in SENSORS
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
|
class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
|
||||||
@@ -98,24 +109,14 @@ class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: OwletCoordinator,
|
coordinator: OwletCoordinator,
|
||||||
description: OwletBinarySensorEntityDescription,
|
sensor_description: OwletBinarySensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the binary sensor."""
|
"""Initialize the binary sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description = description
|
self.entity_description = sensor_description
|
||||||
self._attr_unique_id = f"{self.sock.serial}-{description.key}"
|
self._attr_unique_id = f"{self.sock.serial}-{self.entity_description.name}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
"""Return true if the binary sensor is on."""
|
"""Return true if the binary sensor is on."""
|
||||||
state = self.sock.properties[self.entity_description.key]
|
return self.sock.properties[self.entity_description.element]
|
||||||
|
|
||||||
if self.entity_description.key == "sleep_state":
|
|
||||||
if self.sock.properties["charging"]:
|
|
||||||
return None
|
|
||||||
if state in [8, 15]:
|
|
||||||
state = False
|
|
||||||
else:
|
|
||||||
state = True
|
|
||||||
|
|
||||||
return state
|
|
||||||
|
|||||||
@@ -1,42 +1,46 @@
|
|||||||
"""Config flow for Owlet Smart Sock integration."""
|
"""Config flow for Owlet Smart Sock integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from collections.abc import Mapping
|
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pyowletapi.api import OwletAPI
|
from pyowletapi.api import OwletAPI
|
||||||
|
from pyowletapi.sock import Sock
|
||||||
from pyowletapi.exceptions import (
|
from pyowletapi.exceptions import (
|
||||||
OwletCredentialsError,
|
|
||||||
OwletDevicesError,
|
OwletDevicesError,
|
||||||
OwletEmailError,
|
OwletEmailError,
|
||||||
OwletPasswordError,
|
OwletPasswordError,
|
||||||
)
|
)
|
||||||
from pyowletapi.sock import Sock
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant import config_entries, exceptions
|
from homeassistant import config_entries, exceptions
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import (
|
|
||||||
CONF_API_TOKEN,
|
|
||||||
CONF_PASSWORD,
|
|
||||||
CONF_REGION,
|
|
||||||
CONF_SCAN_INTERVAL,
|
|
||||||
CONF_USERNAME,
|
|
||||||
)
|
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.data_entry_flow import FlowResult
|
from homeassistant.data_entry_flow import FlowResult
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
from .const import CONF_OWLET_EXPIRY, CONF_OWLET_REFRESH, DOMAIN, POLLING_INTERVAL
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
|
CONF_OWLET_EXPIRY,
|
||||||
|
POLLING_INTERVAL,
|
||||||
|
SUPPORTED_VERSIONS,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_REGION): vol.In(["europe", "world"]),
|
vol.Required("region"): vol.In(["europe", "world"]),
|
||||||
vol.Required(CONF_USERNAME): str,
|
vol.Required("username"): str,
|
||||||
vol.Required(CONF_PASSWORD): str,
|
vol.Required("password"): str,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -45,10 +49,14 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle a config flow for Owlet Smart Sock."""
|
"""Handle a config flow for Owlet Smart Sock."""
|
||||||
|
|
||||||
VERSION = 1
|
VERSION = 1
|
||||||
reauth_entry: ConfigEntry | None = None
|
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
"""Initialise config flow."""
|
self._entry: ConfigEntry
|
||||||
|
self._region: str
|
||||||
|
self._username: str
|
||||||
|
self._password: str
|
||||||
|
self._devices: dict[str, Sock]
|
||||||
|
self.reauth_entry: ConfigEntry | None = None
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
@@ -56,43 +64,45 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
|
self._region = user_input[CONF_REGION]
|
||||||
|
self._username = user_input[CONF_USERNAME]
|
||||||
|
self._password = user_input[CONF_PASSWORD]
|
||||||
|
|
||||||
owlet_api = OwletAPI(
|
owlet_api = OwletAPI(
|
||||||
region=user_input[CONF_REGION],
|
self._region,
|
||||||
user=user_input[CONF_USERNAME],
|
self._username,
|
||||||
password=user_input[CONF_PASSWORD],
|
self._password,
|
||||||
session=async_get_clientsession(self.hass),
|
session=async_get_clientsession(self.hass),
|
||||||
)
|
)
|
||||||
|
|
||||||
await self.async_set_unique_id(user_input[CONF_USERNAME].lower())
|
await self.async_set_unique_id(self._username.lower())
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
token = await owlet_api.authenticate()
|
token = await owlet_api.authenticate()
|
||||||
await owlet_api.validate_authentication()
|
try:
|
||||||
|
await owlet_api.get_devices(SUPPORTED_VERSIONS)
|
||||||
except OwletDevicesError:
|
|
||||||
errors["base"] = "no_devices"
|
|
||||||
except OwletEmailError:
|
|
||||||
errors[CONF_USERNAME] = "invalid_email"
|
|
||||||
except OwletPasswordError:
|
|
||||||
errors[CONF_PASSWORD] = "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(
|
return self.async_create_entry(
|
||||||
title=user_input[CONF_USERNAME],
|
title=self._username,
|
||||||
data={
|
data={
|
||||||
CONF_REGION: user_input[CONF_REGION],
|
CONF_REGION: self._region,
|
||||||
CONF_USERNAME: user_input[CONF_PASSWORD],
|
CONF_USERNAME: self._username,
|
||||||
|
CONF_PASSWORD: self._password,
|
||||||
CONF_API_TOKEN: token[CONF_API_TOKEN],
|
CONF_API_TOKEN: token[CONF_API_TOKEN],
|
||||||
CONF_OWLET_EXPIRY: token[CONF_OWLET_EXPIRY],
|
CONF_OWLET_EXPIRY: token[CONF_OWLET_EXPIRY],
|
||||||
CONF_OWLET_REFRESH: token[CONF_OWLET_REFRESH],
|
|
||||||
},
|
},
|
||||||
options={CONF_SCAN_INTERVAL: POLLING_INTERVAL},
|
options={CONF_SCAN_INTERVAL: POLLING_INTERVAL},
|
||||||
)
|
)
|
||||||
|
except OwletDevicesError:
|
||||||
|
errors["base"] = "no_devices"
|
||||||
|
|
||||||
|
except OwletEmailError:
|
||||||
|
errors["base"] = "invalid_email"
|
||||||
|
except OwletPasswordError:
|
||||||
|
errors["base"] = "invalid_password"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||||
@@ -100,21 +110,19 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@callback
|
@callback
|
||||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
|
def async_get_options_flow(config_entry):
|
||||||
"""Get the options flow for this handler."""
|
"""Get the options flow for this handler."""
|
||||||
return OptionsFlowHandler(config_entry)
|
return OptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult:
|
async def async_step_reauth(self, user_input=None):
|
||||||
"""Handle reauth."""
|
"""Handle reauth"""
|
||||||
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
self.reauth_entry = self.hass.config_entries.async_get_entry(
|
||||||
self.context["entry_id"]
|
self.context["entry_id"]
|
||||||
)
|
)
|
||||||
return await self.async_step_reauth_confirm()
|
return await self.async_step_reauth_confirm()
|
||||||
|
|
||||||
async def async_step_reauth_confirm(
|
async def async_step_reauth_confirm(self, user_input=None):
|
||||||
self, user_input: dict[str, Any] | None = None
|
"""Dialog that informs the user that reauth is required"""
|
||||||
) -> FlowResult:
|
|
||||||
"""Dialog that informs the user that reauth is required."""
|
|
||||||
assert self.reauth_entry is not None
|
assert self.reauth_entry is not None
|
||||||
errors: dict[str, str] = {}
|
errors: dict[str, str] = {}
|
||||||
|
|
||||||
@@ -129,18 +137,22 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
try:
|
try:
|
||||||
token = await owlet_api.authenticate()
|
token = await owlet_api.authenticate()
|
||||||
if token:
|
if token:
|
||||||
|
user_input[CONF_API_TOKEN] = token[CONF_API_TOKEN]
|
||||||
|
user_input[CONF_OWLET_EXPIRY] = token[CONF_OWLET_EXPIRY]
|
||||||
self.hass.config_entries.async_update_entry(
|
self.hass.config_entries.async_update_entry(
|
||||||
self.reauth_entry, data={**entry_data, **token}
|
self.reauth_entry, data={**entry_data, **user_input}
|
||||||
)
|
)
|
||||||
|
|
||||||
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 OwletEmailError:
|
||||||
|
errors["base"] = "invalid_email"
|
||||||
except OwletPasswordError:
|
except OwletPasswordError:
|
||||||
errors[CONF_PASSWORD] = "invalid_password"
|
errors["base"] = "invalid_password"
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception("Error reauthenticating")
|
_LOGGER.exception("error reauthing")
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="reauth_confirm",
|
step_id="reauth_confirm",
|
||||||
@@ -150,16 +162,14 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
|||||||
|
|
||||||
|
|
||||||
class OptionsFlowHandler(config_entries.OptionsFlow):
|
class OptionsFlowHandler(config_entries.OptionsFlow):
|
||||||
"""Handle a options flow for owlet."""
|
"""Handle a options flow for owlet"""
|
||||||
|
|
||||||
def __init__(self, config_entry: ConfigEntry) -> None:
|
def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
|
||||||
"""Initialise options flow."""
|
"""Initialise options flow"""
|
||||||
self.config_entry = config_entry
|
self.config_entry = config_entry
|
||||||
|
|
||||||
async def async_step_init(
|
async def async_step_init(self, user_input=None):
|
||||||
self, user_input: dict[str, Any] | None = None
|
"""Handle options flow"""
|
||||||
) -> FlowResult:
|
|
||||||
"""Handle options flow."""
|
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
return self.async_create_entry(title="", data=user_input)
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
@@ -176,4 +186,4 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
|
|||||||
|
|
||||||
|
|
||||||
class InvalidAuth(exceptions.HomeAssistantError):
|
class InvalidAuth(exceptions.HomeAssistantError):
|
||||||
"""Error to indicate there is invalid auth."""
|
"""Error to indiciate there is invalud auth"""
|
||||||
|
|||||||
@@ -3,9 +3,7 @@
|
|||||||
DOMAIN = "owlet"
|
DOMAIN = "owlet"
|
||||||
|
|
||||||
CONF_OWLET_EXPIRY = "expiry"
|
CONF_OWLET_EXPIRY = "expiry"
|
||||||
CONF_OWLET_REFRESH = "refresh"
|
|
||||||
|
|
||||||
SUPPORTED_VERSIONS = [3]
|
SUPPORTED_VERSIONS = [3]
|
||||||
POLLING_INTERVAL = 5
|
POLLING_INTERVAL = 10
|
||||||
MANUFACTURER = "Owlet Baby Care"
|
MANUFACTURER = "Owlet Baby Care"
|
||||||
SLEEP_STATES = {0: "unknown", 1: "awake", 8: "light_sleep", 15: "deep_sleep"}
|
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
"""Owlet integration coordinator class."""
|
"""Owlet integration."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from pyowletapi.exceptions import (
|
|
||||||
OwletAuthenticationError,
|
|
||||||
OwletConnectionError,
|
|
||||||
OwletError,
|
|
||||||
)
|
|
||||||
from pyowletapi.sock import Sock
|
from pyowletapi.sock import Sock
|
||||||
|
from pyowletapi.exceptions import OwletError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import CONF_EMAIL
|
|
||||||
from homeassistant.core import HomeAssistant
|
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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import (
|
||||||
|
DOMAIN,
|
||||||
|
MANUFACTURER,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -25,9 +22,7 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
class OwletCoordinator(DataUpdateCoordinator):
|
class OwletCoordinator(DataUpdateCoordinator):
|
||||||
"""Coordinator is responsible for querying the device at a specified route."""
|
"""Coordinator is responsible for querying the device at a specified route."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, hass: HomeAssistant, sock: Sock, interval: int) -> None:
|
||||||
self, hass: HomeAssistant, sock: Sock, interval, entry: ConfigEntry
|
|
||||||
) -> None:
|
|
||||||
"""Initialise a custom coordinator."""
|
"""Initialise a custom coordinator."""
|
||||||
super().__init__(
|
super().__init__(
|
||||||
hass,
|
hass,
|
||||||
@@ -35,21 +30,24 @@ class OwletCoordinator(DataUpdateCoordinator):
|
|||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=timedelta(seconds=interval),
|
update_interval=timedelta(seconds=interval),
|
||||||
)
|
)
|
||||||
|
assert self.config_entry is not None
|
||||||
|
self._device_unique_id = sock.serial
|
||||||
|
self._model = sock.model
|
||||||
|
self._sw_version = sock.sw_version
|
||||||
|
self._hw_version = sock.version
|
||||||
self.sock = sock
|
self.sock = sock
|
||||||
self.config_entry = entry
|
self.device_info = DeviceInfo(
|
||||||
|
identifiers={(DOMAIN, self._device_unique_id)},
|
||||||
|
name="Owlet Baby Care Sock",
|
||||||
|
manufacturer=MANUFACTURER,
|
||||||
|
model=self._model,
|
||||||
|
sw_version=self._sw_version,
|
||||||
|
hw_version=self._hw_version,
|
||||||
|
)
|
||||||
|
|
||||||
async def _async_update_data(self) -> None:
|
async def _async_update_data(self) -> None:
|
||||||
"""Fetch the data from the device."""
|
"""Fetch the data from the device."""
|
||||||
try:
|
try:
|
||||||
properties = await self.sock.update_properties()
|
await self.sock.update_properties()
|
||||||
if properties["tokens"]:
|
except OwletError as err:
|
||||||
self.hass.config_entries.async_update_entry(
|
|
||||||
self.config_entry,
|
|
||||||
data={**self.config_entry.data, **properties["tokens"]},
|
|
||||||
)
|
|
||||||
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
|
raise UpdateFailed(err) from err
|
||||||
|
|||||||
@@ -2,17 +2,13 @@
|
|||||||
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.helpers.entity import DeviceInfo
|
|
||||||
|
|
||||||
from .coordinator import OwletCoordinator
|
from .coordinator import OwletCoordinator
|
||||||
from .const import DOMAIN, MANUFACTURER
|
|
||||||
|
|
||||||
|
|
||||||
class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
|
class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
|
||||||
"""Base class for Owlet Sock entities."""
|
"""Base class for Owlet Sock entities."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: OwletCoordinator,
|
coordinator: OwletCoordinator,
|
||||||
@@ -20,15 +16,5 @@ class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
|
|||||||
"""Initialize the base entity."""
|
"""Initialize the base entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.sock = coordinator.sock
|
self.sock = coordinator.sock
|
||||||
|
self._attr_device_info = coordinator.device_info
|
||||||
@property
|
self._attr_has_entity_name = True
|
||||||
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}",
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
"domain": "owlet",
|
"domain": "owlet",
|
||||||
"name": "Owlet Smart Sock",
|
"name": "Owlet Smart Sock",
|
||||||
"codeowners": [
|
"codeowners": [
|
||||||
"@ryanbdclark"
|
"@RyanClark123"
|
||||||
],
|
],
|
||||||
"config_flow": true,
|
"config_flow": true,
|
||||||
|
"dependencies": [],
|
||||||
"documentation": "https://www.home-assistant.io/integrations/owlet",
|
"documentation": "https://www.home-assistant.io/integrations/owlet",
|
||||||
|
"homekit": {},
|
||||||
"iot_class": "cloud_polling",
|
"iot_class": "cloud_polling",
|
||||||
"issue_tracker": "https://github.com/ryanbdclark/owlet/issues",
|
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"pyowletapi==2023.9.1"
|
"pyowletapi==2023.5.20"
|
||||||
],
|
],
|
||||||
"version": "2023.9.1"
|
"version":"1.5.0"
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
"""Support for Owlet sensors."""
|
"""Support for Android IP Webcam binary sensors."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from homeassistant.components.sensor import (
|
from homeassistant.components.sensor import (
|
||||||
@@ -10,97 +9,90 @@ from homeassistant.components.sensor import (
|
|||||||
SensorStateClass,
|
SensorStateClass,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
UnitOfTemperature,
|
|
||||||
UnitOfTime,
|
UnitOfTime,
|
||||||
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
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 .const import DOMAIN
|
||||||
from .coordinator import OwletCoordinator
|
from .coordinator import OwletCoordinator
|
||||||
from .entity import OwletBaseEntity
|
from .entity import OwletBaseEntity
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class OwletSensorEntityDescription(SensorEntityDescription):
|
class OwletSensorEntityDescriptionMixin:
|
||||||
|
"""Owlet sensor description mix in"""
|
||||||
|
|
||||||
|
element: str
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class OwletSensorEntityDescription(
|
||||||
|
SensorEntityDescription, OwletSensorEntityDescriptionMixin
|
||||||
|
):
|
||||||
"""Represent the owlet sensor entity description."""
|
"""Represent the owlet sensor entity description."""
|
||||||
|
|
||||||
|
|
||||||
SENSORS_ALL: tuple[OwletSensorEntityDescription, ...] = (
|
SENSORS: tuple[OwletSensorEntityDescription, ...] = (
|
||||||
OwletSensorEntityDescription(
|
OwletSensorEntityDescription(
|
||||||
key="battery_percentage",
|
key="batterypercentage",
|
||||||
translation_key="batterypercent",
|
name="Battery",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
device_class=SensorDeviceClass.BATTERY,
|
device_class=SensorDeviceClass.BATTERY,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
element="battery_percentage",
|
||||||
),
|
),
|
||||||
OwletSensorEntityDescription(
|
OwletSensorEntityDescription(
|
||||||
key="oxygen_saturation",
|
key="oxygensaturation",
|
||||||
translation_key="o2saturation",
|
name="O2 Saturation",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
element="oxygen_saturation",
|
||||||
icon="mdi:leaf",
|
icon="mdi:leaf",
|
||||||
),
|
),
|
||||||
OwletSensorEntityDescription(
|
OwletSensorEntityDescription(
|
||||||
key="heart_rate",
|
key="oxygensaturation10a",
|
||||||
translation_key="heartrate",
|
name="O2 Saturation 10 Minute Average",
|
||||||
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
element="oxygen_10_av",
|
||||||
|
icon="mdi:leaf",
|
||||||
|
),
|
||||||
|
OwletSensorEntityDescription(
|
||||||
|
key="heartrate",
|
||||||
|
name="Heart rate",
|
||||||
native_unit_of_measurement="bpm",
|
native_unit_of_measurement="bpm",
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
element="heart_rate",
|
||||||
icon="mdi:heart-pulse",
|
icon="mdi:heart-pulse",
|
||||||
),
|
),
|
||||||
OwletSensorEntityDescription(
|
OwletSensorEntityDescription(
|
||||||
key="battery_minutes",
|
key="batteryminutes",
|
||||||
translation_key="batterymin",
|
name="Battery Remaining",
|
||||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||||
device_class=SensorDeviceClass.DURATION,
|
device_class=SensorDeviceClass.DURATION,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
element="battery_minutes",
|
||||||
),
|
),
|
||||||
OwletSensorEntityDescription(
|
OwletSensorEntityDescription(
|
||||||
key="signal_strength",
|
key="signalstrength",
|
||||||
translation_key="signalstrength",
|
name="Singal Strength",
|
||||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||||
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
element="signal_strength",
|
||||||
),
|
),
|
||||||
OwletSensorEntityDescription(
|
OwletSensorEntityDescription(
|
||||||
key="skin_temperature",
|
key="skintemp",
|
||||||
translation_key="skintemp",
|
name="Skin Temperature",
|
||||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
device_class=SensorDeviceClass.TEMPERATURE,
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
),
|
element="skin_temperature",
|
||||||
OwletSensorEntityDescription(
|
|
||||||
key="sleep_state",
|
|
||||||
translation_key="sleepstate",
|
|
||||||
device_class=SensorDeviceClass.ENUM,
|
|
||||||
),
|
|
||||||
OwletSensorEntityDescription(
|
|
||||||
key="movement",
|
|
||||||
translation_key="movement",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
icon="mdi:cursor-move",
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
SENSORS_OLD: tuple[OwletSensorEntityDescription, ...] = (
|
|
||||||
OwletSensorEntityDescription(
|
|
||||||
key="oxygen_10_av",
|
|
||||||
translation_key="o2saturation10a",
|
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
icon="mdi:leaf",
|
|
||||||
),
|
|
||||||
OwletSensorEntityDescription(
|
|
||||||
key="movement_bucket",
|
|
||||||
translation_key="movementbucket",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
icon="mdi:bucket-outline",
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -112,21 +104,11 @@ async def async_setup_entry(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the owlet sensors from config entry."""
|
"""Set up the owlet sensors from config entry."""
|
||||||
|
|
||||||
coordinators: list[OwletCoordinator] = list(
|
coordinator: OwletCoordinator = hass.data[DOMAIN][config_entry.entry_id]
|
||||||
hass.data[DOMAIN][config_entry.entry_id].values()
|
|
||||||
)
|
|
||||||
|
|
||||||
sensors = []
|
entities = [OwletSensor(coordinator, sensor) for sensor in SENSORS]
|
||||||
|
|
||||||
sensor_list = SENSORS_ALL
|
async_add_entities(entities)
|
||||||
for coordinator in coordinators:
|
|
||||||
if coordinator.sock.revision < 5:
|
|
||||||
sensor_list += SENSORS_OLD
|
|
||||||
|
|
||||||
for sensor in sensor_list:
|
|
||||||
sensors.append(OwletSensor(coordinator, sensor))
|
|
||||||
|
|
||||||
async_add_entities(sensors)
|
|
||||||
|
|
||||||
|
|
||||||
class OwletSensor(OwletBaseEntity, SensorEntity):
|
class OwletSensor(OwletBaseEntity, SensorEntity):
|
||||||
@@ -135,39 +117,28 @@ class OwletSensor(OwletBaseEntity, SensorEntity):
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
coordinator: OwletCoordinator,
|
coordinator: OwletCoordinator,
|
||||||
description: OwletSensorEntityDescription,
|
sensor_description: OwletSensorEntityDescription,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the sensor."""
|
"""Initialize the sensor."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
self.entity_description: OwletSensorEntityDescription = description
|
self.entity_description = sensor_description
|
||||||
self._attr_unique_id = f"{self.sock.serial}-{description.key}"
|
self._attr_unique_id = f"{self.sock.serial}-{self.entity_description.name}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self):
|
||||||
"""Return sensor value."""
|
"""Return sensor value"""
|
||||||
|
|
||||||
if (
|
if (
|
||||||
self.entity_description.key
|
self.entity_description.element
|
||||||
in [
|
in [
|
||||||
"heart_rate",
|
"heart_rate",
|
||||||
"battery_minutes",
|
"battery_minutes",
|
||||||
"oxygen_saturation",
|
"oxygen_saturation",
|
||||||
"skin_temperature",
|
"skin_temperature",
|
||||||
"oxygen_10_av",
|
"oxygen_10_av",
|
||||||
"sleep_state",
|
|
||||||
]
|
]
|
||||||
and self.sock.properties["charging"]
|
and self.sock.properties["charging"]
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self.entity_description.key == "sleep_state":
|
return self.sock.properties[self.entity_description.element]
|
||||||
return SLEEP_STATES[self.sock.properties["sleep_state"]]
|
|
||||||
|
|
||||||
return self.sock.properties[self.entity_description.key]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def options(self) -> list[str]:
|
|
||||||
"""Set options for sleep state."""
|
|
||||||
if self.entity_description.key != "sleep_state":
|
|
||||||
return None
|
|
||||||
return list(SLEEP_STATES.values())
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user":{
|
||||||
"data": {
|
"title": "Enter login details",
|
||||||
|
"data":{
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
"username": "Email",
|
"username": "Email",
|
||||||
"password": "Password"
|
"password": "Password"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm":{
|
||||||
"title": "Reauthentiaction required for Owlet",
|
"title": "Reauthentiaction required for Owlet",
|
||||||
"data": {
|
"data":{
|
||||||
"password": "Password"
|
"password": "Password"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,94 +20,20 @@
|
|||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_email": "Entered email address is incorrect",
|
"invalid_email": "Entered email address is incorrect",
|
||||||
"invalid_password": "Entered password is incorrect",
|
"invalid_password": "Entered password is incorrect",
|
||||||
"invalid_credentials": "Entered credentials are incorrect",
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
"unknown": "Unknown error occured"
|
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Device already configured",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
"reauth_successful": "Reauthentication successful"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init":{
|
||||||
"title": "Configure options for Owlet",
|
"title":"Configure options for Owlet",
|
||||||
"data": {
|
"data":{
|
||||||
"pollinterval": "Polling interval in seconds, min 10"
|
"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"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user":{
|
||||||
"data": {
|
"title": "Enter login details",
|
||||||
|
"data":{
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
"username": "Email",
|
"username": "Email",
|
||||||
"password": "Password"
|
"password": "Password"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm":{
|
||||||
"title": "Reauthentiaction required for Owlet",
|
"title": "Reauthentiaction required for Owlet",
|
||||||
"data": {
|
"data":{
|
||||||
"password": "Password"
|
"password": "Password"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,94 +20,20 @@
|
|||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_email": "Entered email address is incorrect",
|
"invalid_email": "Entered email address is incorrect",
|
||||||
"invalid_password": "Entered password is incorrect",
|
"invalid_password": "Entered password is incorrect",
|
||||||
"invalid_credentials": "Entered credentials are incorrect",
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
"unknown": "Unknown error occured"
|
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Device already configured",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
"reauth_successful": "Reauthentication successful"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init":{
|
||||||
"title": "Configure options for Owlet",
|
"title":"Configure options for Owlet",
|
||||||
"data": {
|
"data":{
|
||||||
"pollinterval": "Polling interval in seconds, min 10"
|
"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"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
{
|
{
|
||||||
"config": {
|
"config": {
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user":{
|
||||||
"data": {
|
"title": "Enter login details",
|
||||||
|
"data":{
|
||||||
"region": "Region",
|
"region": "Region",
|
||||||
"username": "Email",
|
"username": "Email",
|
||||||
"password": "Password"
|
"password": "Password"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reauth_confirm": {
|
"reauth_confirm":{
|
||||||
"title": "Reauthentiaction required for Owlet",
|
"title": "Reauthentiaction required for Owlet",
|
||||||
"data": {
|
"data":{
|
||||||
"password": "Password"
|
"password": "Password"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,94 +20,20 @@
|
|||||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
"invalid_email": "Entered email address is incorrect",
|
"invalid_email": "Entered email address is incorrect",
|
||||||
"invalid_password": "Entered password is incorrect",
|
"invalid_password": "Entered password is incorrect",
|
||||||
"invalid_credentials": "Entered credentials are incorrect",
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
"unknown": "Unknown error occured"
|
|
||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "Device already configured",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
"reauth_successful": "Reauthentication successful"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"options": {
|
"options": {
|
||||||
"step": {
|
"step": {
|
||||||
"init": {
|
"init":{
|
||||||
"title": "Configure options for Owlet",
|
"title":"Configure options for Owlet",
|
||||||
"data": {
|
"data":{
|
||||||
"pollinterval": "Polling interval in seconds, min 10"
|
"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"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
20
info.md
20
info.md
@@ -1,6 +1,8 @@
|
|||||||
# Owlet Custom Integration
|
# Owlet Custom Integration
|
||||||
|
|
||||||
[![GitHub Release][releases-shield]][releases]
|
[![GitHub Release][releases-shield]][releases]
|
||||||
|
![GitHub all releases][download-all]
|
||||||
|
![GitHub release (latest by RyanClark123)][download-latest]
|
||||||
[![GitHub Activity][commits-shield]][commits]
|
[![GitHub Activity][commits-shield]][commits]
|
||||||
|
|
||||||
[![License][license-shield]][license]
|
[![License][license-shield]][license]
|
||||||
@@ -19,19 +21,23 @@ If you have a smart sock 2 and would like to contribute then please do so.
|
|||||||
3. Hard refresh browser cache.
|
3. Hard refresh browser cache.
|
||||||
4. [![Add Integration][add-integration-badge]][add-integration] or in the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Owlet Smart Sock".
|
4. [![Add Integration][add-integration-badge]][add-integration] or in the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Owlet Smart Sock".
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!---->
|
<!---->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[commits-shield]: https://img.shields.io/github/commit-activity/w/ryanbdclark/owlet?style=for-the-badge
|
[commits-shield]: https://img.shields.io/github/commit-activity/w/RyanClark123/owlet?style=for-the-badge
|
||||||
[commits]: https://github.com/ryanbdclark/owlet/commits/main
|
[commits]: https://github.com/RyanClark123/owlet/commits/main
|
||||||
[hacs]: https://github.com/hacs/integration
|
[hacs]: https://github.com/hacs/integration
|
||||||
[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge
|
[hacsbadge]: https://img.shields.io/badge/HACS-Custom-orange.svg?style=for-the-badge
|
||||||
[license]: LICENSE
|
[license]: LICENSE
|
||||||
[license-shield]: https://img.shields.io/github/license/ryanbdclark/owlet.svg?style=for-the-badge
|
[license-shield]: https://img.shields.io/github/license/RyanClark123/owlet.svg?style=for-the-badge
|
||||||
[maintenance-shield]: https://img.shields.io/badge/maintainer-Ryan%20Clark%20%40ryanbdclark-blue.svg?style=for-the-badge
|
[maintenance-shield]: https://img.shields.io/badge/maintainer-Ryan%20Clark%20%40RyanClark123-blue.svg?style=for-the-badge
|
||||||
[releases-shield]: https://img.shields.io/github/release/ryanbdclark/owlet.svg?style=for-the-badge
|
[releases-shield]: https://img.shields.io/github/release/RyanClark123/owlet.svg?style=for-the-badge
|
||||||
[releases]: https://github.com/ryanbdclark/owlet/releases
|
[releases]: https://github.com/RyanClark123/owlet/releases
|
||||||
[user_profile]: https://github.com/ryanbdclark
|
[user_profile]: https://github.com/RyanClark123
|
||||||
|
[download-all]: https://img.shields.io/github/downloads/RyanClark123/Owlet/total?style=for-the-badge
|
||||||
|
[download-latest]: https://img.shields.io/github/downloads/RyanClark123/Owlet/latest/total?style=for-the-badge
|
||||||
[add-integration]: https://my.home-assistant.io/redirect/config_flow_start?domain=owlet
|
[add-integration]: https://my.home-assistant.io/redirect/config_flow_start?domain=owlet
|
||||||
[add-integration-badge]: https://my.home-assistant.io/badges/config_flow_start.svg
|
[add-integration-badge]: https://my.home-assistant.io/badges/config_flow_start.svg
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
"""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
32
tests/fixtures/get_devices.json
vendored
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"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
36
tests/fixtures/get_devices_with_tokens.json
vendored
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"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
1051
tests/fixtures/update_properties_asleep.json
vendored
File diff suppressed because it is too large
Load Diff
1051
tests/fixtures/update_properties_awake.json
vendored
1051
tests/fixtures/update_properties_awake.json
vendored
File diff suppressed because it is too large
Load Diff
1055
tests/fixtures/update_properties_charging.json
vendored
1055
tests/fixtures/update_properties_charging.json
vendored
File diff suppressed because it is too large
Load Diff
@@ -1,237 +0,0 @@
|
|||||||
"""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}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
"""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
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
"""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)
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
"""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