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:
"""Jewish Calendar results dataclass."""
daytime_date: HDateInfo
after_shkia_date: HDateInfo
after_tzais_date: HDateInfo
dateinfo: HDateInfo
zmanim: Zmanim

View File

@ -16,10 +16,10 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.const import SUN_EVENT_SUNSET, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.const import EntityCategory
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import event
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 .entity import (
@ -37,15 +37,19 @@ class JewishCalendarBaseSensorDescription(SensorEntityDescription):
"""Base class describing Jewish Calendar sensor entities."""
value_fn: Callable | None
next_update_fn: Callable[[Zmanim], dt.datetime | None] | None
@dataclass(frozen=True, kw_only=True)
class JewishCalendarSensorDescription(JewishCalendarBaseSensorDescription):
"""Class describing Jewish Calendar sensor entities."""
value_fn: Callable[[JewishCalendarDataResults], str | int]
attr_fn: Callable[[JewishCalendarDataResults], dict[str, str]] | None = None
value_fn: Callable[[HDateInfo], str | int]
attr_fn: Callable[[HDateInfo], dict[str, 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)
@ -55,17 +59,18 @@ class JewishCalendarTimestampSensorDescription(JewishCalendarBaseSensorDescripti
value_fn: (
Callable[[HDateInfo, Callable[[dt.date], Zmanim]], dt.datetime | None] | None
) = None
next_update_fn: Callable[[Zmanim], dt.datetime | None] | None = None
INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
JewishCalendarSensorDescription(
key="date",
translation_key="hebrew_date",
value_fn=lambda results: str(results.after_shkia_date.hdate),
attr_fn=lambda results: {
"hebrew_year": str(results.after_shkia_date.hdate.year),
"hebrew_month_name": str(results.after_shkia_date.hdate.month),
"hebrew_day": str(results.after_shkia_date.hdate.day),
value_fn=lambda info: str(info.hdate),
attr_fn=lambda info: {
"hebrew_year": str(info.hdate.year),
"hebrew_month_name": str(info.hdate.month),
"hebrew_day": str(info.hdate.day),
},
),
JewishCalendarSensorDescription(
@ -73,24 +78,19 @@ INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
translation_key="weekly_portion",
device_class=SensorDeviceClass.ENUM,
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(
key="holiday",
translation_key="holiday",
device_class=SensorDeviceClass.ENUM,
options_fn=lambda diaspora: HolidayDatabase(diaspora).get_all_names(),
value_fn=lambda results: ", ".join(
str(holiday) for holiday in results.after_shkia_date.holidays
),
attr_fn=lambda results: {
"id": ", ".join(
holiday.name for holiday in results.after_shkia_date.holidays
),
value_fn=lambda info: ", ".join(str(holiday) for holiday in info.holidays),
attr_fn=lambda info: {
"id": ", ".join(holiday.name for holiday in info.holidays),
"type": ", ".join(
dict.fromkeys(
_holiday.type.name for _holiday in results.after_shkia_date.holidays
)
dict.fromkeys(_holiday.type.name for _holiday in info.holidays)
),
},
),
@ -98,13 +98,13 @@ INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
key="omer_count",
translation_key="omer_count",
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(
key="daf_yomi",
translation_key="daf_yomi",
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(
at_date.upcoming_shabbat.previous_day.gdate
).candle_lighting,
next_update_fn=lambda zmanim: zmanim.havdalah,
),
JewishCalendarTimestampSensorDescription(
key="upcoming_shabbat_havdalah",
translation_key="upcoming_shabbat_havdalah",
entity_registry_enabled_default=False,
value_fn=lambda at_date, mz: mz(at_date.upcoming_shabbat.gdate).havdalah,
next_update_fn=lambda zmanim: zmanim.havdalah,
),
JewishCalendarTimestampSensorDescription(
key="upcoming_candle_lighting",
@ -197,6 +199,7 @@ TIME_SENSORS: tuple[JewishCalendarTimestampSensorDescription, ...] = (
value_fn=lambda at_date, mz: mz(
at_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate
).candle_lighting,
next_update_fn=lambda zmanim: zmanim.havdalah,
),
JewishCalendarTimestampSensorDescription(
key="upcoming_havdalah",
@ -204,6 +207,7 @@ TIME_SENSORS: tuple[JewishCalendarTimestampSensorDescription, ...] = (
value_fn=lambda at_date, mz: mz(
at_date.upcoming_shabbat_or_yom_tov.last_day.gdate
).havdalah,
next_update_fn=lambda zmanim: zmanim.havdalah,
),
)
@ -227,46 +231,79 @@ async def async_setup_entry(
class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
"""Base class for Jewish calendar sensors."""
_attr_should_poll = False
_attr_entity_category = EntityCategory.DIAGNOSTIC
_update_unsub: CALLBACK_TYPE | None = None
async def async_update(self) -> None:
"""Update the state of the sensor."""
entity_description: JewishCalendarBaseSensorDescription
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()
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)
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:
_LOGGER.error("Can't get sunset event date for %s", today)
return
def get_dateinfo(self, now: dt.datetime | None = None) -> HDateInfo:
"""Get the next date info."""
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)
# The Jewish day starts after darkness (called "tzais") and finishes at
# sunset ("shkia"). The time in between is a gray area
# (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
)
_LOGGER.debug("Today: %s, update: %s", today, update)
if update is not None and now >= update:
return self.data.results.dateinfo.next_day
return self.data.results.dateinfo
class JewishCalendarSensor(JewishCalendarBaseSensor):
@ -288,18 +325,14 @@ class JewishCalendarSensor(JewishCalendarBaseSensor):
@property
def native_value(self) -> str | int | dt.datetime | None:
"""Return the state of the sensor."""
if self.data.results is None:
return None
return self.entity_description.value_fn(self.data.results)
return self.entity_description.value_fn(self.get_dateinfo())
@property
def extra_state_attributes(self) -> dict[str, str]:
"""Return the state attributes."""
if self.data.results is None:
if self.entity_description.attr_fn is None:
return {}
if self.entity_description.attr_fn is not None:
return self.entity_description.attr_fn(self.data.results)
return {}
return self.entity_description.attr_fn(self.get_dateinfo())
class JewishCalendarTimeSensor(JewishCalendarBaseSensor):
@ -312,9 +345,8 @@ class JewishCalendarTimeSensor(JewishCalendarBaseSensor):
def native_value(self) -> dt.datetime | None:
"""Return the state of the sensor."""
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:
return self.data.results.zmanim.zmanim[self.entity_description.key].local
return self.entity_description.value_fn(
self.data.results.after_tzais_date, self.make_zmanim
)
return self.entity_description.value_fn(self.get_dateinfo(), self.make_zmanim)

View File

@ -18,25 +18,7 @@
}),
}),
'results': dict({
'after_shkia_date': 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({
'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
@ -92,25 +74,7 @@
}),
}),
'results': dict({
'after_shkia_date': 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({
'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
@ -166,25 +130,7 @@
}),
}),
'results': dict({
'after_shkia_date': 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({
'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,