Skip to content

Commit 3307132

Browse files
tsvijoostlek
andauthored
Jewish calendar: appropriate polling for sensors (2/3) (home-assistant#144906)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
1 parent da255af commit 3307132

3 files changed

Lines changed: 98 additions & 122 deletions

File tree

homeassistant/components/jewish_calendar/entity.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@
1919
class JewishCalendarDataResults:
2020
"""Jewish Calendar results dataclass."""
2121

22-
daytime_date: HDateInfo
23-
after_shkia_date: HDateInfo
24-
after_tzais_date: HDateInfo
22+
dateinfo: HDateInfo
2523
zmanim: Zmanim
2624

2725

homeassistant/components/jewish_calendar/sensor.py

Lines changed: 94 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@
1616
SensorEntity,
1717
SensorEntityDescription,
1818
)
19-
from homeassistant.const import SUN_EVENT_SUNSET, EntityCategory
20-
from homeassistant.core import HomeAssistant
19+
from homeassistant.const import EntityCategory
20+
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
21+
from homeassistant.helpers import event
2122
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
22-
from homeassistant.helpers.sun import get_astral_event_date
2323
from homeassistant.util import dt as dt_util
2424

2525
from .entity import (
@@ -37,15 +37,19 @@ class JewishCalendarBaseSensorDescription(SensorEntityDescription):
3737
"""Base class describing Jewish Calendar sensor entities."""
3838

3939
value_fn: Callable | None
40+
next_update_fn: Callable[[Zmanim], dt.datetime | None] | None
4041

4142

4243
@dataclass(frozen=True, kw_only=True)
4344
class JewishCalendarSensorDescription(JewishCalendarBaseSensorDescription):
4445
"""Class describing Jewish Calendar sensor entities."""
4546

46-
value_fn: Callable[[JewishCalendarDataResults], str | int]
47-
attr_fn: Callable[[JewishCalendarDataResults], dict[str, str]] | None = None
47+
value_fn: Callable[[HDateInfo], str | int]
48+
attr_fn: Callable[[HDateInfo], dict[str, str]] | None = None
4849
options_fn: Callable[[bool], list[str]] | None = None
50+
next_update_fn: Callable[[Zmanim], dt.datetime | None] | None = (
51+
lambda zmanim: zmanim.shkia.local
52+
)
4953

5054

5155
@dataclass(frozen=True, kw_only=True)
@@ -55,56 +59,52 @@ class JewishCalendarTimestampSensorDescription(JewishCalendarBaseSensorDescripti
5559
value_fn: (
5660
Callable[[HDateInfo, Callable[[dt.date], Zmanim]], dt.datetime | None] | None
5761
) = None
62+
next_update_fn: Callable[[Zmanim], dt.datetime | None] | None = None
5863

5964

6065
INFO_SENSORS: tuple[JewishCalendarSensorDescription, ...] = (
6166
JewishCalendarSensorDescription(
6267
key="date",
6368
translation_key="hebrew_date",
64-
value_fn=lambda results: str(results.after_shkia_date.hdate),
65-
attr_fn=lambda results: {
66-
"hebrew_year": str(results.after_shkia_date.hdate.year),
67-
"hebrew_month_name": str(results.after_shkia_date.hdate.month),
68-
"hebrew_day": str(results.after_shkia_date.hdate.day),
69+
value_fn=lambda info: str(info.hdate),
70+
attr_fn=lambda info: {
71+
"hebrew_year": str(info.hdate.year),
72+
"hebrew_month_name": str(info.hdate.month),
73+
"hebrew_day": str(info.hdate.day),
6974
},
7075
),
7176
JewishCalendarSensorDescription(
7277
key="weekly_portion",
7378
translation_key="weekly_portion",
7479
device_class=SensorDeviceClass.ENUM,
7580
options_fn=lambda _: [str(p) for p in Parasha],
76-
value_fn=lambda results: results.after_tzais_date.upcoming_shabbat.parasha,
81+
value_fn=lambda info: info.upcoming_shabbat.parasha,
82+
next_update_fn=lambda zmanim: zmanim.havdalah,
7783
),
7884
JewishCalendarSensorDescription(
7985
key="holiday",
8086
translation_key="holiday",
8187
device_class=SensorDeviceClass.ENUM,
8288
options_fn=lambda diaspora: HolidayDatabase(diaspora).get_all_names(),
83-
value_fn=lambda results: ", ".join(
84-
str(holiday) for holiday in results.after_shkia_date.holidays
85-
),
86-
attr_fn=lambda results: {
87-
"id": ", ".join(
88-
holiday.name for holiday in results.after_shkia_date.holidays
89-
),
89+
value_fn=lambda info: ", ".join(str(holiday) for holiday in info.holidays),
90+
attr_fn=lambda info: {
91+
"id": ", ".join(holiday.name for holiday in info.holidays),
9092
"type": ", ".join(
91-
dict.fromkeys(
92-
_holiday.type.name for _holiday in results.after_shkia_date.holidays
93-
)
93+
dict.fromkeys(_holiday.type.name for _holiday in info.holidays)
9494
),
9595
},
9696
),
9797
JewishCalendarSensorDescription(
9898
key="omer_count",
9999
translation_key="omer_count",
100100
entity_registry_enabled_default=False,
101-
value_fn=lambda results: results.after_shkia_date.omer.total_days,
101+
value_fn=lambda info: info.omer.total_days,
102102
),
103103
JewishCalendarSensorDescription(
104104
key="daf_yomi",
105105
translation_key="daf_yomi",
106106
entity_registry_enabled_default=False,
107-
value_fn=lambda results: results.daytime_date.daf_yomi,
107+
value_fn=lambda info: info.daf_yomi,
108108
),
109109
)
110110

@@ -184,26 +184,30 @@ class JewishCalendarTimestampSensorDescription(JewishCalendarBaseSensorDescripti
184184
value_fn=lambda at_date, mz: mz(
185185
at_date.upcoming_shabbat.previous_day.gdate
186186
).candle_lighting,
187+
next_update_fn=lambda zmanim: zmanim.havdalah,
187188
),
188189
JewishCalendarTimestampSensorDescription(
189190
key="upcoming_shabbat_havdalah",
190191
translation_key="upcoming_shabbat_havdalah",
191192
entity_registry_enabled_default=False,
192193
value_fn=lambda at_date, mz: mz(at_date.upcoming_shabbat.gdate).havdalah,
194+
next_update_fn=lambda zmanim: zmanim.havdalah,
193195
),
194196
JewishCalendarTimestampSensorDescription(
195197
key="upcoming_candle_lighting",
196198
translation_key="upcoming_candle_lighting",
197199
value_fn=lambda at_date, mz: mz(
198200
at_date.upcoming_shabbat_or_yom_tov.first_day.previous_day.gdate
199201
).candle_lighting,
202+
next_update_fn=lambda zmanim: zmanim.havdalah,
200203
),
201204
JewishCalendarTimestampSensorDescription(
202205
key="upcoming_havdalah",
203206
translation_key="upcoming_havdalah",
204207
value_fn=lambda at_date, mz: mz(
205208
at_date.upcoming_shabbat_or_yom_tov.last_day.gdate
206209
).havdalah,
210+
next_update_fn=lambda zmanim: zmanim.havdalah,
207211
),
208212
)
209213

@@ -227,46 +231,79 @@ async def async_setup_entry(
227231
class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
228232
"""Base class for Jewish calendar sensors."""
229233

234+
_attr_should_poll = False
230235
_attr_entity_category = EntityCategory.DIAGNOSTIC
236+
_update_unsub: CALLBACK_TYPE | None = None
231237

232-
async def async_update(self) -> None:
233-
"""Update the state of the sensor."""
234-
now = dt_util.now()
235-
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
238+
entity_description: JewishCalendarBaseSensorDescription
236239

237-
today = now.date()
238-
event_date = get_astral_event_date(self.hass, SUN_EVENT_SUNSET, today)
240+
async def async_added_to_hass(self) -> None:
241+
"""Call when entity is added to hass."""
242+
await super().async_added_to_hass()
243+
self._schedule_update()
244+
245+
async def async_will_remove_from_hass(self) -> None:
246+
"""Run when entity will be removed from hass."""
247+
if self._update_unsub:
248+
self._update_unsub()
249+
self._update_unsub = None
250+
return await super().async_will_remove_from_hass()
239251

240-
if event_date is None:
241-
_LOGGER.error("Can't get sunset event date for %s", today)
242-
return
252+
def _schedule_update(self) -> None:
253+
"""Schedule the next update of the sensor."""
254+
now = dt_util.now()
255+
zmanim = self.make_zmanim(now.date())
256+
update = None
257+
if self.entity_description.next_update_fn:
258+
update = self.entity_description.next_update_fn(zmanim)
259+
next_midnight = dt_util.start_of_local_day() + dt.timedelta(days=1)
260+
if update is None or now > update:
261+
update = next_midnight
262+
if self._update_unsub:
263+
self._update_unsub()
264+
self._update_unsub = event.async_track_point_in_time(
265+
self.hass, self._update_data, update
266+
)
243267

244-
sunset = dt_util.as_local(event_date)
268+
@callback
269+
def _update_data(self, now: dt.datetime | None = None) -> None:
270+
"""Update the sensor data."""
271+
self._update_unsub = None
272+
self._schedule_update()
273+
self.create_results(now)
274+
self.async_write_ha_state()
245275

246-
_LOGGER.debug("Now: %s Sunset: %s", now, sunset)
276+
def create_results(self, now: dt.datetime | None = None) -> None:
277+
"""Create the results for the sensor."""
278+
if now is None:
279+
now = dt_util.now()
247280

248-
daytime_date = HDateInfo(today, diaspora=self.data.diaspora)
281+
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
249282

250-
# The Jewish day starts after darkness (called "tzais") and finishes at
251-
# sunset ("shkia"). The time in between is a gray area
252-
# (aka "Bein Hashmashot" # codespell:ignore
253-
# - literally: "in between the sun and the moon").
283+
today = now.date()
284+
zmanim = self.make_zmanim(today)
285+
dateinfo = HDateInfo(today, diaspora=self.data.diaspora)
286+
self.data.results = JewishCalendarDataResults(dateinfo, zmanim)
254287

255-
# For some sensors, it is more interesting to consider the date to be
256-
# tomorrow based on sunset ("shkia"), for others based on "tzais".
257-
# Hence the following variables.
258-
after_tzais_date = after_shkia_date = daytime_date
259-
today_times = self.make_zmanim(today)
288+
def get_dateinfo(self, now: dt.datetime | None = None) -> HDateInfo:
289+
"""Get the next date info."""
290+
if self.data.results is None:
291+
self.create_results()
292+
assert self.data.results is not None, "Results should be available"
260293

261-
if now > sunset:
262-
after_shkia_date = daytime_date.next_day
294+
if now is None:
295+
now = dt_util.now()
263296

264-
if today_times.havdalah and now > today_times.havdalah:
265-
after_tzais_date = daytime_date.next_day
297+
today = now.date()
298+
zmanim = self.make_zmanim(today)
299+
update = None
300+
if self.entity_description.next_update_fn:
301+
update = self.entity_description.next_update_fn(zmanim)
266302

267-
self.data.results = JewishCalendarDataResults(
268-
daytime_date, after_shkia_date, after_tzais_date, today_times
269-
)
303+
_LOGGER.debug("Today: %s, update: %s", today, update)
304+
if update is not None and now >= update:
305+
return self.data.results.dateinfo.next_day
306+
return self.data.results.dateinfo
270307

271308

272309
class JewishCalendarSensor(JewishCalendarBaseSensor):
@@ -288,18 +325,14 @@ def __init__(
288325
@property
289326
def native_value(self) -> str | int | dt.datetime | None:
290327
"""Return the state of the sensor."""
291-
if self.data.results is None:
292-
return None
293-
return self.entity_description.value_fn(self.data.results)
328+
return self.entity_description.value_fn(self.get_dateinfo())
294329

295330
@property
296331
def extra_state_attributes(self) -> dict[str, str]:
297332
"""Return the state attributes."""
298-
if self.data.results is None:
333+
if self.entity_description.attr_fn is None:
299334
return {}
300-
if self.entity_description.attr_fn is not None:
301-
return self.entity_description.attr_fn(self.data.results)
302-
return {}
335+
return self.entity_description.attr_fn(self.get_dateinfo())
303336

304337

305338
class JewishCalendarTimeSensor(JewishCalendarBaseSensor):
@@ -312,9 +345,8 @@ class JewishCalendarTimeSensor(JewishCalendarBaseSensor):
312345
def native_value(self) -> dt.datetime | None:
313346
"""Return the state of the sensor."""
314347
if self.data.results is None:
315-
return None
348+
self.create_results()
349+
assert self.data.results is not None, "Results should be available"
316350
if self.entity_description.value_fn is None:
317351
return self.data.results.zmanim.zmanim[self.entity_description.key].local
318-
return self.entity_description.value_fn(
319-
self.data.results.after_tzais_date, self.make_zmanim
320-
)
352+
return self.entity_description.value_fn(self.get_dateinfo(), self.make_zmanim)

tests/components/jewish_calendar/snapshots/test_diagnostics.ambr

Lines changed: 3 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,7 @@
1818
}),
1919
}),
2020
'results': dict({
21-
'after_shkia_date': dict({
22-
'date': dict({
23-
'day': 21,
24-
'month': 10,
25-
'year': 5785,
26-
}),
27-
'diaspora': False,
28-
'nusach': 'sephardi',
29-
}),
30-
'after_tzais_date': dict({
31-
'date': dict({
32-
'day': 21,
33-
'month': 10,
34-
'year': 5785,
35-
}),
36-
'diaspora': False,
37-
'nusach': 'sephardi',
38-
}),
39-
'daytime_date': dict({
21+
'dateinfo': dict({
4022
'date': dict({
4123
'day': 21,
4224
'month': 10,
@@ -92,25 +74,7 @@
9274
}),
9375
}),
9476
'results': dict({
95-
'after_shkia_date': dict({
96-
'date': dict({
97-
'day': 21,
98-
'month': 10,
99-
'year': 5785,
100-
}),
101-
'diaspora': True,
102-
'nusach': 'sephardi',
103-
}),
104-
'after_tzais_date': dict({
105-
'date': dict({
106-
'day': 21,
107-
'month': 10,
108-
'year': 5785,
109-
}),
110-
'diaspora': True,
111-
'nusach': 'sephardi',
112-
}),
113-
'daytime_date': dict({
77+
'dateinfo': dict({
11478
'date': dict({
11579
'day': 21,
11680
'month': 10,
@@ -166,25 +130,7 @@
166130
}),
167131
}),
168132
'results': dict({
169-
'after_shkia_date': dict({
170-
'date': dict({
171-
'day': 21,
172-
'month': 10,
173-
'year': 5785,
174-
}),
175-
'diaspora': False,
176-
'nusach': 'sephardi',
177-
}),
178-
'after_tzais_date': dict({
179-
'date': dict({
180-
'day': 21,
181-
'month': 10,
182-
'year': 5785,
183-
}),
184-
'diaspora': False,
185-
'nusach': 'sephardi',
186-
}),
187-
'daytime_date': dict({
133+
'dateinfo': dict({
188134
'date': dict({
189135
'day': 21,
190136
'month': 10,

0 commit comments

Comments
 (0)