1 """Support for schedules in Home Assistant."""
3 from __future__
import annotations
5 from collections.abc
import Callable
6 from datetime
import datetime, time, timedelta
8 from typing
import Any, Literal
10 import voluptuous
as vol
24 DictStorageCollection,
25 DictStorageCollectionWebsocket,
27 SerializedStorageCollection,
29 sync_entity_lifecycle,
51 STORAGE_VERSION_MINOR = 1
55 """Validate the schedule of time ranges.
57 Ensure they have no overlap and the end time is greater than the start time.
64 schedule = sorted(schedule, key=
lambda time_range: time_range[CONF_FROM])
68 for time_range
in schedule:
69 if time_range[CONF_FROM] >= time_range[CONF_TO]:
71 f
"Invalid time range, from {time_range[CONF_FROM]} is after"
72 f
" {time_range[CONF_TO]}"
76 if previous_to
is not None and previous_to > time_range[CONF_FROM]:
77 raise vol.Invalid(
"Overlapping times found in schedule")
79 previous_to = time_range[CONF_TO]
85 """Convert 24:00 and 24:00:00 to time.max."""
86 if not isinstance(value, str):
89 parts = value.split(
":")
93 minute =
int(parts[1])
95 if hour == 24
and minute == 0:
102 """Convert time.max to 24:00:00."""
103 if value == time.max:
105 return vol.Coerce(str)(value)
108 BASE_SCHEMA: VolDictType = {
109 vol.Required(CONF_NAME): vol.All(str, vol.Length(min=1)),
110 vol.Optional(CONF_ICON): cv.icon,
114 CUSTOM_DATA_SCHEMA = vol.Schema({str: vol.Any(bool, str, int, float)})
116 TIME_RANGE_SCHEMA: VolDictType = {
117 vol.Required(CONF_FROM): cv.time,
118 vol.Required(CONF_TO): deserialize_to_time,
119 vol.Optional(CONF_DATA): CUSTOM_DATA_SCHEMA,
123 STORAGE_TIME_RANGE_SCHEMA = vol.Schema(
125 vol.Required(CONF_FROM): vol.Coerce(str),
126 vol.Required(CONF_TO): serialize_to_time,
127 vol.Optional(CONF_DATA): CUSTOM_DATA_SCHEMA,
131 SCHEDULE_SCHEMA: VolDictType = {
132 vol.Optional(day, default=[]): vol.All(
133 cv.ensure_list, [TIME_RANGE_SCHEMA], valid_schedule
135 for day
in CONF_ALL_DAYS
137 STORAGE_SCHEDULE_SCHEMA: VolDictType = {
138 vol.Optional(day, default=[]): vol.All(
139 cv.ensure_list, [TIME_RANGE_SCHEMA], valid_schedule, [STORAGE_TIME_RANGE_SCHEMA]
141 for day
in CONF_ALL_DAYS
145 CONFIG_SCHEMA = vol.Schema(
146 {DOMAIN: cv.schema_with_slug_keys(vol.All(BASE_SCHEMA | SCHEDULE_SCHEMA))},
147 extra=vol.ALLOW_EXTRA,
150 STORAGE_SCHEMA = vol.Schema(
151 {vol.Required(CONF_ID): cv.string} | BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA
154 ENTITY_SCHEMA = vol.Schema(
155 {vol.Required(CONF_ID): cv.string} | BASE_SCHEMA | SCHEDULE_SCHEMA
159 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
160 """Set up a schedule."""
161 component = EntityComponent[Schedule](LOGGER, DOMAIN, hass)
172 version=STORAGE_VERSION,
173 minor_version=STORAGE_VERSION_MINOR,
179 await yaml_collection.async_load(
180 [{CONF_ID: id_, **cfg}
for id_, cfg
in config.get(DOMAIN, {}).items()]
182 await storage_collection.async_load()
188 BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA,
189 BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA,
192 async
def reload_service_handler(service_call: ServiceCall) ->
None:
193 """Reload yaml entities."""
194 conf = await component.async_prepare_reload(skip_reset=
True)
197 await yaml_collection.async_load(
198 [{CONF_ID: id_, **cfg}
for id_, cfg
in conf.get(DOMAIN, {}).items()]
205 reload_service_handler,
212 """Schedules stored in storage."""
214 SCHEMA = vol.Schema(BASE_SCHEMA | STORAGE_SCHEDULE_SCHEMA)
217 """Validate the config is valid."""
223 """Suggest an ID based on the config."""
224 name: str = info[CONF_NAME]
228 """Return a new updated data object."""
229 self.
SCHEMASCHEMA(update_data)
230 return item | update_data
240 """Schedule entity."""
242 _entity_component_unrecorded_attributes = frozenset(
243 {ATTR_EDITABLE, ATTR_NEXT_EVENT}
246 _attr_has_entity_name =
True
247 _attr_should_poll =
False
248 _attr_state: Literal[
"on",
"off"]
251 _unsub_update: Callable[[],
None] |
None =
None
253 def __init__(self, config: ConfigType, editable: bool) ->
None:
254 """Initialize a schedule."""
269 """Return entity instance initialized from storage."""
270 return cls(config, editable=
True)
274 """Return entity instance initialized from yaml."""
275 schedule = cls(config, editable=
False)
276 schedule.entity_id = f
"{DOMAIN}.{config[CONF_ID]}"
280 """Handle when the config is updated."""
282 self.
_attr_icon_attr_icon = config.get(CONF_ICON)
289 """Remove the update timer."""
295 """Run when entity about to be added to hass."""
300 def _update(self, _: datetime |
None =
None) ->
None:
301 """Update the states of the schedule."""
303 todays_schedule = self.
_config_config.
get(WEEKDAY_TO_CONF[now.weekday()], [])
306 for time_range
in todays_schedule:
308 if now.time() < time_range[CONF_FROM]:
312 if now.time() < time_range[CONF_TO]
or time_range[CONF_TO] == time.max:
314 current_data = time_range.get(CONF_DATA)
325 WEEKDAY_TO_CONF[(now.weekday() + day) % 7], []
330 [time_range[CONF_FROM], time_range[CONF_TO]]
331 for time_range
in day_schedule
336 if next_event := next(
339 for timestamp
in times
341 possible_next_event := (
342 datetime.combine(now.date(), timestamp, tzinfo=now.tzinfo)
344 if timestamp != time.max
346 else datetime.combine(now.date(),
time(), tzinfo=now.tzinfo)
358 ATTR_NEXT_EVENT: next_event,
375 """Return the set of all currently used custom data attribute keys."""
378 for weekday
in WEEKDAY_TO_CONF.values():
379 if not (weekday_config := self.
_config_config.
get(weekday)):
382 for time_range
in weekday_config:
383 time_range_custom_data = time_range.get(CONF_DATA)
385 if not time_range_custom_data
or not isinstance(
386 time_range_custom_data, dict
390 data_keys.update(time_range_custom_data.keys())
392 return frozenset(data_keys)
dict _process_create_data(self, dict data)
dict _update_data(self, dict item, dict update_data)
SerializedStorageCollection|None _async_load_data(self)
str _get_suggested_id(self, dict info)
Schedule from_yaml(cls, ConfigType config)
None _clean_up_listener(self)
_attr_extra_state_attributes
Schedule from_storage(cls, ConfigType config)
None async_update_config(self, ConfigType config)
None async_added_to_hass(self)
None __init__(self, ConfigType config, bool editable)
frozenset[str] all_custom_data_keys(self)
_entity_component_unrecorded_attributes
None _update(self, datetime|None _=None)
_attr_capability_attributes
_Entity__combined_unrecorded_attributes
None async_write_ha_state(self)
None async_on_remove(self, CALLBACK_TYPE func)
web.Response get(self, web.Request request, str config_key)
IssData update(pyiss.ISS iss)
Any serialize_to_time(Any value)
bool async_setup(HomeAssistant hass, ConfigType config)
Any deserialize_to_time(Any value)
list[dict[str, str]] valid_schedule(list[dict[str, str]] schedule)
None sync_entity_lifecycle(HomeAssistant hass, str domain, str platform, EntityComponent[_EntityT] entity_component, StorageCollection|YamlCollection collection, type[CollectionEntity] entity_class)
bool time(HomeAssistant hass, dt_time|str|None before=None, dt_time|str|None after=None, str|Container[str]|None weekday=None)
CALLBACK_TYPE async_track_point_in_utc_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)
None async_register_admin_service(HomeAssistant hass, str domain, str service, Callable[[ServiceCall], Awaitable[None]|None] service_func, VolSchemaType schema=vol.Schema({}, extra=vol.PREVENT_EXTRA))