New apps Added

This commit is contained in:
2026-07-08 10:43:39 -04:00
parent 3b1f4bbd75
commit fefc2c8b5c
1114 changed files with 406637 additions and 154 deletions
+446
View File
@@ -0,0 +1,446 @@
"""The Scheduler Integration."""
import logging
import voluptuous as vol
import datetime
import homeassistant.util.dt as dt_util
from homeassistant.helpers import config_validation as cv
from homeassistant.components.switch import DOMAIN as PLATFORM
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
ATTR_ENTITY_ID,
ATTR_NAME,
)
from homeassistant.core import HomeAssistant, asyncio, CoreState, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.entity_registry import async_get as get_entity_registry
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
)
from homeassistant.helpers.event import (
async_call_later,
async_track_state_change_event,
async_track_point_in_time,
)
from . import const
from .store import async_get_registry
from .websockets import async_register_websockets
_LOGGER = logging.getLogger(__name__)
async def async_setup(hass, config):
"""Track states and offer events for sensors."""
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"""Set up Scheduler integration from a config entry."""
session = async_get_clientsession(hass)
store = await async_get_registry(hass)
coordinator = SchedulerCoordinator(hass, session, entry, store)
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(const.DOMAIN, coordinator.id)},
name="Scheduler",
model="Scheduler",
sw_version=const.VERSION,
manufacturer="@nielsfaber",
)
hass.data.setdefault(const.DOMAIN, {})
hass.data[const.DOMAIN] = {"coordinator": coordinator, "schedules": {}}
if entry.unique_id is None:
hass.config_entries.async_update_entry(entry, unique_id=coordinator.id)
await hass.config_entries.async_forward_entry_setups(entry, [PLATFORM])
await async_register_websockets(hass)
@callback
def service_create_schedule(service):
coordinator.async_create_schedule(dict(service.data))
hass.services.async_register(
const.DOMAIN,
const.SERVICE_ADD,
service_create_schedule,
schema=const.ADD_SCHEDULE_SCHEMA,
)
@callback
def async_service_edit_schedule(service):
match = None
for (schedule_id, entity) in hass.data[const.DOMAIN]["schedules"].items():
if entity.entity_id == service.data[const.ATTR_ENTITY_ID]:
match = schedule_id
continue
if not match:
raise vol.Invalid(
"Entity not found: {}".format(service.data[const.ATTR_ENTITY_ID])
)
else:
data = dict(service.data)
del data[const.ATTR_ENTITY_ID]
coordinator.async_edit_schedule(match, data)
hass.services.async_register(
const.DOMAIN,
const.SERVICE_EDIT,
async_service_edit_schedule,
schema=const.EDIT_SCHEDULE_SCHEMA.extend(
{vol.Required(ATTR_ENTITY_ID): cv.string}
),
)
@callback
def async_service_remove_schedule(service):
match = None
for (schedule_id, entity) in hass.data[const.DOMAIN]["schedules"].items():
if entity.entity_id == service.data["entity_id"]:
match = schedule_id
continue
if not match:
raise vol.Invalid("Entity not found: {}".format(service.data["entity_id"]))
else:
coordinator.async_delete_schedule(match)
hass.services.async_register(
const.DOMAIN,
const.SERVICE_REMOVE,
async_service_remove_schedule,
schema=vol.Schema({vol.Required(ATTR_ENTITY_ID): cv.string}),
)
@callback
def service_copy_schedule(service):
match = None
for (schedule_id, entity) in hass.data[const.DOMAIN]["schedules"].items():
if entity.entity_id == service.data[const.ATTR_ENTITY_ID]:
match = schedule_id
continue
if not match:
raise vol.Invalid(
"Entity not found: {}".format(service.data[const.ATTR_ENTITY_ID])
)
else:
data = store.async_get_schedule(match)
tags = coordinator.async_get_tags_for_schedule(data[const.ATTR_SCHEDULE_ID])
if tags:
data[const.ATTR_TAGS] = tags
del data[const.ATTR_SCHEDULE_ID]
if ATTR_NAME in service.data:
data[ATTR_NAME] = service.data[ATTR_NAME].strip()
coordinator.async_create_schedule(data)
hass.services.async_register(
const.DOMAIN,
const.SERVICE_COPY,
service_copy_schedule,
schema=vol.Schema(
{
vol.Required(ATTR_ENTITY_ID): cv.string,
vol.Optional(ATTR_NAME): vol.Any(cv.string, None),
}
),
)
async def async_service_disable_all(service):
await coordinator.async_disable_all_schedules()
hass.services.async_register(
const.DOMAIN,
const.SERVICE_DISABLE_ALL,
async_service_disable_all
)
async def async_service_enable_all(service):
await coordinator.async_enable_all_schedules()
hass.services.async_register(
const.DOMAIN,
const.SERVICE_ENABLE_ALL,
async_service_enable_all
)
return True
async def async_migrate_entry(hass, config_entry: ConfigEntry):
"""Migrate old entry."""
_LOGGER.debug("Migrating from version %s", config_entry.version)
if config_entry.version == 1:
config_entry.version = 2
config_entry.data = {"migrate_entities": True}
return True
async def async_unload_entry(hass, entry):
"""Unload Scheduler config entry."""
unload_ok = all(
await asyncio.gather(
*[hass.config_entries.async_forward_entry_unload(entry, PLATFORM)]
)
)
coordinator = hass.data[const.DOMAIN]["coordinator"]
await coordinator.async_unload()
return unload_ok
async def async_remove_entry(hass, entry):
"""Remove Scheduler data."""
coordinator = hass.data[const.DOMAIN]["coordinator"]
await coordinator.async_delete_config()
del hass.data[const.DOMAIN]
class SchedulerCoordinator(DataUpdateCoordinator):
"""Define an object to hold scheduler data."""
def __init__(self, hass, session, entry, store):
"""Initialize."""
self.id = entry.unique_id
self.hass = hass
self.store = store
self.state = const.STATE_INIT
self._workday_tracker = None
self._workday_timer = None
self.stopped = False
super().__init__(hass, _LOGGER, name=const.DOMAIN)
# detect time of prior shutdown to determine which schedules need to be triggered
time_shutdown = self.store.async_get_time_shutdown()
if time_shutdown:
self.time_shutdown = dt_util.as_local(datetime.datetime.fromisoformat(time_shutdown))
_LOGGER.debug("Scheduler detected a shutdown at {}.".format(self.time_shutdown))
else:
self.time_shutdown = None
# wait for 10 seconds after HA startup to allow entities to be initialized
@callback
def handle_startup(_event):
hass.async_create_task(self.async_init_workday_sensor())
@callback
def async_timer_finished(_now):
self.state = const.STATE_READY
async_dispatcher_send(self.hass, const.EVENT_STARTED)
async_call_later(hass, 10, async_timer_finished)
if hass.state == CoreState.running:
handle_startup(None)
else:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, handle_startup)
# store the current date+time when scheduler is being shutdown
@callback
async def async_handle_shutdown(_event):
if self.stopped:
return
now = dt_util.utcnow().isoformat()
await self.store.async_set_time_shutdown(now)
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_handle_shutdown)
def async_get_schedule(self, schedule_id: str):
"""fetch a schedule (websocket API hook)"""
if schedule_id not in self.hass.data[const.DOMAIN]["schedules"]:
return None
item = self.hass.data[const.DOMAIN]["schedules"][schedule_id]
return item.async_get_entity_state()
def async_get_schedules(self):
"""fetch a list of schedules (websocket API hook)"""
schedules = self.hass.data[const.DOMAIN]["schedules"]
data = []
for item in schedules.values():
config = item.async_get_entity_state()
data.append(config)
return data
@callback
def async_create_schedule(self, data):
"""add a new schedule"""
tags = None
if const.ATTR_TAGS in data:
tags = data[const.ATTR_TAGS]
del data[const.ATTR_TAGS]
res = self.store.async_create_schedule(data)
if res:
self.async_assign_tags_to_schedule(res.schedule_id, tags)
async_dispatcher_send(self.hass, const.EVENT_ITEM_CREATED, res)
@callback
def async_edit_schedule(self, schedule_id: str, data: dict):
"""edit an existing schedule"""
if schedule_id not in self.hass.data[const.DOMAIN]["schedules"]:
return
item = self.async_get_schedule(schedule_id)
if ATTR_NAME in data and item[ATTR_NAME] != data[ATTR_NAME]:
data[ATTR_NAME] = data[ATTR_NAME].strip()
elif ATTR_NAME in data:
del data[ATTR_NAME]
tags_updated = False
tags = None
if const.ATTR_TAGS in data:
tags_updated = True
tags = data[const.ATTR_TAGS]
del data[const.ATTR_TAGS]
entry = self.store.async_update_schedule(schedule_id, data)
if tags_updated:
self.async_assign_tags_to_schedule(schedule_id, tags)
entity = self.hass.data[const.DOMAIN]["schedules"][schedule_id]
if ATTR_NAME in data:
# if the name has been changed, the entity ID must change hence the entity should be destroyed
entity_registry = get_entity_registry(self.hass)
entity_registry.async_remove(entity.entity_id)
async_dispatcher_send(self.hass, const.EVENT_ITEM_CREATED, entry)
else:
async_dispatcher_send(self.hass, const.EVENT_ITEM_UPDATED, schedule_id)
@callback
def async_delete_schedule(self, schedule_id: str):
"""delete an existing schedule"""
if schedule_id not in self.hass.data[const.DOMAIN]["schedules"]:
return
entity = self.hass.data[const.DOMAIN]["schedules"][schedule_id]
entity_registry = get_entity_registry(self.hass)
entity_registry.async_remove(entity.entity_id)
self.store.async_delete_schedule(schedule_id)
self.async_assign_tags_to_schedule(schedule_id, None)
self.hass.data[const.DOMAIN]["schedules"].pop(schedule_id, None)
async_dispatcher_send(self.hass, const.EVENT_ITEM_REMOVED, schedule_id)
async def _async_update_data(self):
"""Update data via library."""
return True
async def async_unload(self):
if self._workday_tracker:
self._workday_tracker()
self._workday_tracker = None
self.stopped = True
async def async_delete_config(self):
await self.store.async_delete()
def async_get_tags(self):
"""fetch a list of tags (websocket API hook)"""
tags = self.store.async_get_tags()
return list(tags.values())
def async_get_tags_for_schedule(self, schedule_id: str):
"""fetch a list of tags for a schedule"""
tags = self.async_get_tags()
result = filter(lambda el: schedule_id in el[const.ATTR_SCHEDULES], tags)
result = list(map(lambda x: x[ATTR_NAME], result))
result = sorted(result)
return result
def async_assign_tags_to_schedule(self, schedule_id: str, new_tags: list):
if not new_tags:
new_tags = []
old_tags = self.async_get_tags_for_schedule(schedule_id)
for tag_name in old_tags:
if tag_name not in new_tags:
# remove old tag
el = self.store.async_get_tag(tag_name)
if len(el[const.ATTR_SCHEDULES]) > 1:
self.store.async_update_tag(
tag_name,
{
const.ATTR_SCHEDULES: [
x for x in el[const.ATTR_SCHEDULES] if x != schedule_id
]
},
)
else:
self.store.async_delete_tag(tag_name)
else:
new_tags.remove(tag_name)
for tag_name in new_tags:
# assign new tag
el = self.store.async_get_tag(tag_name)
if el:
self.store.async_update_tag(
tag_name,
{const.ATTR_SCHEDULES: el[const.ATTR_SCHEDULES] + [schedule_id]},
)
else:
self.store.async_create_tag(
{ATTR_NAME: tag_name, const.ATTR_SCHEDULES: [schedule_id]}
)
async def async_reset_workday_timer(self):
"""the workday polling timer has finished"""
@callback
async def async_workday_timer_finished(_now):
"""perform daily polling of the workday entity"""
_LOGGER.debug("Performing daily update of workday sensor")
await self.async_reset_workday_timer()
async_dispatcher_send(self.hass, const.EVENT_WORKDAY_SENSOR_UPDATED)
now = dt_util.as_local(dt_util.utcnow())
ts = dt_util.find_next_time_expression_time(
now, seconds=[0], minutes=[5], hours=[0]
)
today = now.date()
while ts.date() == today:
# ensure the timer is set for the next day
now = now + datetime.timedelta(days=1)
ts = dt_util.find_next_time_expression_time(
now, seconds=[0], minutes=[5], hours=[0]
)
if self._workday_timer:
self._workday_timer()
self._workday_timer = async_track_point_in_time(
self.hass, async_workday_timer_finished, ts
)
async def async_init_workday_sensor(self):
"""watch for changes in the workday sensor"""
workday_entity = self.hass.states.get(const.WORKDAY_ENTITY)
if not workday_entity:
return None
@callback
async def async_workday_state_updated(_event):
"""the workday sensor has been updated"""
_LOGGER.debug("Workday sensor has updated")
await self.async_reset_workday_timer()
async_dispatcher_send(self.hass, const.EVENT_WORKDAY_SENSOR_UPDATED)
self._workday_tracker = async_track_state_change_event(
self.hass, const.WORKDAY_ENTITY, async_workday_state_updated
)
await self.async_reset_workday_timer()
async def async_enable_all_schedules(self):
"""enables all schedules"""
for schedule in self.hass.data[const.DOMAIN]["schedules"].values():
await schedule.async_turn_on()
async def async_disable_all_schedules(self):
"""disables all schedules"""
for schedule in self.hass.data[const.DOMAIN]["schedules"].values():
await schedule.async_turn_off()