From 454ca0ce955cf2a0ddc769da3da4cae68506ec5c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 18 Jul 2024 01:40:05 +0200 Subject: [PATCH] Add timer support to mobile app (#121469) * Add timer support to mobile app * Fix tests * Make it time-sensitive --- .../components/mobile_app/__init__.py | 13 +++- .../components/mobile_app/manifest.json | 9 ++- homeassistant/components/mobile_app/timers.py | 52 ++++++++++++++ tests/components/mobile_app/test_timers.py | 68 +++++++++++++++++++ 4 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/mobile_app/timers.py create mode 100644 tests/components/mobile_app/test_timers.py diff --git a/homeassistant/components/mobile_app/__init__.py b/homeassistant/components/mobile_app/__init__.py index 4c40e4f22b3..a8577cc596d 100644 --- a/homeassistant/components/mobile_app/__init__.py +++ b/homeassistant/components/mobile_app/__init__.py @@ -1,9 +1,10 @@ """Integrates Native Apps to Home Assistant.""" from contextlib import suppress +from functools import partial from typing import Any -from homeassistant.components import cloud, notify as hass_notify +from homeassistant.components import cloud, intent, notify as hass_notify from homeassistant.components.webhook import ( async_register as webhook_register, async_unregister as webhook_unregister, @@ -46,7 +47,8 @@ from .const import ( ) from .helpers import savable_state from .http_api import RegistrationsView -from .util import async_create_cloud_hook +from .timers import async_handle_timer_event +from .util import async_create_cloud_hook, supports_push from .webhook import handle_webhook PLATFORMS = [Platform.BINARY_SENSOR, Platform.DEVICE_TRACKER, Platform.SENSOR] @@ -133,6 +135,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) + if supports_push(hass, webhook_id): + entry.async_on_unload( + intent.async_register_timer_handler( + hass, device.id, partial(async_handle_timer_event, hass, entry) + ) + ) + await hass_notify.async_reload(hass, DOMAIN) return True diff --git a/homeassistant/components/mobile_app/manifest.json b/homeassistant/components/mobile_app/manifest.json index aeab576a7cd..5fdb43f704a 100644 --- a/homeassistant/components/mobile_app/manifest.json +++ b/homeassistant/components/mobile_app/manifest.json @@ -4,7 +4,14 @@ "after_dependencies": ["cloud", "camera", "conversation", "notify"], "codeowners": ["@home-assistant/core"], "config_flow": true, - "dependencies": ["http", "webhook", "person", "tag", "websocket_api"], + "dependencies": [ + "http", + "intent", + "person", + "tag", + "webhook", + "websocket_api" + ], "documentation": "https://www.home-assistant.io/integrations/mobile_app", "iot_class": "local_push", "loggers": ["nacl"], diff --git a/homeassistant/components/mobile_app/timers.py b/homeassistant/components/mobile_app/timers.py new file mode 100644 index 00000000000..93b4ac53be5 --- /dev/null +++ b/homeassistant/components/mobile_app/timers.py @@ -0,0 +1,52 @@ +"""Timers for the mobile app.""" + +from datetime import timedelta + +from homeassistant.components import notify +from homeassistant.components.intent.timers import TimerEventType, TimerInfo +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_DEVICE_ID +from homeassistant.core import HomeAssistant, callback + +from . import device_action + + +@callback +def async_handle_timer_event( + hass: HomeAssistant, + entry: ConfigEntry, + event_type: TimerEventType, + timer_info: TimerInfo, +) -> None: + """Handle timer events.""" + if event_type != TimerEventType.FINISHED: + return + + if timer_info.name: + message = f"{timer_info.name} finished" + else: + message = f"{timedelta(seconds=timer_info.created_seconds)} timer finished" + + entry.async_create_task( + hass, + device_action.async_call_action_from_config( + hass, + { + CONF_DEVICE_ID: timer_info.device_id, + notify.ATTR_MESSAGE: message, + notify.ATTR_DATA: { + "group": "timers", + # Android + "channel": "Timers", + "importance": "high", + # iOS + "push": { + "interruption-level": "time-sensitive", + }, + }, + }, + {}, + None, + ), + "mobile_app_timer_notification", + ) diff --git a/tests/components/mobile_app/test_timers.py b/tests/components/mobile_app/test_timers.py new file mode 100644 index 00000000000..0eba88f7328 --- /dev/null +++ b/tests/components/mobile_app/test_timers.py @@ -0,0 +1,68 @@ +"""Test mobile app timers.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.mobile_app import DATA_DEVICES, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import intent as intent_helper + + +@pytest.mark.parametrize( + ("intent_args", "message"), + [ + ( + {}, + "0:02:00 timer finished", + ), + ( + {"name": {"value": "pizza"}}, + "pizza finished", + ), + ], +) +async def test_timer_events( + hass: HomeAssistant, push_registration, intent_args: dict, message: str +) -> None: + """Test for timer events.""" + webhook_id = push_registration["webhook_id"] + device_id = hass.data[DOMAIN][DATA_DEVICES][webhook_id].id + + await intent_helper.async_handle( + hass, + "test", + intent_helper.INTENT_START_TIMER, + { + "minutes": {"value": 2}, + } + | intent_args, + device_id=device_id, + ) + + with patch( + "homeassistant.components.mobile_app.notify.MobileAppNotificationService.async_send_message" + ) as mock_send_message: + await intent_helper.async_handle( + hass, + "test", + intent_helper.INTENT_DECREASE_TIMER, + { + "minutes": {"value": 2}, + }, + device_id=device_id, + ) + await hass.async_block_till_done() + + assert mock_send_message.mock_calls[0][2] == { + "target": [webhook_id], + "message": message, + "data": { + "channel": "Timers", + "group": "timers", + "importance": "high", + "push": { + "interruption-level": "time-sensitive", + }, + }, + }