Jewish calendar: appropriate polling for sensors (2/3) (#144906)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Tsvi Mostovicz 2025-07-09 23:50:09 +03:00 committed by GitHub
parent da255af8de
commit 3307132441
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 100 additions and 124 deletions

View File

@ -19,9 +19,7 @@ type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarData]
class JewishCalendarDataResults: class JewishCalendarDataResults:
"""Jewish Calendar results dataclass.""" """Jewish Calendar results dataclass."""
daytime_date: HDateInfo dateinfo: HDateInfo
after_shkia_date: HDateInfo
after_tzais_date: HDateInfo
zmanim: Zmanim zmanim: Zmanim

View File

@ -16,10 +16,10 @@ from homeassistant.components.sensor import (
SensorEntity, SensorEntity,
SensorEntityDescription, SensorEntityDescription,
) )
from homeassistant.const import SUN_EVENT_SUNSET, EntityCategory from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import event
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.sun import get_astral_event_date
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
from .entity import ( from .entity import (
@ -37,15 +37,19 @@ class JewishCalendarBaseSensorDescription(SensorEntityDescription):
"""Base class describing Jewish Calendar sensor entities.""" """Base class describing Jewish Calendar sensor entities."""
value_fn: Callable | None value_fn: Callable | None
next_update_fn: Callable[[Zmanim], dt.datetime | None] | None
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
class JewishCalendarSensorDescription(JewishCalendarBaseSensorDescription): class JewishCalendarSensorDescription(JewishCalendarBaseSensorDescription):
"""Class describing Jewish Calendar sensor entities.""" """Class describing Jewish Calendar sensor entities."""
value_fn: Callable[[JewishCalendarDataResults], str | int] value_fn: Callable[[HDateInfo], str | int]
attr_fn: Callable[[JewishCalendarDataResults], dict[str, str]] | None = None attr_fn: Callable[[HDateInfo], dict[str, str]] | None = None
options_fn: Callable[[bool], list[str]] | None = None options_fn: Callable[[bool], list[str]] | None = None
next_update_fn: Callable[[Zmanim], dt.datetime | None] | None = (
lambda zmanim: zmanim.shkia.local
)
@dataclass(frozen=True, kw_only=True) @dataclass(frozen=True, kw_only=True)
@ -55,17 +59,18 @@ class JewishCalendarTimestampSensorDescription(JewishCalendarBaseSensorDescripti
value_fn: ( value_fn: (
Callable[[HDateInfo, Callable[[dt.date], Zmanim]], dt.datetime | None] | None Callable[[HDateInfo, Callable[[dt.date], Zmanim]], dt.datetime | None] | None
) = None ) = None
next_update_fn: Callable[[Zmanim], dt.datetime | None] | None = None
INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = ( INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
JewishCalendarSensorDescription( JewishCalendarSensorDescription(
key="date", key="date",
translation_key="hebrew_date", translation_key="hebrew_date",
value_fn=lambda results: str(results.after_shkia_date.hdate), value_fn=lambda info: str(info.hdate),
attr_fn=lambda results: { attr_fn=lambda info: {
"hebrew_year": str(results.after_shkia_date.hdate.year), "hebrew_year": str(info.hdate.year),
"hebrew_month_name": str(results.after_shkia_date.hdate.month), "hebrew_month_name": str(info.hdate.month),
"hebrew_day": str(results.after_shkia_date.hdate.day), "hebrew_day": str(info.hdate.day),
}, },
), ),
JewishCalendarSensorDescription( JewishCalendarSensorDescription(
@ -73,24 +78,19 @@ INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
translation_key="weekly_portion", translation_key="weekly_portion",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options_fn=lambda _: [str(p) for p in Parasha], options_fn=lambda _: [str(p) for p in Parasha],
value_fn=lambda results: results.after_tzais_date.upcoming_shabbat.parasha, value_fn=lambda info: info.upcoming_shabbat.parasha,
next_update_fn=lambda zmanim: zmanim.havdalah,
), ),
JewishCalendarSensorDescription( JewishCalendarSensorDescription(
key="holiday", key="holiday",
translation_key="holiday", translation_key="holiday",
device_class=SensorDeviceClass.ENUM, device_class=SensorDeviceClass.ENUM,
options_fn=lambda diaspora: HolidayDatabase(diaspora).get_all_names(), options_fn=lambda diaspora: HolidayDatabase(diaspora).get_all_names(),
value_fn=lambda results: ", ".join( value_fn=lambda info: ", ".join(str(holiday) for holiday in info.holidays),
str(holiday) for holiday in results.after_shkia_date.holidays attr_fn=lambda info: {
), "id": ", ".join(holiday.name for holiday in info.holidays),
attr_fn=lambda results: {
"id": ", ".join(
holiday.name for holiday in results.after_shkia_date.holidays
),
"type": ", ".join( "type": ", ".join(
dict.fromkeys( dict.fromkeys(_holiday.type.name for _holiday in info.holidays)
_holiday.type.name for _holiday in results.after_shkia_date.holidays
)
), ),
}, },
), ),
@ -98,13 +98,13 @@ INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
key="omer_count", key="omer_count",
translation_key="omer_count", translation_key="omer_count",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda results: results.after_shkia_date.omer.total_days, value_fn=lambda info: info.omer.total_days,
), ),
JewishCalendarSensorDescription( JewishCalendarSensorDescription(
key="daf_yomi", key="daf_yomi",
translation_key="daf_yomi", translation_key="daf_yomi",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda results: results.daytime_date.daf_yomi, value_fn=lambda info: info.daf_yomi,
), ),
) )
@ -184,12 +184,14 @@ TIME_SENSORS: tuple[JewishCalendarTimestampSensorDescription, ...] = (
value_fn=lambda at_date, mz: mz( value_fn=lambda at_date, mz: mz(
at_date.upcoming_shabbat.previous_day.gdate at_date.upcoming_shabbat.previous_day.gdate
).candle_lighting, ).candle_lighting,
next_update_fn=lambda zmanim: zmanim.havdalah,
), ),
JewishCalendarTimestampSensorDescription( JewishCalendarTimestampSensorDescription(
key="upcoming_shabbat_havdalah", key="upcoming_shabbat_havdalah",
translation_key="upcoming_shabbat_havdalah", translation_key="upcoming_shabbat_havdalah",
entity_registry_enabled_default=False, entity_registry_enabled_default=False,
value_fn=lambda at_date, mz: mz(at_date.upcoming_shabbat.gdate).havdalah, value_fn=lambda at_date, mz: mz(at_date.upcoming_shabbat.gdate).havdalah,
next_update_fn=lambda zmanim: zmanim.havdalah,
), ),
JewishCalendarTimestampSensorDescription( JewishCalendarTimestampSensorDescription(
key="upcoming_candle_lighting", key="upcoming_candle_lighting",
@ -197,6 +199,7 @@ TIME_SENSORS: tuple[JewishCalendarTimestampSensorDescription, ...] = (
value_fn=lambda at_date, mz: mz( value_fn=lambda at_date, mz: mz(
at_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate at_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate
).candle_lighting, ).candle_lighting,
next_update_fn=lambda zmanim: zmanim.havdalah,
), ),
JewishCalendarTimestampSensorDescription( JewishCalendarTimestampSensorDescription(
key="upcoming_havdalah", key="upcoming_havdalah",
@ -204,6 +207,7 @@ TIME_SENSORS: tuple[JewishCalendarTimestampSensorDescription, ...] = (
value_fn=lambda at_date, mz: mz( value_fn=lambda at_date, mz: mz(
at_date.upcoming_shabbat_or_yom_tov.last_day.gdate at_date.upcoming_shabbat_or_yom_tov.last_day.gdate
).havdalah, ).havdalah,
next_update_fn=lambda zmanim: zmanim.havdalah,
), ),
) )
@ -227,46 +231,79 @@ async def async_setup_entry(
class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity): class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
"""Base class for Jewish calendar sensors.""" """Base class for Jewish calendar sensors."""
_attr_should_poll = False
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
_update_unsub: CALLBACK_TYPE | None = None
async def async_update(self) -> None: entity_description: JewishCalendarBaseSensorDescription
"""Update the state of the sensor."""
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
await super().async_added_to_hass()
self._schedule_update()
async def async_will_remove_from_hass(self) -> None:
"""Run when entity will be removed from hass."""
if self._update_unsub:
self._update_unsub()
self._update_unsub = None
return await super().async_will_remove_from_hass()
def _schedule_update(self) -> None:
"""Schedule the next update of the sensor."""
now = dt_util.now() now = dt_util.now()
zmanim = self.make_zmanim(now.date())
update = None
if self.entity_description.next_update_fn:
update = self.entity_description.next_update_fn(zmanim)
next_midnight = dt_util.start_of_local_day() + dt.timedelta(days=1)
if update is None or now > update:
update = next_midnight
if self._update_unsub:
self._update_unsub()
self._update_unsub = event.async_track_point_in_time(
self.hass, self._update_data, update
)
@callback
def _update_data(self, now: dt.datetime | None = None) -> None:
"""Update the sensor data."""
self._update_unsub = None
self._schedule_update()
self.create_results(now)
self.async_write_ha_state()
def create_results(self, now: dt.datetime | None = None) -> None:
"""Create the results for the sensor."""
if now is None:
now = dt_util.now()
_LOGGER.debug("Now: %s Location: %r", now, self.data.location) _LOGGER.debug("Now: %s Location: %r", now, self.data.location)
today = now.date() today = now.date()
event_date = get_astral_event_date(self.hass, SUN_EVENT_SUNSET, today) zmanim = self.make_zmanim(today)
dateinfo = HDateInfo(today, diaspora=self.data.diaspora)
self.data.results = JewishCalendarDataResults(dateinfo, zmanim)
if event_date is None: def get_dateinfo(self, now: dt.datetime | None = None) -> HDateInfo:
_LOGGER.error("Can't get sunset event date for %s", today) """Get the next date info."""
return if self.data.results is None:
self.create_results()
assert self.data.results is not None, "Results should be available"
sunset = dt_util.as_local(event_date) if now is None:
now = dt_util.now()
_LOGGER.debug("Now: %s Sunset: %s", now, sunset) today = now.date()
zmanim = self.make_zmanim(today)
update = None
if self.entity_description.next_update_fn:
update = self.entity_description.next_update_fn(zmanim)
daytime_date = HDateInfo(today, diaspora=self.data.diaspora) _LOGGER.debug("Today: %s, update: %s", today, update)
if update is not None and now >= update:
# The Jewish day starts after darkness (called "tzais") and finishes at return self.data.results.dateinfo.next_day
# sunset ("shkia"). The time in between is a gray area return self.data.results.dateinfo
# (aka "Bein Hashmashot" # codespell:ignore
# - literally: "in between the sun and the moon").
# For some sensors, it is more interesting to consider the date to be
# tomorrow based on sunset ("shkia"), for others based on "tzais".
# Hence the following variables.
after_tzais_date = after_shkia_date = daytime_date
today_times = self.make_zmanim(today)
if now > sunset:
after_shkia_date = daytime_date.next_day
if today_times.havdalah and now > today_times.havdalah:
after_tzais_date = daytime_date.next_day
self.data.results = JewishCalendarDataResults(
daytime_date, after_shkia_date, after_tzais_date, today_times
)
class JewishCalendarSensor(JewishCalendarBaseSensor): class JewishCalendarSensor(JewishCalendarBaseSensor):
@ -288,18 +325,14 @@ class JewishCalendarSensor(JewishCalendarBaseSensor):
@property @property
def native_value(self) -> str | int | dt.datetime | None: def native_value(self) -> str | int | dt.datetime | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
if self.data.results is None: return self.entity_description.value_fn(self.get_dateinfo())
return None
return self.entity_description.value_fn(self.data.results)
@property @property
def extra_state_attributes(self) -> dict[str, str]: def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes.""" """Return the state attributes."""
if self.data.results is None: if self.entity_description.attr_fn is None:
return {} return {}
if self.entity_description.attr_fn is not None: return self.entity_description.attr_fn(self.get_dateinfo())
return self.entity_description.attr_fn(self.data.results)
return {}
class JewishCalendarTimeSensor(JewishCalendarBaseSensor): class JewishCalendarTimeSensor(JewishCalendarBaseSensor):
@ -312,9 +345,8 @@ class JewishCalendarTimeSensor(JewishCalendarBaseSensor):
def native_value(self) -> dt.datetime | None: def native_value(self) -> dt.datetime | None:
"""Return the state of the sensor.""" """Return the state of the sensor."""
if self.data.results is None: if self.data.results is None:
return None self.create_results()
assert self.data.results is not None, "Results should be available"
if self.entity_description.value_fn is None: if self.entity_description.value_fn is None:
return self.data.results.zmanim.zmanim[self.entity_description.key].local return self.data.results.zmanim.zmanim[self.entity_description.key].local
return self.entity_description.value_fn( return self.entity_description.value_fn(self.get_dateinfo(), self.make_zmanim)
self.data.results.after_tzais_date, self.make_zmanim
)

View File

@ -18,25 +18,7 @@
}), }),
}), }),
'results': dict({ 'results': dict({
'after_shkia_date': dict({ 'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': False,
'nusach': 'sephardi',
}),
'after_tzais_date': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': False,
'nusach': 'sephardi',
}),
'daytime_date': dict({
'date': dict({ 'date': dict({
'day': 21, 'day': 21,
'month': 10, 'month': 10,
@ -92,25 +74,7 @@
}), }),
}), }),
'results': dict({ 'results': dict({
'after_shkia_date': dict({ 'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': True,
'nusach': 'sephardi',
}),
'after_tzais_date': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': True,
'nusach': 'sephardi',
}),
'daytime_date': dict({
'date': dict({ 'date': dict({
'day': 21, 'day': 21,
'month': 10, 'month': 10,
@ -166,25 +130,7 @@
}), }),
}), }),
'results': dict({ 'results': dict({
'after_shkia_date': dict({ 'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': False,
'nusach': 'sephardi',
}),
'after_tzais_date': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': False,
'nusach': 'sephardi',
}),
'daytime_date': dict({
'date': dict({ 'date': dict({
'day': 21, 'day': 21,
'month': 10, 'month': 10,