From e952751b2dfd1c36c975958d06d12847dfaede18 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 16 Sep 2026 17:39:50 +0000 Subject: [PATCH] Add notify entity to LaMetric (#181971) Co-authored-by: Claude --- homeassistant/components/lametric/const.py | 1 + homeassistant/components/lametric/helpers.py | 6 +- homeassistant/components/lametric/notify.py | 45 ++++++- .../components/lametric/strings.json | 13 ++ .../lametric/snapshots/test_notify.ambr | 52 ++++++++ tests/components/lametric/test_notify.py | 111 +++++++++++++++++- 6 files changed, 223 insertions(+), 5 deletions(-) create mode 100644 tests/components/lametric/snapshots/test_notify.ambr diff --git a/homeassistant/components/lametric/const.py b/homeassistant/components/lametric/const.py index 8c05b15ad1f5..0ca6762f4850 100644 --- a/homeassistant/components/lametric/const.py +++ b/homeassistant/components/lametric/const.py @@ -9,6 +9,7 @@ from homeassistant.const import Platform DOMAIN: Final = "lametric" PLATFORMS = [ Platform.BUTTON, + Platform.NOTIFY, Platform.NUMBER, Platform.SELECT, Platform.SENSOR, diff --git a/homeassistant/components/lametric/helpers.py b/homeassistant/components/lametric/helpers.py index a0fab0ec31a6..b50d07298eae 100644 --- a/homeassistant/components/lametric/helpers.py +++ b/homeassistant/components/lametric/helpers.py @@ -34,12 +34,14 @@ def lametric_exception_handler[_LaMetricEntityT: LaMetricEntity, **_P]( self.coordinator.last_update_success = False self.coordinator.async_update_listeners() raise HomeAssistantError( - "Error communicating with the LaMetric device" + translation_domain=DOMAIN, + translation_key="communication_error", ) from error except LaMetricError as error: raise HomeAssistantError( - "Invalid response from the LaMetric device" + translation_domain=DOMAIN, + translation_key="invalid_response", ) from error return handler diff --git a/homeassistant/components/lametric/notify.py b/homeassistant/components/lametric/notify.py index c6e0e9bec8f1..7972486d58f1 100644 --- a/homeassistant/components/lametric/notify.py +++ b/homeassistant/components/lametric/notify.py @@ -15,15 +15,56 @@ from demetriek import ( Sound, ) -from homeassistant.components.notify import ATTR_DATA, BaseNotificationService +from homeassistant.components.notify import ( + ATTR_DATA, + BaseNotificationService, + NotifyEntity, +) from homeassistant.const import CONF_ICON from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.util.enum import try_parse_enum from .const import CONF_CYCLES, CONF_ICON_TYPE, CONF_PRIORITY, CONF_SOUND -from .coordinator import LaMetricConfigEntry +from .coordinator import LaMetricConfigEntry, LaMetricDataUpdateCoordinator +from .entity import LaMetricEntity +from .helpers import lametric_exception_handler + +PARALLEL_UPDATES = 1 + + +async def async_setup_entry( + hass: HomeAssistant, + entry: LaMetricConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up LaMetric notify entity based on a config entry.""" + async_add_entities([LaMetricNotifyEntity(entry.runtime_data)]) + + +class LaMetricNotifyEntity(LaMetricEntity, NotifyEntity): + """Representation of a LaMetric notify entity.""" + + _attr_translation_key = "message" + + def __init__(self, coordinator: LaMetricDataUpdateCoordinator) -> None: + """Initialize the notify entity.""" + super().__init__(coordinator=coordinator) + self._attr_unique_id = f"{coordinator.data.serial_number}-message" + + @lametric_exception_handler + @override + async def async_send_message(self, message: str, title: str | None = None) -> None: + """Send a message to the LaMetric device.""" + await self.coordinator.lametric.notify( + notification=Notification( + icon_type=NotificationIconType.NONE, + priority=NotificationPriority.INFO, + model=Model(frames=[Simple(text=message)]), + ) + ) async def async_get_service( diff --git a/homeassistant/components/lametric/strings.json b/homeassistant/components/lametric/strings.json index d9c29af7bd97..5148378e437f 100644 --- a/homeassistant/components/lametric/strings.json +++ b/homeassistant/components/lametric/strings.json @@ -65,6 +65,11 @@ "name": "Dismiss current notification" } }, + "notify": { + "message": { + "name": "Message" + } + }, "number": { "brightness": { "name": "Brightness" @@ -93,6 +98,14 @@ } } }, + "exceptions": { + "communication_error": { + "message": "Error communicating with the LaMetric device" + }, + "invalid_response": { + "message": "Invalid response from the LaMetric device" + } + }, "selector": { "icon_type": { "options": { diff --git a/tests/components/lametric/snapshots/test_notify.ambr b/tests/components/lametric/snapshots/test_notify.ambr new file mode 100644 index 000000000000..e9ec419528c7 --- /dev/null +++ b/tests/components/lametric/snapshots/test_notify.ambr @@ -0,0 +1,52 @@ +# serializer version: 1 +# name: test_all_entities[notify][notify.frenck_s_lametric_message-entry] + EntityRegistryEntrySnapshot({ + 'aliases': list([ + None, + ]), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'notify', + 'entity_category': None, + 'entity_id': 'notify.frenck_s_lametric_message', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'object_id_base': 'Message', + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Message', + 'platform': 'lametric', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'message', + 'unique_id': 'SA110405124500W00BS9-message', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[notify][notify.frenck_s_lametric_message-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + : "Frenck's LaMetric Message", + : , + }), + 'context': , + 'entity_id': 'notify.frenck_s_lametric_message', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'unknown', + }) +# --- diff --git a/tests/components/lametric/test_notify.py b/tests/components/lametric/test_notify.py index d30a8c86543a..eb26ad0467c6 100644 --- a/tests/components/lametric/test_notify.py +++ b/tests/components/lametric/test_notify.py @@ -3,7 +3,9 @@ from unittest.mock import MagicMock from demetriek import ( + LaMetricConnectionError, LaMetricError, + Model, Notification, NotificationIconType, NotificationPriority, @@ -12,18 +14,34 @@ from demetriek import ( Simple, ) import pytest +from syrupy.assertion import SnapshotAssertion +from homeassistant.components.lametric.const import DOMAIN from homeassistant.components.notify import ( ATTR_DATA, ATTR_MESSAGE, DOMAIN as NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_UNAVAILABLE, + STATE_UNKNOWN, + Platform, ) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import entity_registry as er + +from tests.common import MockConfigEntry, snapshot_platform NOTIFY_SERVICE = "frenck_s_lametric" +ENTITY_ID = "notify.frenck_s_lametric_message" -pytestmark = pytest.mark.usefixtures("init_integration") +pytestmark = [ + pytest.mark.parametrize("init_integration", [Platform.NOTIFY], indirect=True), + pytest.mark.usefixtures("init_integration"), +] async def test_notification_defaults( @@ -122,3 +140,94 @@ async def test_notification_error( }, blocking=True, ) + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.freeze_time("2022-09-19 12:07:30") +async def test_send_message( + hass: HomeAssistant, + mock_lametric: MagicMock, +) -> None: + """Test sending a message through the LaMetric notify entity.""" + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MESSAGE: "The way to get started is to quit talking and begin doing", + }, + blocking=True, + ) + + mock_lametric.notify.assert_called_once_with( + notification=Notification( + icon_type=NotificationIconType.NONE, + priority=NotificationPriority.INFO, + model=Model( + frames=[ + Simple( + text="The way to get started is to quit talking and begin doing" + ) + ] + ), + ) + ) + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == "2022-09-19T12:07:30+00:00" + + +@pytest.mark.parametrize( + ("side_effect", "translation_key", "expected_state"), + [ + pytest.param( + LaMetricError, + "invalid_response", + STATE_UNKNOWN, + id="error", + ), + pytest.param( + LaMetricConnectionError, + "communication_error", + STATE_UNAVAILABLE, + id="connection_error", + ), + ], +) +async def test_send_message_error( + hass: HomeAssistant, + mock_lametric: MagicMock, + side_effect: type[LaMetricError], + translation_key: str, + expected_state: str, +) -> None: + """Test error handling of the LaMetric notify entity.""" + mock_lametric.notify.side_effect = side_effect + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + NOTIFY_DOMAIN, + SERVICE_SEND_MESSAGE, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_MESSAGE: "It's failure that gives you the proper perspective", + }, + blocking=True, + ) + + assert err.value.translation_domain == DOMAIN + assert err.value.translation_key == translation_key + + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == expected_state