22
33from __future__ import annotations
44
5+ from datetime import timedelta
6+ from functools import cached_property , partial
7+ import logging
8+ from typing import Any , final , override
9+
510import voluptuous as vol
611
712import homeassistant .components .persistent_notification as pn
8- from homeassistant .const import CONF_NAME , CONF_PLATFORM
13+ from homeassistant .config_entries import ConfigEntry
14+ from homeassistant .const import CONF_NAME , CONF_PLATFORM , STATE_UNAVAILABLE
915from homeassistant .core import HomeAssistant , ServiceCall
1016import homeassistant .helpers .config_validation as cv
17+ from homeassistant .helpers .entity import EntityDescription
18+ from homeassistant .helpers .entity_component import EntityComponent
19+ from homeassistant .helpers .restore_state import RestoreEntity
1120from homeassistant .helpers .template import Template
1221from homeassistant .helpers .typing import ConfigType
22+ from homeassistant .util import dt as dt_util
1323
1424from .const import ( # noqa: F401
1525 ATTR_DATA ,
1626 ATTR_MESSAGE ,
27+ ATTR_RECIPIENTS ,
1728 ATTR_TARGET ,
1829 ATTR_TITLE ,
1930 DOMAIN ,
2031 NOTIFY_SERVICE_SCHEMA ,
2132 SERVICE_NOTIFY ,
2233 SERVICE_PERSISTENT_NOTIFICATION ,
34+ SERVICE_SEND_MESSAGE ,
2335)
2436from .legacy import ( # noqa: F401
2537 BaseNotificationService ,
2941 check_templates_warn ,
3042)
3143
44+ # mypy: disallow-any-generics
45+
3246# Platform specific data
3347ATTR_TITLE_DEFAULT = "Home Assistant"
3448
49+ ENTITY_ID_FORMAT = DOMAIN + ".{}"
50+
51+ MIN_TIME_BETWEEN_SCANS = timedelta (seconds = 10 )
52+
53+ _LOGGER = logging .getLogger (__name__ )
54+
3555PLATFORM_SCHEMA = vol .Schema (
3656 {vol .Required (CONF_PLATFORM ): cv .string , vol .Optional (CONF_NAME ): cv .string },
3757 extra = vol .ALLOW_EXTRA ,
@@ -50,6 +70,13 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
5070 # legacy platforms to finish setting up.
5171 hass .async_create_task (setup , eager_start = True )
5272
73+ component = hass .data [DOMAIN ] = EntityComponent [NotifyEntity ](_LOGGER , DOMAIN , hass )
74+ component .async_register_entity_service (
75+ SERVICE_SEND_MESSAGE ,
76+ {vol .Required (ATTR_MESSAGE ): cv .string },
77+ "_async_send_message" ,
78+ )
79+
5380 async def persistent_notification (service : ServiceCall ) -> None :
5481 """Send notification via the built-in persistent_notify integration."""
5582 message : Template = service .data [ATTR_MESSAGE ]
@@ -79,3 +106,66 @@ async def persistent_notification(service: ServiceCall) -> None:
79106 )
80107
81108 return True
109+
110+
111+ class NotifyEntityDescription (EntityDescription , frozen_or_thawed = True ):
112+ """A class that describes button entities."""
113+
114+
115+ async def async_setup_entry (hass : HomeAssistant , entry : ConfigEntry ) -> bool :
116+ """Set up a config entry."""
117+ component : EntityComponent [NotifyEntity ] = hass .data [DOMAIN ]
118+ return await component .async_setup_entry (entry )
119+
120+
121+ async def async_unload_entry (hass : HomeAssistant , entry : ConfigEntry ) -> bool :
122+ """Unload a config entry."""
123+ component : EntityComponent [NotifyEntity ] = hass .data [DOMAIN ]
124+ return await component .async_unload_entry (entry )
125+
126+
127+ class NotifyEntity (RestoreEntity ):
128+ """Representation of a notify entity."""
129+
130+ entity_description : NotifyEntityDescription
131+ _attr_should_poll = False
132+ _attr_device_class : None
133+ _attr_state : None = None
134+ __last_notified_isoformat : str | None = None
135+
136+ @cached_property
137+ @final
138+ @override
139+ def state (self ) -> str | None :
140+ """Return the entity state."""
141+ return self .__last_notified_isoformat
142+
143+ def __set_state (self , state : str | None ) -> None :
144+ """Invalidate the cache of the cached property."""
145+ self .__dict__ .pop ("state" , None )
146+ self .__last_notified_isoformat = state
147+
148+ async def async_internal_added_to_hass (self ) -> None :
149+ """Call when the notify entity is added to hass."""
150+ await super ().async_internal_added_to_hass ()
151+ state = await self .async_get_last_state ()
152+ if state is not None and state .state not in (STATE_UNAVAILABLE , None ):
153+ self .__set_state (state .state )
154+
155+ @final
156+ async def _async_send_message (self , ** kwargs : Any ) -> None :
157+ """Send a notification message (from e.g., service call).
158+
159+ Should not be overridden, handle setting last notification timestamp.
160+ """
161+ self .__set_state (dt_util .utcnow ().isoformat ())
162+ self .async_write_ha_state ()
163+ await self .async_send_message (** kwargs )
164+
165+ def send_message (self , message : str ) -> None :
166+ """Send a message."""
167+ raise NotImplementedError
168+
169+ async def async_send_message (self , message : str ) -> None :
170+ """Send a message."""
171+ await self .hass .async_add_executor_job (partial (self .send_message , message ))
0 commit comments