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
|
|
||||||
|
|||||||
54
CHANGELOG.md
54
CHANGELOG.md
@@ -1,60 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
<!--next-version-placeholder-->
|
<!--next-version-placeholder-->
|
||||||
## 2023.08.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.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 ([`dc58b19`](https://github.com/ryanbdclark/owlet/commit/0141f7d01a9ac9b3e1dcc74cabb896e19bd4a821))
|
|
||||||
|
|
||||||
## 2023.05.4 (2023-05-17)
|
|
||||||
#### Fix
|
|
||||||
* Bumping to pyowletapi 2023.5.25
|
|
||||||
|
|
||||||
## 2023.05.3 (2023-05-17)
|
|
||||||
#### Fix
|
|
||||||
* Bumping to pyowletapi 2023.5.24
|
|
||||||
* Reauthing now no longer re adds users' password to config entry
|
|
||||||
|
|
||||||
## 2023.05.2 (2023-05-16)
|
|
||||||
#### Feature
|
|
||||||
* Integration now makes use of refresh token from pyowletapi to reauthenticate, user password in no longer stored by integration ([`dc710a1`](https://github.com/ryanbdclark/owlet/commit/dc710a1783a4cad9d6cf355240fe12ac779a87ef))
|
|
||||||
* New sensors create for baby sleep state ([`9b3392b`](https://github.com/ryanbdclark/owlet/commit/9b3392bdbcd82015ed31d3a50a517e4e22905684))
|
|
||||||
|
|
||||||
## 2023.05.1 (2023-05-15)
|
|
||||||
#### 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,61 +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,
|
||||||
entity_registry_enabled_default=False,
|
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",
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -84,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):
|
||||||
@@ -99,27 +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]
|
||||||
|
|
||||||
entity = self.entity_description.key
|
|
||||||
|
|
||||||
if self.sock.properties["charging"] and entity in ["sleep_state"]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if entity == "sleep_state":
|
|
||||||
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=self.sock.version,
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
{
|
{
|
||||||
"domain": "owlet",
|
"domain": "owlet",
|
||||||
"name": "Owlet Smart Sock",
|
"name": "Owlet Smart Sock",
|
||||||
"codeowners": ["@ryanbdclark"],
|
"codeowners": [
|
||||||
|
"@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.7.2"],
|
"pyowletapi==2023.5.20"
|
||||||
"version":"2023.8.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,94 +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: 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="oxygen_10_av",
|
key="oxygensaturation10a",
|
||||||
translation_key="o2saturation10a",
|
name="O2 Saturation 10 Minute Average",
|
||||||
native_unit_of_measurement=PERCENTAGE,
|
native_unit_of_measurement=PERCENTAGE,
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
element="oxygen_10_av",
|
||||||
icon="mdi:leaf",
|
icon="mdi:leaf",
|
||||||
),
|
),
|
||||||
OwletSensorEntityDescription(
|
OwletSensorEntityDescription(
|
||||||
key="heart_rate",
|
key="heartrate",
|
||||||
translation_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,
|
|
||||||
),
|
|
||||||
OwletSensorEntityDescription(
|
|
||||||
key="movement_bucket",
|
|
||||||
translation_key="movementbucket",
|
|
||||||
state_class=SensorStateClass.MEASUREMENT,
|
|
||||||
icon="mdi:bucket-outline",
|
|
||||||
entity_registry_enabled_default=False,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -109,15 +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()
|
|
||||||
)
|
|
||||||
|
|
||||||
async_add_entities(
|
entities = [OwletSensor(coordinator, sensor) for sensor in SENSORS]
|
||||||
OwletSensor(coordinator, sensor)
|
|
||||||
for coordinator in coordinators
|
async_add_entities(entities)
|
||||||
for sensor in SENSORS
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class OwletSensor(OwletBaseEntity, SensorEntity):
|
class OwletSensor(OwletBaseEntity, SensorEntity):
|
||||||
@@ -126,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