14 Commits

Author SHA1 Message Date
RyanClark123
45e65f384b Updating version in manifest and changelog 2025-04-15 19:14:31 +01:00
ryanbdclark
df6b45621e Merge pull request #25 from MarjovanLier/fix-multiple-devices
(Fixed) Ensure entities from multiple Owlet devices register correctly
2025-04-15 19:08:46 +01:00
Marjo Wenzel van Lier
1244bffcb4 fix(entity): Ensure entities from multiple devices register correctly
- Modify sensor and switch setup to use `extend` instead of list
  reassignment. This prevents overwriting entities from previously
  processed devices.
- Update `OwletBaseEntity` initialisation to correctly store the
  coordinator instance.
- Refine device information retrieval using `getattr` for enhanced
  robustness and provide more specific device details (e.g., serial
  number in name).

This change addresses a bug where, in setups with multiple Owlet devices,
only the entities belonging to the last device in the configuration
were registered. Using `extend` ensures all entities across all devices
are correctly added. Device information presentation is also improved.
2025-04-15 00:36:04 +02:00
RyanClark123
d323cbfd11 Bumping pyowletapi
### Fix
* Bumping pyowletapi to 2025.4.1, should hopefully stop issue where only one device was added to HA.
2025-04-14 14:53:04 +01:00
RyanClark123
2accec2b49 Merge branch 'main' of https://github.com/ryanbdclark/owlet 2025-04-11 13:51:11 +01:00
RyanClark123
6b343a76ca Fix for errors after refactoring of api
### Fix
* Changes to stop errors after refactoring pyowletapi
2025-04-11 13:50:59 +01:00
ryanbdclark
fa2e06dcf4 Merge pull request #23 from jusso-dev/patch-1
Update README.md
2025-04-11 13:37:12 +01:00
ryanbdclark
975e98c337 Update CHANGELOG.md 2025-04-11 13:35:12 +01:00
RyanClark123
268365ccd4 Bumping pyowletapi
### Fix
* Bumping pyowletapi to 2025.4.0
2025-04-11 13:34:11 +01:00
ryanbdclark
ac9c8c6111 Update manifest.json
Added documentation link
2025-04-05 21:42:48 +01:00
ryanbdclark
4fa40f8621 Update manifest.json
Remove documentation link
2025-04-05 21:38:38 +01:00
Justin Middler
c04d6b7bf8 Update README.md 2025-04-01 15:01:19 +11:00
RyanClark123
dd17aca283 Merge branch 'main' of https://github.com/ryanbdclark/owlet 2024-10-10 11:23:35 +01:00
RyanClark123
91578464de Refactoring
### Fix
* Refactoring
* Changed switch to follow a more homeassistant pattern and allow for easier addition of future switches
2024-10-10 11:23:30 +01:00
11 changed files with 170 additions and 118 deletions

View File

