Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ This integration provides the following entities:

- Binary sensors - charging status, high heart rate alert, low heart rate alert, high oxygen alert, low oxygen alert, low battery alert, lost power alert, sock diconnected alert, and sock status.
- Sensors - battery level, oxygen saturation, oxygen saturation 10 minute average, heart rate, battery time remaining, signal strength, and skin temperature.
- Button - acknowledge/clear an active alarm without leaving Home Assistant.

## Options

Expand Down
7 changes: 6 additions & 1 deletion custom_components/owlet/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@
from .const import CONF_OWLET_EXPIRY, CONF_OWLET_REFRESH, DOMAIN, SUPPORTED_VERSIONS
from .coordinator import OwletCoordinator

PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR, Platform.SWITCH]
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.SWITCH,
Platform.BUTTON,
]

_LOGGER = logging.getLogger(__name__)

Expand Down
70 changes: 70 additions & 0 deletions custom_components/owlet/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Support for Owlet buttons."""

from __future__ import annotations

from dataclasses import dataclass

from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback

from .const import DOMAIN
from .coordinator import OwletCoordinator
from .entity import OwletBaseEntity


@dataclass(kw_only=True)
class OwletButtonEntityDescription(ButtonEntityDescription):
"""Represent the Owlet button entity description."""


BUTTONS: tuple[OwletButtonEntityDescription, ...] = (
OwletButtonEntityDescription(
key="acknowledge_alarm",
translation_key="ack_alarm",
icon="mdi:bell-check-outline",
),
)


async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Owlet buttons from config entry."""
coordinators: list[OwletCoordinator] = list(
hass.data[DOMAIN][config_entry.entry_id].values()
)

buttons: list[OwletAlarmButton] = []

for coordinator in coordinators:
buttons.extend(
OwletAlarmButton(coordinator, description) for description in BUTTONS
)

async_add_entities(buttons)


class OwletAlarmButton(OwletBaseEntity, ButtonEntity):
"""Representation of the alarm acknowledge button."""

entity_description: OwletButtonEntityDescription

def __init__(
self,
coordinator: OwletCoordinator,
description: OwletButtonEntityDescription,
) -> None:
"""Initialize the button."""
super().__init__(coordinator)
self.entity_description = description
self._attr_unique_id = f"{self.sock.serial}-{description.key}"

async def async_press(self) -> None:
"""Handle the button press."""
await self.sock.acknowledge_alert()
await self.coordinator.async_request_refresh()

4 changes: 2 additions & 2 deletions custom_components/owlet/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"iot_class": "cloud_polling",
"issue_tracker": "https://github.com/ryanbdclark/owlet/issues",
"requirements": [
"pyowletapi==2025.4.1"
"pyowletapi@git+https://github.com/mrThe/pyowletapi.git@7b21d46c5cbd000ca94e835dbf83f9c36064cd7d"
],
"version": "2025.4.3"
"version": "2025.4.4"
}
5 changes: 5 additions & 0 deletions custom_components/owlet/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
}
},
"entity": {
"button": {
"ack_alarm": {
"name": "Acknowledge alarm"
}
},
"binary_sensor": {
"charging": {
"name": "Charging"
Expand Down
5 changes: 5 additions & 0 deletions custom_components/owlet/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@
}
},
"entity": {
"button": {
"ack_alarm": {
"name": "Acknowledge alarm"
}
},
"binary_sensor": {
"awake": {
"name": "Awake"
Expand Down
5 changes: 5 additions & 0 deletions custom_components/owlet/translations/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
}
},
"entity": {
"button": {
"ack_alarm": {
"name": "Accuser l'alarme"
}
},
"binary_sensor": {
"charging": {
"name": "En charge"
Expand Down
5 changes: 5 additions & 0 deletions custom_components/owlet/translations/uk.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
}
},
"entity": {
"button": {
"ack_alarm": {
"name": "Acknowledge alarm"
}
},
"binary_sensor": {
"charging": {
"name": "Charging"
Expand Down
4 changes: 4 additions & 0 deletions info.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ A custom component for the Owlet smart sock

<!---->

## Features

- Dedicated Home Assistant button entity to acknowledge active Owlet Sock alarms.

---

[commits-shield]: https://img.shields.io/github/commit-activity/w/ryanbdclark/owlet?style=for-the-badge
Expand Down
49 changes: 49 additions & 0 deletions tests/test_button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""Test Owlet button platform."""
from __future__ import annotations

from unittest.mock import AsyncMock, patch

from homeassistant.const import Platform
from homeassistant.core import HomeAssistant

from . import async_init_integration

BUTTON_ENTITY_ID = "button.owlet_baby_care_sock_acknowledge_alarm"


async def test_button_entity_exists(hass: HomeAssistant) -> None:
"""Ensure the acknowledge button is created."""
await async_init_integration(
hass, properties_fixture="update_properties_awake.json"
)

buttons = hass.states.async_all(Platform.BUTTON)
assert len(buttons) == 1

state = hass.states.get(BUTTON_ENTITY_ID)
assert state is not None


async def test_button_press_triggers_acknowledge(hass: HomeAssistant) -> None:
"""Ensure pressing the button calls the pause alert command."""
await async_init_integration(
hass, properties_fixture="update_properties_awake.json"
)

with patch(
"pyowletapi.sock.Sock.acknowledge_alert",
AsyncMock(return_value=True),
) as mock_ack, patch(
"homeassistant.components.owlet.button.OwletCoordinator.async_request_refresh",
AsyncMock(return_value=None),
) as mock_refresh:
await hass.services.async_call(
Platform.BUTTON,
"press",
{"entity_id": BUTTON_ENTITY_ID},
blocking=True,
)

assert mock_ack.await_count == 1
assert mock_refresh.await_count == 1

9 changes: 7 additions & 2 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@

from . import async_init_integration

PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
PLATFORMS: list[Platform] = [
Platform.BINARY_SENSOR,
Platform.SENSOR,
Platform.SWITCH,
Platform.BUTTON,
]


async def test_async_setup_entry(hass: HomeAssistant) -> None:
Expand All @@ -43,7 +48,7 @@ async def test_async_setup_entry(hass: HomeAssistant) -> None:

entities = er.async_entries_for_device(entity_registry, device_entry.id)

assert len(entities) == 18
assert len(entities) == 19

await entry.async_unload(hass)

Expand Down