Jewish Calendar add coordinator (#141456)

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
Tsvi Mostovicz
2025-08-11 23:22:13 +03:00
committed by GitHub
parent e394435d7c
commit 3b358df9e7
7 changed files with 227 additions and 164 deletions

View File

@@ -29,7 +29,8 @@ from .const import (
DEFAULT_LANGUAGE,
DOMAIN,
)
from .entity import JewishCalendarConfigEntry, JewishCalendarData
from .coordinator import JewishCalendarData, JewishCalendarUpdateCoordinator
from .entity import JewishCalendarConfigEntry
from .services import async_setup_services
_LOGGER = logging.getLogger(__name__)
@@ -69,7 +70,7 @@ async def async_setup_entry(
)
)
config_entry.runtime_data = JewishCalendarData(
data = JewishCalendarData(
language,
diaspora,
location,
@@ -77,8 +78,11 @@ async def async_setup_entry(
havdalah_offset,
)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
coordinator = JewishCalendarUpdateCoordinator(hass, config_entry, data)
await coordinator.async_config_entry_first_refresh()
config_entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
@@ -86,7 +90,13 @@ async def async_unload_entry(
hass: HomeAssistant, config_entry: JewishCalendarConfigEntry
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
if unload_ok := await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
):
coordinator = config_entry.runtime_data
if coordinator.event_unsub:
coordinator.event_unsub()
return unload_ok
async def async_migrate_entry(

View File

@@ -72,8 +72,7 @@ class JewishCalendarBinarySensor(JewishCalendarEntity, BinarySensorEntity):
@property
def is_on(self) -> bool:
"""Return true if sensor is on."""
zmanim = self.make_zmanim(dt.date.today())
return self.entity_description.is_on(zmanim)(dt_util.now())
return self.entity_description.is_on(self.coordinator.zmanim)(dt_util.now())
def _update_times(self, zmanim: Zmanim) -> list[dt.datetime | None]:
"""Return a list of times to update the sensor."""

View File

@@ -0,0 +1,116 @@
"""Data update coordinator for Jewish calendar."""
from dataclasses import dataclass
import datetime as dt
import logging
from hdate import HDateInfo, Location, Zmanim
from hdate.translator import Language, set_language
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers import event
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
import homeassistant.util.dt as dt_util
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarUpdateCoordinator]
@dataclass
class JewishCalendarData:
"""Jewish Calendar runtime dataclass."""
language: Language
diaspora: bool
location: Location
candle_lighting_offset: int
havdalah_offset: int
dateinfo: HDateInfo | None = None
zmanim: Zmanim | None = None
class JewishCalendarUpdateCoordinator(DataUpdateCoordinator[JewishCalendarData]):
"""Data update coordinator class for Jewish calendar."""
config_entry: JewishCalendarConfigEntry
event_unsub: CALLBACK_TYPE | None = None
def __init__(
self,
hass: HomeAssistant,
config_entry: JewishCalendarConfigEntry,
data: JewishCalendarData,
) -> None:
"""Initialize the coordinator."""
super().__init__(hass, _LOGGER, name=DOMAIN, config_entry=config_entry)
self.data = data
self._unsub_update: CALLBACK_TYPE | None = None
set_language(data.language)
async def _async_update_data(self) -> JewishCalendarData:
"""Return HDate and Zmanim for today."""
now = dt_util.now()
_LOGGER.debug("Now: %s Location: %r", now, self.data.location)
today = now.date()
self.data.dateinfo = HDateInfo(today, self.data.diaspora)
self.data.zmanim = self.make_zmanim(today)
self.async_schedule_future_update()
return self.data
@callback
def async_schedule_future_update(self) -> None:
"""Schedule the next update of the sensor for the upcoming midnight."""
# Cancel any existing update
if self._unsub_update:
self._unsub_update()
self._unsub_update = None
# Calculate the next midnight
next_midnight = dt_util.start_of_local_day() + dt.timedelta(days=1)
_LOGGER.debug("Scheduling next update at %s", next_midnight)
# Schedule update at next midnight
self._unsub_update = event.async_track_point_in_time(
self.hass, self._handle_midnight_update, next_midnight
)
@callback
def _handle_midnight_update(self, _now: dt.datetime) -> None:
"""Handle midnight update callback."""
self._unsub_update = None
self.async_set_updated_data(self.data)
async def async_shutdown(self) -> None:
"""Cancel any scheduled updates when the coordinator is shutting down."""
await super().async_shutdown()
if self._unsub_update:
self._unsub_update()
self._unsub_update = None
def make_zmanim(self, date: dt.date) -> Zmanim:
"""Create a Zmanim object."""
return Zmanim(
date=date,
location=self.data.location,
candle_lighting_offset=self.data.candle_lighting_offset,
havdalah_offset=self.data.havdalah_offset,
)
@property
def zmanim(self) -> Zmanim:
"""Return the current Zmanim."""
assert self.data.zmanim is not None, "Zmanim data not available"
return self.data.zmanim
@property
def dateinfo(self) -> HDateInfo:
"""Return the current HDateInfo."""
assert self.data.dateinfo is not None, "HDateInfo data not available"
return self.data.dateinfo

View File

@@ -24,5 +24,5 @@ async def async_get_config_entry_diagnostics(
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"data": async_redact_data(asdict(entry.runtime_data), TO_REDACT),
"data": async_redact_data(asdict(entry.runtime_data.data), TO_REDACT),
}

View File

@@ -1,48 +1,22 @@
"""Entity representing a Jewish Calendar sensor."""
from abc import abstractmethod
from dataclasses import dataclass
import datetime as dt
import logging
from hdate import HDateInfo, Location, Zmanim
from hdate.translator import Language, set_language
from hdate import Zmanim
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, callback
from homeassistant.helpers import event
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type JewishCalendarConfigEntry = ConfigEntry[JewishCalendarData]
from .coordinator import JewishCalendarConfigEntry, JewishCalendarUpdateCoordinator
@dataclass
class JewishCalendarDataResults:
"""Jewish Calendar results dataclass."""
dateinfo: HDateInfo
zmanim: Zmanim
@dataclass
class JewishCalendarData:
"""Jewish Calendar runtime dataclass."""
language: Language
diaspora: bool
location: Location
candle_lighting_offset: int
havdalah_offset: int
results: JewishCalendarDataResults | None = None
class JewishCalendarEntity(Entity):
class JewishCalendarEntity(CoordinatorEntity[JewishCalendarUpdateCoordinator]):
"""An HA implementation for Jewish Calendar entity."""
_attr_has_entity_name = True
@@ -55,23 +29,13 @@ class JewishCalendarEntity(Entity):
description: EntityDescription,
) -> None:
"""Initialize a Jewish Calendar entity."""
super().__init__(config_entry.runtime_data)
self.entity_description = description
self._attr_unique_id = f"{config_entry.entry_id}-{description.key}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
identifiers={(DOMAIN, config_entry.entry_id)},
)
self.data = config_entry.runtime_data
set_language(self.data.language)
def make_zmanim(self, date: dt.date) -> Zmanim:
"""Create a Zmanim object."""
return Zmanim(
date=date,
location=self.data.location,
candle_lighting_offset=self.data.candle_lighting_offset,
havdalah_offset=self.data.havdalah_offset,
)
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
@@ -92,10 +56,9 @@ class JewishCalendarEntity(Entity):
def _schedule_update(self) -> None:
"""Schedule the next update of the sensor."""
now = dt_util.now()
zmanim = self.make_zmanim(now.date())
update = dt_util.start_of_local_day() + dt.timedelta(days=1)
for update_time in self._update_times(zmanim):
for update_time in self._update_times(self.coordinator.zmanim):
if update_time is not None and now < update_time < update:
update = update_time
@@ -110,17 +73,4 @@ class JewishCalendarEntity(Entity):
"""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()
zmanim = self.make_zmanim(today)
dateinfo = HDateInfo(today, diaspora=self.data.diaspora)
self.data.results = JewishCalendarDataResults(dateinfo, zmanim)

View File

@@ -19,7 +19,7 @@ from homeassistant.components.sensor import (
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
import homeassistant.util.dt as dt_util
from .entity import JewishCalendarConfigEntry, JewishCalendarEntity
@@ -236,25 +236,18 @@ class JewishCalendarBaseSensor(JewishCalendarEntity, SensorEntity):
return []
return [self.entity_description.next_update_fn(zmanim)]
def get_dateinfo(self, now: dt.datetime | None = None) -> HDateInfo:
def get_dateinfo(self) -> 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"
if now is None:
now = dt_util.now()
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)
_LOGGER.debug("Today: %s, update: %s", today, update)
if self.entity_description.next_update_fn:
update = self.entity_description.next_update_fn(self.coordinator.zmanim)
_LOGGER.debug("Today: %s, update: %s", now.date(), update)
if update is not None and now >= update:
return self.data.results.dateinfo.next_day
return self.data.results.dateinfo
return self.coordinator.dateinfo.next_day
return self.coordinator.dateinfo
class JewishCalendarSensor(JewishCalendarBaseSensor):
@@ -271,7 +264,9 @@ class JewishCalendarSensor(JewishCalendarBaseSensor):
super().__init__(config_entry, description)
# Set the options for enumeration sensors
if self.entity_description.options_fn is not None:
self._attr_options = self.entity_description.options_fn(self.data.diaspora)
self._attr_options = self.entity_description.options_fn(
self.coordinator.data.diaspora
)
@property
def native_value(self) -> str | int | dt.datetime | None:
@@ -295,9 +290,8 @@ class JewishCalendarTimeSensor(JewishCalendarBaseSensor):
@property
def native_value(self) -> dt.datetime | None:
"""Return the state of the sensor."""
if self.data.results is 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.get_dateinfo(), self.make_zmanim)
return self.coordinator.zmanim.zmanim[self.entity_description.key].local
return self.entity_description.value_fn(
self.get_dateinfo(), self.coordinator.make_zmanim
)

View File

@@ -3,6 +3,15 @@
dict({
'data': dict({
'candle_lighting_offset': 40,
'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': False,
'nusach': 'sephardi',
}),
'diaspora': False,
'havdalah_offset': 0,
'language': 'en',
@@ -17,16 +26,6 @@
'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')",
}),
}),
'results': dict({
'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': False,
'nusach': 'sephardi',
}),
'zmanim': dict({
'candle_lighting_offset': 40,
'date': dict({
@@ -47,7 +46,6 @@
}),
}),
}),
}),
'entry_data': dict({
'diaspora': False,
'language': 'en',
@@ -59,6 +57,15 @@
dict({
'data': dict({
'candle_lighting_offset': 18,
'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': True,
'nusach': 'sephardi',
}),
'diaspora': True,
'havdalah_offset': 0,
'language': 'en',
@@ -73,16 +80,6 @@
'repr': "zoneinfo.ZoneInfo(key='America/New_York')",
}),
}),
'results': dict({
'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': True,
'nusach': 'sephardi',
}),
'zmanim': dict({
'candle_lighting_offset': 18,
'date': dict({
@@ -103,7 +100,6 @@
}),
}),
}),
}),
'entry_data': dict({
'diaspora': True,
'language': 'en',
@@ -115,6 +111,15 @@
dict({
'data': dict({
'candle_lighting_offset': 18,
'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': False,
'nusach': 'sephardi',
}),
'diaspora': False,
'havdalah_offset': 0,
'language': 'en',
@@ -129,16 +134,6 @@
'repr': "zoneinfo.ZoneInfo(key='US/Pacific')",
}),
}),
'results': dict({
'dateinfo': dict({
'date': dict({
'day': 21,
'month': 10,
'year': 5785,
}),
'diaspora': False,
'nusach': 'sephardi',
}),
'zmanim': dict({
'candle_lighting_offset': 18,
'date': dict({
@@ -159,7 +154,6 @@
}),
}),
}),
}),
'entry_data': dict({
'language': 'en',
}),