Skip to content

Commit 3b2f73e

Browse files
committed
Properly handle homeassistant.update_entity action
Improve Sun2Entity._log_name.
1 parent a9c7561 commit 3b2f73e

File tree

3 files changed

+127
-70
lines changed

3 files changed

+127
-70
lines changed

custom_components/sun2/binary_sensor.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@
4343
class Sun2ElevationBinarySensor(Sun2EntityWithElvAdjs, BinarySensorEntity):
4444
"""Sun2 Elevation Binary Sensor."""
4545

46+
_supports_entity_update_action = True
47+
4648
_use_nxt_dir_chg: bool = False
4749

4850
def __init__(
@@ -76,6 +78,10 @@ async def _update(self, cur_dttm: datetime) -> None:
7678
self._attr_is_on = self._get_cur_state(cur_dttm)
7779
self._attr_icon = ICON_ABOVE if self._attr_is_on else ICON_BELOW
7880

81+
if self._update_scheduled:
82+
# homeassistant.update_entity was called. Leave next scheduled update as is.
83+
return
84+
7985
if nxt_chg := await self._get_nxt_chg():
8086
self._schedule_update(nxt_chg)
8187
nxt_chg = self._as_tz(nxt_chg)

custom_components/sun2/helpers.py

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,19 @@
1818
from astral.location import Location
1919
from 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
2126
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
2227
from 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

@@ -34,12 +39,16 @@
3439
except 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
3943
from homeassistant.helpers.dispatcher import async_dispatcher_connect
4044
from homeassistant.helpers.entity import Entity
4145
from 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+
)
4352
from homeassistant.util import dt as dt_util
4453
from 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:
278287
class 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

Comments
 (0)