diff --git a/README.md b/README.md index ded9c86..8dce98e 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/custom_components/owlet/__init__.py b/custom_components/owlet/__init__.py index a6f93f1..e8dff80 100644 --- a/custom_components/owlet/__init__.py +++ b/custom_components/owlet/__init__.py @@ -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__) diff --git a/custom_components/owlet/button.py b/custom_components/owlet/button.py new file mode 100644 index 0000000..0c10aaf --- /dev/null +++ b/custom_components/owlet/button.py @@ -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() + diff --git a/custom_components/owlet/manifest.json b/custom_components/owlet/manifest.json index b0d699d..934683b 100644 --- a/custom_components/owlet/manifest.json +++ b/custom_components/owlet/manifest.json @@ -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" } diff --git a/custom_components/owlet/strings.json b/custom_components/owlet/strings.json index 6f9ec72..d8c4496 100644 --- a/custom_components/owlet/strings.json +++ b/custom_components/owlet/strings.json @@ -38,6 +38,11 @@ } }, "entity": { + "button": { + "ack_alarm": { + "name": "Acknowledge alarm" + } + }, "binary_sensor": { "charging": { "name": "Charging" diff --git a/custom_components/owlet/translations/en.json b/custom_components/owlet/translations/en.json index 1e1e4fb..23e6259 100644 --- a/custom_components/owlet/translations/en.json +++ b/custom_components/owlet/translations/en.json @@ -28,6 +28,11 @@ } }, "entity": { + "button": { + "ack_alarm": { + "name": "Acknowledge alarm" + } + }, "binary_sensor": { "awake": { "name": "Awake" diff --git a/custom_components/owlet/translations/fr.json b/custom_components/owlet/translations/fr.json index 7a1d32e..28fdce0 100644 --- a/custom_components/owlet/translations/fr.json +++ b/custom_components/owlet/translations/fr.json @@ -38,6 +38,11 @@ } }, "entity": { + "button": { + "ack_alarm": { + "name": "Accuser l'alarme" + } + }, "binary_sensor": { "charging": { "name": "En charge" diff --git a/custom_components/owlet/translations/uk.json b/custom_components/owlet/translations/uk.json index f3a0e95..4d872cb 100644 --- a/custom_components/owlet/translations/uk.json +++ b/custom_components/owlet/translations/uk.json @@ -38,6 +38,11 @@ } }, "entity": { + "button": { + "ack_alarm": { + "name": "Acknowledge alarm" + } + }, "binary_sensor": { "charging": { "name": "Charging" diff --git a/info.md b/info.md index c57a99a..c21956a 100644 --- a/info.md +++ b/info.md @@ -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 diff --git a/tests/test_button.py b/tests/test_button.py new file mode 100644 index 0000000..1cec810 --- /dev/null +++ b/tests/test_button.py @@ -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 + diff --git a/tests/test_init.py b/tests/test_init.py index 8718023..861f64c 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -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: @@ -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)