@@ -1,6 +1,23 @@
# Changelog
<!--next-version-placeholder-->
## 2025.4.3 (2025-04-15)
### Fix
* Bumping pyowletapi to 2025.4.8.
* Changes to how the sensors are stored to solve the issue where only one device is added, thanks [`@MarjovanLier`](https://github.com/MarjovanLier). ([`1244bff`](https://github.com/ryanbdclark/owlet/commit/1244bffcb48d7337a9d7a0da518959fe4b31a230))
## 2025.4.2 (2025-04-14)
### Fix
* Bumping pyowletapi to 2025.4.1, should hopefully stop issue where only one device was added to HA. ([`d323cbf`](https://github.com/ryanbdclark/owlet/commit/d323cbfd11411ff34866ead492de10c109c72689))
## 2025.4.1 (2025-04-11)
### Fix
* Changes to stop errors after refactoring pyowletapi ([`6b343a7`](https://github.com/ryanbdclark/owlet/commit/6b343a76caad3375e10c80f4d26942a1bbbb831d))
## 2025.4.0 (2025-04-11)
### Fix
* Bumping pyowletapi to 2025.4.0 ([`268365c`](https://github.com/ryanbdclark/owlet/commit/268365ccd428418dd5707f0569ce738b54a12fdd))
## 2024.10.1 (2024-10-09)
### Feature
* Base station has now been removed from binary sensors and added as a switch ([`f63e0a6`](https://github.com/ryanbdclark/owlet/commit/f63e0a6dfeab1a05ba09ef3e0087cb404ba0dac4))

View File

@@ -12,7 +12,7 @@ A custom component for the Owlet smart sock
## Installation
1. Use [HACS](https://hacs.xyz/docs/setup/download), in `HACS > Integrations > Explore & Add Repositories` search for "Owlet".
1. Use [HACS](https://hacs.xyz/docs/use/download/download/), in `HACS > Integrations > Explore & Add Repositories` search for "Owlet".
2. Restart Home Assistant.
3. [![Add Integration][add-integration-badge]][add-integration] or in the HA UI go to "Settings" -> "Devices & Services" then click "+" and search for "Owlet Smart Sock".

View File

@@ -68,7 +68,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.error("No owlet devices found to set up")
return False
if devices["tokens"]:
if "tokens" in devices:
hass.config_entries.async_update_entry(
entry, data={**entry.data, **devices["tokens"]}
)

View File

@@ -106,9 +106,11 @@ async def async_setup_entry(
sensors = []
for coordinator in coordinators:
for sensor in SENSORS:
if sensor.key in coordinator.sock.properties:
sensors.append(OwletBinarySensor(coordinator, sensor))
sensors.extend([
OwletBinarySensor(coordinator, sensor)
for sensor in SENSORS
if sensor.key in coordinator.sock.properties
])
if OwletAwakeSensor.entity_description.key in coordinator.sock.properties:
sensors.append(OwletAwakeSensor(coordinator))
@@ -140,13 +142,6 @@ class OwletBinarySensor(OwletBaseEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
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 self.sock.properties[self.entity_description.key]
@@ -171,8 +166,4 @@ class OwletAwakeSensor(OwletBinarySensor):
@property
def is_on(self) -> bool:
"""Return true if the binary sensor is on."""
return (
False
if self.sock.properties[self.entity_description.key] in [8, 15]
else True
)
return self.sock.properties[self.entity_description.key] not in [8, 15]

View File

@@ -1,4 +1,5 @@
"""Config flow for Owlet Smart Sock integration."""
from __future__ import annotations
from collections.abc import Mapping
@@ -15,19 +16,17 @@ from pyowletapi.exceptions import (
import voluptuous as vol
from homeassistant import config_entries, exceptions
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigFlowResult
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.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_OWLET_EXPIRY, CONF_OWLET_REFRESH, DOMAIN, POLLING_INTERVAL
from .const import DOMAIN, POLLING_INTERVAL
_LOGGER = logging.getLogger(__name__)
@@ -51,7 +50,7 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
) -> ConfigFlowResult:
"""Handle the initial step."""
errors: dict[str, str] = {}
if user_input is not None:
@@ -101,7 +100,9 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult:
async def async_step_reauth(
self, user_input: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle reauth."""
self.reauth_entry = self.hass.config_entries.async_get_entry(
self.context["entry_id"]
@@ -110,7 +111,7 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
assert self.reauth_entry is not None
errors: dict[str, str] = {}
@@ -124,13 +125,14 @@ class OwletConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
session=async_get_clientsession(self.hass),
)
try:
token = await owlet_api.authenticate()
if token:
if token := await owlet_api.authenticate():
self.hass.config_entries.async_update_entry(
self.reauth_entry, data={**entry_data, **token}
)
await self.hass.config_entries.async_reload(self.reauth_entry.entry_id)
await self.hass.config_entries.async_reload(
self.reauth_entry.entry_id
)
return self.async_abort(reason="reauth_successful")
@@ -155,7 +157,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow):
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> FlowResult:
) -> ConfigFlowResult:
"""Handle options flow."""
if user_input is not None:
return self.async_create_entry(title="", data=user_input)

View File

@@ -42,7 +42,7 @@ class OwletCoordinator(DataUpdateCoordinator):
"""Fetch the data from the device."""
try:
properties = await self.sock.update_properties()
if properties["tokens"]:
if "tokens" in properties:
self.hass.config_entries.async_update_entry(
self.config_entry,
data={**self.config_entry.data, **properties["tokens"]},

View File

@@ -1,11 +1,11 @@
"""Base class for Owlet entities."""
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.device_registry import DeviceInfo
from .coordinator import OwletCoordinator
from .const import DOMAIN, MANUFACTURER
from .coordinator import OwletCoordinator
class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
@@ -19,6 +19,7 @@ class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
) -> None:
"""Initialize the base entity."""
super().__init__(coordinator)
self.coordinator = coordinator
self.sock = coordinator.sock
@property
@@ -26,9 +27,12 @@ class OwletBaseEntity(CoordinatorEntity[OwletCoordinator], Entity):
"""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}",
name=f"Owlet Sock {self.sock.serial}",
connections={("mac", getattr(self.sock, "mac", "unknown"))},
suggested_area="Nursery",
configuration_url="https://my.owletcare.com/",
manufacturer="Owlet Baby Care",
model=getattr(self.sock, "model", None),
sw_version=getattr(self.sock, "sw_version", None),
hw_version=getattr(self.sock, "hw_version", "3r8"),
)

View File

@@ -5,11 +5,11 @@
"@ryanbdclark"
],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/owlet",
"documentation":"https://github.com/ryanbdclark/owlet",
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/ryanbdclark/owlet/issues",
"requirements": [
"pyowletapi==2024.10.1"
"pyowletapi==2025.4.8"
],
"version": "2024.10.1"
"version": "2025.4.3"
}

View File

@@ -115,9 +115,11 @@ async def async_setup_entry(
sensors = []
for coordinator in coordinators:
for sensor in SENSORS:
if sensor.key in coordinator.sock.properties:
sensors.append(OwletSensor(coordinator, sensor))
sensors.extend([
OwletSensor(coordinator, sensor)
for sensor in SENSORS
if sensor.key in coordinator.sock.properties
])
if OwletSleepSensor.entity_description.key in coordinator.sock.properties:
sensors.append(OwletSleepSensor(coordinator))

View File

@@ -2,12 +2,15 @@
from __future__ import annotations
from collections.abc import Callable, Coroutine
from dataclasses import dataclass
from datetime import timedelta
from typing import Any
from homeassistant.config_entries import ConfigEntry
from pyowletapi.sock import Sock
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -19,6 +22,26 @@ SCAN_INTERVAL = timedelta(seconds=5)
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class OwletSwitchEntityDescription(SwitchEntityDescription):
"""Describes Owlet switch entity."""
turn_on_fn: Callable[[Sock], Callable[[bool], Coroutine[Any, Any, None]]]
turn_off_fn: Callable[[Sock], Callable[[bool], Coroutine[Any, Any, None]]]
available_during_charging: bool
SWITCHES: tuple[OwletSwitchEntityDescription, ...] = (
OwletSwitchEntityDescription(
key="base_station_on",
translation_key="base_on",
turn_on_fn=lambda sock: (lambda state: sock.control_base_station(state)),
turn_off_fn=lambda sock: (lambda state: sock.control_base_station(state)),
available_during_charging=False,
),
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
@@ -29,30 +52,43 @@ async def async_setup_entry(
switches = []
for coordinator in coordinators:
switches.append(OwletBaseSwitch(coordinator))
switches.extend([OwletBaseSwitch(coordinator, switch) for switch in SWITCHES])
async_add_entities(switches)
class OwletBaseSwitch(OwletBaseEntity, SwitchEntity):
"""Defines a Owlet switch."""
_attr_has_entity_name = True
_attr_translation_key = "base_on"
entity_description: OwletSwitchEntityDescription
def __init__(self, coordinator: OwletCoordinator) -> None:
"""Initialize ecobee ventilator platform."""
def __init__(
self,
coordinator: OwletCoordinator,
description: OwletSwitchEntityDescription,
) -> None:
"""Initialize owlet switch platform."""
super().__init__(coordinator)
self._attr_unique_id = f"{self.sock.serial}-base_station_on"
self.entity_description = description
self._attr_unique_id = f"{self.sock.serial}-{description.key}"
self._attr_is_on = False
@property
def is_on(self):
return self.sock.properties["base_station_on"]
def available(self) -> bool:
"""Return if entity is available."""
return super().available and (
not self.sock.properties["charging"]
or self.entity_description.available_during_charging
)
@property
def is_on(self) -> bool:
"""Return if switch is on or off."""
return self.sock.properties[self.entity_description.key]
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn on the switch."""
await self.sock.control_base_station(True)
await self.entity_description.turn_on_fn(self.sock)(True)
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off the switch."""
await self.sock.control_base_station(False)
await self.entity_description.turn_off_fn(self.sock)(False)

View File

@@ -1,87 +1,86 @@
{
"config": {
"step": {
"user": {
"data": {
"region": "Region",
"username": "Email",
"password": "Password"
}
},
"reauth_confirm": {
"title": "Reauthentiaction required for Owlet",
"data": {
"password": "Password"
}
}
},
"error": {
"cannot_connect": "Failed to connect",
"invalid_email": "Entered email address is incorrect",
"invalid_password": "Entered password is incorrect",
"invalid_credentials": "Entered credentials are incorrect",
"unknown": "Unknown error occured"
},
"abort": {
"already_configured": "Device already configured",
"reauth_successful": "Reauthentication successful"
}
},
"options": {
"error": {
"cannot_connect": "Failed to connect",
"invalid_credentials": "Entered credentials are incorrect",
"invalid_email": "Entered email address is incorrect",
"invalid_password": "Entered password is incorrect",
"unknown": "Unknown error occured"
},
"step": {
"init": {
"title": "Configure options for Owlet",
"reauth_confirm": {
"data": {
"pollinterval": "Polling interval in seconds, min 10"
"password": "Password"
},
"title": "Reauthentiaction required for Owlet"
},
"user": {
"data": {
"password": "Password",
"region": "Region",
"username": "Email"
}
}
}
},
"entity": {
"binary_sensor": {
"awake": {
"name": "Awake"
},
"charging": {
"name": "Charging"
},
"high_hr_alrt": {
"name": "High heart rate alert"
},
"low_hr_alrt": {
"name": "Low heart rate alert"
},
"high_ox_alrt": {
"name": "High oxygen alert"
},
"low_ox_alrt": {
"name": "Low oxygen alert"
},
"crit_ox_alrt": {
"name": "Critical oxygen alert"
},
"low_batt_alrt": {
"name": "Low battery alert"
},
"crit_batt_alrt": {
"name": "Critical battery alert"
},
"crit_ox_alrt": {
"name": "Critical oxygen alert"
},
"high_hr_alrt": {
"name": "High heart rate alert"
},
"high_ox_alrt": {
"name": "High oxygen alert"
},
"lost_pwr_alrt": {
"name": "Lost power alert"
},
"low_batt_alrt": {
"name": "Low battery alert"
},
"low_hr_alrt": {
"name": "Low heart rate alert"
},
"low_ox_alrt": {
"name": "Low oxygen alert"
},
"sock_discon_alrt": {
"name": "Sock disconnected alert"
},
"sock_off": {
"name": "Sock off"
},
"awake": {
"name": "Awake"
}
},
"sensor": {
"batterymin": {
"name": "Battery remaining"
},
"batterypercent": {
"name": "Battery percentage"
},
"signalstrength": {
"name": "Signal strength"
"heartrate": {
"name": "Heart rate"
},
"movement": {
"name": "Movement"
},
"movementbucket": {
"name": "Movement bucket"
},
"o2saturation": {
"name": "O2 saturation"
@@ -89,11 +88,8 @@
"o2saturation10a": {
"name": "O2 saturation 10 minute average"
},
"heartrate": {
"name": "Heart rate"
},
"batterymin": {
"name": "Battery remaining"
"signalstrength": {
"name": "Signal strength"
},
"skintemp": {
"name": "Skin temperature"
@@ -101,17 +97,11 @@
"sleepstate": {
"name": "Sleep state",
"state": {
"unknown": "Unknown",
"awake": "Awake",
"deep_sleep": "Deep sleep",
"light_sleep": "Light sleep",
"deep_sleep": "Deep sleep"
"unknown": "Unknown"
}
},
"movement": {
"name": "Movement"
},
"movementbucket": {
"name": "Movement bucket"
}
},
"switch": {
@@ -119,5 +109,15 @@
"name": "Base station on"
}
}
},
"options": {
"step": {
"init": {
"data": {
"pollinterval": "Polling interval in seconds, min 10"
},
"title": "Configure options for Owlet"
}
}
}
}