1818from astral .location import Location
1919from astral .sun import adjust_to_horizon , adjust_to_obscuring_feature
2020
21+ from homeassistant .components .binary_sensor import (
22+ DOMAIN as BS_DOMAIN ,
23+ BinarySensorEntity ,
24+ )
25+ from homeassistant .components .sensor import DOMAIN as S_DOMAIN
2126from homeassistant .config_entries import SOURCE_IMPORT , ConfigEntry
2227from homeassistant .const import (
2328 CONF_ELEVATION ,
2429 CONF_LATITUDE ,
2530 CONF_LONGITUDE ,
2631 CONF_TIME_ZONE ,
2732)
28- from homeassistant .core import CALLBACK_TYPE , HomeAssistant , callback
33+ from homeassistant .core import CALLBACK_TYPE , Event , HomeAssistant , callback
2934
3035# Config moved from core to core_config in 2024.11
3136
3439except ImportError :
3540 from homeassistant .core import Config # type: ignore[no-redef]
3641
37- from homeassistant .helpers import device_registry as dr
38- from homeassistant .helpers .device_registry import DeviceEntryType , DeviceInfo
42+ from homeassistant .helpers import device_registry as dr , entity_registry as er
3943from homeassistant .helpers .dispatcher import async_dispatcher_connect
4044from homeassistant .helpers .entity import Entity
4145from homeassistant .helpers .entity_platform import AddEntitiesCallback
42- from homeassistant .helpers .event import async_call_later , async_track_point_in_utc_time
46+ from homeassistant .helpers .event import (
47+ async_call_later ,
48+ async_track_device_registry_updated_event ,
49+ async_track_entity_registry_updated_event ,
50+ async_track_point_in_utc_time ,
51+ )
4352from homeassistant .util import dt as dt_util
4453from homeassistant .util .hass_dict import HassKey
4554
@@ -244,10 +253,10 @@ def hours_to_hms(hours: Num | None) -> str | None:
244253 return None
245254
246255
247- def sun2_dev_info (hass : HomeAssistant , entry : ConfigEntry ) -> DeviceInfo :
256+ def sun2_dev_info (hass : HomeAssistant , entry : ConfigEntry ) -> dr . DeviceInfo :
248257 """Sun2 device (service) info."""
249- return DeviceInfo (
250- entry_type = DeviceEntryType .SERVICE ,
258+ return dr . DeviceInfo (
259+ entry_type = dr . DeviceEntryType .SERVICE ,
251260 identifiers = {(DOMAIN , entry .entry_id )},
252261 translation_key = "service" ,
253262 translation_placeholders = {"location" : entry .title },
@@ -278,7 +287,7 @@ class AstralData:
278287class Sun2EntityParams :
279288 """Sun2Entity parameters."""
280289
281- device_info : DeviceInfo
290+ device_info : dr . DeviceInfo
282291 astral_data : AstralData
283292 unique_id : str = ""
284293
@@ -291,6 +300,10 @@ class Sun2Entity(Entity, ABC):
291300 declare PARALLEL_UPDATES = 1!
292301 """
293302
303+ # Override in subclass as needed
304+ _supports_entity_update_action = False
305+ _reschedule_at_midnight = False
306+
294307 _unrecorded_attributes = frozenset (
295308 {
296309 ATTR_NEXT_CHANGE ,
@@ -302,6 +315,7 @@ class Sun2Entity(Entity, ABC):
302315 }
303316 )
304317 _attr_should_poll = False
318+
305319 _unsub_update : CALLBACK_TYPE | None = None
306320 _first_update = True
307321
@@ -318,13 +332,21 @@ def __init__(self, sun2_entity_params: Sun2EntityParams) -> None:
318332 @cached_property
319333 def _log_name (self ) -> str :
320334 """Return entity name for logging."""
335+ uid = cast (str , self .unique_id )
336+ ent_reg = er .async_get (self .hass )
337+ ent_domain = BS_DOMAIN if isinstance (self , BinarySensorEntity ) else S_DOMAIN
338+ ent_name = (
339+ (eid := ent_reg .async_get_entity_id (ent_domain , DOMAIN , uid ))
340+ and (ent_entry := ent_reg .async_get (eid ))
341+ and (ent_entry .name or ent_entry .original_name )
342+ ) or str (self .name )
321343 dev_reg = dr .async_get (self .hass )
322344 assert self .platform .config_entry
323345 cfg_entry_id = self .platform .config_entry .entry_id
324346 dev_entry = dev_reg .async_get_device (identifiers = {(DOMAIN , cfg_entry_id )})
325- if dev_entry and dev_entry .name :
326- return f"{ dev_entry . name } { self . name } "
327- return str ( self . name )
347+ if dev_entry and ( dev_name := dev_entry .name_by_user or dev_entry . name ) :
348+ return f"{ dev_name } { ent_name } "
349+ return ent_name
328350
329351 @cached_property
330352 def _loc (self ) -> Location :
@@ -346,6 +368,11 @@ def _west_obs_elv(self) -> ObsElv:
346368 """Return westerly observer elevation."""
347369 return self ._astral_data .obs_elvs .west
348370
371+ @property
372+ def _update_scheduled (self ) -> bool :
373+ """Return if an update is currently scheduled."""
374+ return bool (self ._unsub_update )
375+
349376 def _as_tz (self , dttm : datetime ) -> datetime :
350377 """Return datetime in location's time zone."""
351378 return dttm .astimezone (self ._tzi )
@@ -356,6 +383,15 @@ def _dttm_2_str(self, dttm: datetime) -> str:
356383
357384 async def async_update (self ) -> None :
358385 """Update state."""
386+ # If there is a scheduled update pending, then update must have been invoked by
387+ # homeassistant.update_entity action because scheduled updates are automatically
388+ # cleared before this method is called. If action is not supported by the
389+ # sensor, then simply ignore it. Warn the user so they know not to bother in the
390+ # future.
391+ if self ._update_scheduled and not self ._supports_entity_update_action :
392+ LOGGER .warning ("%s: Does not support homeassistant.update_entity action" )
393+ return
394+
359395 cur_dttm = dt_util .utcnow ()
360396 LOGGER .debug (
361397 "%s: +++++++++++++++++++++ first update: %s, update at: %s" ,
@@ -372,22 +408,48 @@ async def async_update(self) -> None:
372408 self ._first_update ,
373409 (dt_util .utcnow () - cur_dttm ).total_seconds (),
374410 )
411+
412+ if self ._reschedule_at_midnight :
413+ self ._schedule_update (next_midnight (self ._as_tz (cur_dttm )))
414+
375415 self ._first_update = False
376416
377417 async def async_added_to_hass (self ) -> None :
378418 """Run when entity about to be added to hass."""
379- self ._setup_fixed_updating ()
419+
420+ def reset_log_name (
421+ event : Event [dr .EventDeviceRegistryUpdatedData ]
422+ | Event [er .EventEntityRegistryUpdatedData ]
423+ | None = None ,
424+ ) -> None :
425+ """Clear _log_name cache."""
426+ with suppress (AttributeError ):
427+ del self ._log_name
428+
429+ reset_log_name ()
430+ assert self .device_entry
431+ assert self .entity_id
432+ self .async_on_remove (
433+ async_track_device_registry_updated_event (
434+ self .hass , self .device_entry .id , reset_log_name
435+ )
436+ )
437+ self .async_on_remove (
438+ async_track_entity_registry_updated_event (
439+ self .hass , self .entity_id , reset_log_name
440+ )
441+ )
380442
381443 def _schedule_update (self , dttm_or_delta : datetime | Num ) -> None :
382444 """Schedule an update."""
445+ assert not self ._unsub_update
383446
384447 @callback
385448 def async_schedule_update (now : datetime ) -> None :
386449 """Schedule entity update."""
387450 self ._unsub_update = None
388451 self .async_schedule_update_ha_state (True )
389452
390- self ._cancel_update ()
391453 if isinstance (dttm_or_delta , datetime ):
392454 self ._unsub_update = async_track_point_in_utc_time (
393455 self .hass , async_schedule_update , dttm_or_delta
@@ -413,12 +475,6 @@ def _update_setup(self, cur_dttm: datetime) -> None:
413475 async def _update (self , cur_dttm : datetime ) -> None :
414476 """Update state."""
415477
416- def _setup_fixed_updating (self ) -> None :
417- """Set up fixed updating.
418-
419- None by default. Override in subclass if needed.
420- """
421-
422478 async def update_astral_data (self , astral_data : AstralData ) -> None :
423479 """Update astral data.
424480
@@ -428,7 +484,6 @@ async def update_astral_data(self, astral_data: AstralData) -> None:
428484
429485 def _update_astral_data (self , astral_data : AstralData ) -> None :
430486 """Update astral data."""
431- self ._first_update = True
432487 self ._cancel_update ()
433488 self ._astral_data = astral_data
434489 with suppress (AttributeError ):
@@ -439,7 +494,7 @@ def _update_astral_data(self, astral_data: AstralData) -> None:
439494 del self ._east_obs_elv
440495 with suppress (AttributeError ):
441496 del self ._west_obs_elv
442- self ._setup_fixed_updating ()
497+ self ._first_update = True
443498
444499 def _dawn (
445500 self , dt : date , solar_depression : Num | str , * , local : bool = False
@@ -682,6 +737,7 @@ def _update_setup(self, cur_dttm: datetime) -> None:
682737
683738 Also initialize self._dt & self._rising based on that determination.
684739 """
740+ super ()._update_setup (cur_dttm )
685741
686742 # Note that solar midnight for a given date can happen early on that same
687743 # day (where the date is the same), or it can happen late on the previous
0 commit comments