mirror of
https://github.com/home-assistant/core.git
synced 2025-09-05 12:51:37 +02:00
Jewish Calendar add coordinator (#141456)
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
@@ -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(
|
||||
|
@@ -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."""
|
||||
|
116
homeassistant/components/jewish_calendar/coordinator.py
Normal file
116
homeassistant/components/jewish_calendar/coordinator.py
Normal 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
|
@@ -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),
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -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)
|
||||
now = dt_util.now()
|
||||
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
|
||||
)
|
||||
|
@@ -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,33 +26,22 @@
|
||||
'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({
|
||||
'__type': "<class 'freezegun.api.FakeDate'>",
|
||||
'isoformat': '2025-05-19',
|
||||
}),
|
||||
'zmanim': dict({
|
||||
'candle_lighting_offset': 40,
|
||||
'date': dict({
|
||||
'__type': "<class 'freezegun.api.FakeDate'>",
|
||||
'isoformat': '2025-05-19',
|
||||
}),
|
||||
'havdalah_offset': 0,
|
||||
'location': dict({
|
||||
'altitude': '**REDACTED**',
|
||||
'diaspora': False,
|
||||
'latitude': '**REDACTED**',
|
||||
'longitude': '**REDACTED**',
|
||||
'name': 'test home',
|
||||
'timezone': dict({
|
||||
'__type': "<class 'zoneinfo.ZoneInfo'>",
|
||||
'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')",
|
||||
}),
|
||||
'havdalah_offset': 0,
|
||||
'location': dict({
|
||||
'altitude': '**REDACTED**',
|
||||
'diaspora': False,
|
||||
'latitude': '**REDACTED**',
|
||||
'longitude': '**REDACTED**',
|
||||
'name': 'test home',
|
||||
'timezone': dict({
|
||||
'__type': "<class 'zoneinfo.ZoneInfo'>",
|
||||
'repr': "zoneinfo.ZoneInfo(key='Asia/Jerusalem')",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@@ -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,33 +80,22 @@
|
||||
'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({
|
||||
'__type': "<class 'freezegun.api.FakeDate'>",
|
||||
'isoformat': '2025-05-19',
|
||||
}),
|
||||
'zmanim': dict({
|
||||
'candle_lighting_offset': 18,
|
||||
'date': dict({
|
||||
'__type': "<class 'freezegun.api.FakeDate'>",
|
||||
'isoformat': '2025-05-19',
|
||||
}),
|
||||
'havdalah_offset': 0,
|
||||
'location': dict({
|
||||
'altitude': '**REDACTED**',
|
||||
'diaspora': True,
|
||||
'latitude': '**REDACTED**',
|
||||
'longitude': '**REDACTED**',
|
||||
'name': 'test home',
|
||||
'timezone': dict({
|
||||
'__type': "<class 'zoneinfo.ZoneInfo'>",
|
||||
'repr': "zoneinfo.ZoneInfo(key='America/New_York')",
|
||||
}),
|
||||
'havdalah_offset': 0,
|
||||
'location': dict({
|
||||
'altitude': '**REDACTED**',
|
||||
'diaspora': True,
|
||||
'latitude': '**REDACTED**',
|
||||
'longitude': '**REDACTED**',
|
||||
'name': 'test home',
|
||||
'timezone': dict({
|
||||
'__type': "<class 'zoneinfo.ZoneInfo'>",
|
||||
'repr': "zoneinfo.ZoneInfo(key='America/New_York')",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
@@ -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,33 +134,22 @@
|
||||
'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({
|
||||
'__type': "<class 'freezegun.api.FakeDate'>",
|
||||
'isoformat': '2025-05-19',
|
||||
}),
|
||||
'zmanim': dict({
|
||||
'candle_lighting_offset': 18,
|
||||
'date': dict({
|
||||
'__type': "<class 'freezegun.api.FakeDate'>",
|
||||
'isoformat': '2025-05-19',
|
||||
}),
|
||||
'havdalah_offset': 0,
|
||||
'location': dict({
|
||||
'altitude': '**REDACTED**',
|
||||
'diaspora': False,
|
||||
'latitude': '**REDACTED**',
|
||||
'longitude': '**REDACTED**',
|
||||
'name': 'test home',
|
||||
'timezone': dict({
|
||||
'__type': "<class 'zoneinfo.ZoneInfo'>",
|
||||
'repr': "zoneinfo.ZoneInfo(key='US/Pacific')",
|
||||
}),
|
||||
'havdalah_offset': 0,
|
||||
'location': dict({
|
||||
'altitude': '**REDACTED**',
|
||||
'diaspora': False,
|
||||
'latitude': '**REDACTED**',
|
||||
'longitude': '**REDACTED**',
|
||||
'name': 'test home',
|
||||
'timezone': dict({
|
||||
'__type': "<class 'zoneinfo.ZoneInfo'>",
|
||||
'repr': "zoneinfo.ZoneInfo(key='US/Pacific')",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
|
Reference in New Issue
Block a user