Files
HomeAssistantVS/custom_components/scheduler/store.py
T
2026-07-08 10:43:39 -04:00

377 lines
13 KiB
Python

import logging
import secrets
from collections import OrderedDict
from typing import MutableMapping, cast
import attr
from homeassistant.core import callback, HomeAssistant
from homeassistant.loader import bind_hass
from homeassistant.const import (
ATTR_NAME,
CONF_CONDITIONS,
)
from homeassistant.helpers.storage import Store
from . import const
_LOGGER = logging.getLogger(__name__)
DATA_REGISTRY = f"{const.DOMAIN}_storage"
STORAGE_KEY = f"{const.DOMAIN}.storage"
STORAGE_VERSION = 3
SAVE_DELAY = 10
@attr.s(slots=True, frozen=True)
class ActionEntry:
"""Action storage Entry."""
service = attr.ib(type=str, default="")
entity_id = attr.ib(type=str, default=None)
service_data = attr.ib(type=dict, default={})
@attr.s(slots=True, frozen=True)
class ConditionEntry:
"""Condition storage Entry."""
entity_id = attr.ib(type=str, default=None)
attribute = attr.ib(type=str, default=None)
value = attr.ib(type=str, default=None)
match_type = attr.ib(type=str, default=None)
@attr.s(slots=True, frozen=True)
class TimeslotEntry:
"""Timeslot storage Entry."""
start = attr.ib(type=str, default=None)
stop = attr.ib(type=str, default=None)
conditions = attr.ib(type=[ConditionEntry], default=[])
condition_type = attr.ib(type=str, default=None)
track_conditions = attr.ib(type=bool, default=False)
actions = attr.ib(type=[ActionEntry], default=[])
@attr.s(slots=True, frozen=True)
class ScheduleEntry:
"""Schedule storage Entry."""
schedule_id = attr.ib(type=str, default=None)
weekdays = attr.ib(type=list, default=[])
start_date = attr.ib(type=str, default=None)
end_date = attr.ib(type=str, default=None)
timeslots = attr.ib(type=[TimeslotEntry], default=[])
repeat_type = attr.ib(type=str, default=None)
name = attr.ib(type=str, default=None)
enabled = attr.ib(type=bool, default=True)
@attr.s(slots=True, frozen=True)
class TagEntry:
"""Tag storage Entry."""
name = attr.ib(type=str, default=None)
schedules = attr.ib(type=[str], default=[])
def parse_schedule_data(data: dict):
if const.ATTR_TIMESLOTS in data:
timeslots = []
for item in data[const.ATTR_TIMESLOTS]:
timeslot = TimeslotEntry(**item)
if CONF_CONDITIONS in item and item[CONF_CONDITIONS]:
conditions = []
for condition in item[CONF_CONDITIONS]:
conditions.append(ConditionEntry(**condition))
timeslot = attr.evolve(timeslot, **{CONF_CONDITIONS: conditions})
if const.ATTR_ACTIONS in item and item[const.ATTR_ACTIONS]:
actions = []
for action in item[const.ATTR_ACTIONS]:
actions.append(ActionEntry(**action))
timeslot = attr.evolve(timeslot, **{const.ATTR_ACTIONS: actions})
timeslots.append(timeslot)
data[const.ATTR_TIMESLOTS] = timeslots
return data
class MigratableStore(Store):
async def _async_migrate_func(self, old_version, data: dict):
def remove_unequal_number_conditions(timeslots):
"""ensure all timeslots have the same number of conditions"""
if len(timeslots) > 1 and not all(
len(el["conditions"]) == len(timeslots[0]["conditions"])
for el in timeslots
):
return [
{
**slot,
"conditions": timeslots[0]["conditions"]
}
for slot in timeslots
]
return timeslots
if old_version < 2:
data["schedules"] = (
[
{
**entry,
const.ATTR_START_DATE: entry[const.ATTR_START_DATE]
if const.ATTR_START_DATE in entry
else None,
const.ATTR_END_DATE: entry[const.ATTR_END_DATE]
if const.ATTR_END_DATE in entry
else None,
}
for entry in data["schedules"]
]
if "schedules" in data
else []
)
if old_version < 3:
data["schedules"] = (
[
{
**entry,
const.ATTR_TIMESLOTS: remove_unequal_number_conditions(entry[const.ATTR_TIMESLOTS])
}
for entry in data["schedules"]
]
if "schedules" in data
else []
)
return data
class ScheduleStorage:
"""Class to hold scheduler data."""
def __init__(self, hass: HomeAssistant) -> None:
"""Initialize the storage."""
self.hass = hass
self.schedules: MutableMapping[str, ScheduleEntry] = {}
self.tags: MutableMapping[str, TagEntry] = {}
self.time_shutdown = None
self._store = MigratableStore(hass, STORAGE_VERSION, STORAGE_KEY)
async def async_load(self) -> None:
"""Load the registry of schedule entries."""
data = await self._store.async_load()
schedules: "OrderedDict[str, ScheduleEntry]" = OrderedDict()
tags: "OrderedDict[str, TagEntry]" = OrderedDict()
if data is not None:
if "schedules" in data:
for entry in data["schedules"]:
entry = parse_schedule_data(entry)
schedules[entry[const.ATTR_SCHEDULE_ID]] = ScheduleEntry(
schedule_id=entry[const.ATTR_SCHEDULE_ID],
weekdays=entry[const.ATTR_WEEKDAYS],
start_date=entry[const.ATTR_START_DATE],
end_date=entry[const.ATTR_END_DATE],
timeslots=entry[const.ATTR_TIMESLOTS],
repeat_type=entry[const.ATTR_REPEAT_TYPE],
name=entry[ATTR_NAME],
enabled=entry[const.ATTR_ENABLED],
)
if "tags" in data:
for entry in data["tags"]:
tags[entry[ATTR_NAME]] = TagEntry(
name=entry[ATTR_NAME],
schedules=entry[const.ATTR_SCHEDULES],
)
if "time_shutdown" in data:
self.time_shutdown = data["time_shutdown"]
self.schedules = schedules
self.tags = tags
@callback
def async_schedule_save(self) -> None:
"""Schedule saving the registry of schedules."""
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
async def async_save(self) -> None:
"""Save the registry of schedules."""
await self._store.async_save(self._data_to_save())
@callback
def _data_to_save(self) -> dict:
"""Return data for the registry for schedules to store in a file."""
store_data = {}
store_data["schedules"] = []
store_data["tags"] = []
for entry in self.schedules.values():
item = {
const.ATTR_SCHEDULE_ID: entry.schedule_id,
const.ATTR_TIMESLOTS: [],
const.ATTR_WEEKDAYS: entry.weekdays,
const.ATTR_START_DATE: entry.start_date,
const.ATTR_END_DATE: entry.end_date,
const.ATTR_REPEAT_TYPE: entry.repeat_type,
ATTR_NAME: entry.name,
const.ATTR_ENABLED: entry.enabled,
}
for slot in entry.timeslots:
timeslot = {
const.ATTR_START: slot.start,
const.ATTR_STOP: slot.stop,
CONF_CONDITIONS: [],
const.ATTR_CONDITION_TYPE: slot.condition_type,
const.ATTR_TRACK_CONDITIONS: slot.track_conditions,
const.ATTR_ACTIONS: [],
}
if slot.conditions:
for condition in slot.conditions:
timeslot[CONF_CONDITIONS].append(attr.asdict(condition))
if slot.actions:
for action in slot.actions:
timeslot[const.ATTR_ACTIONS].append(attr.asdict(action))
item[const.ATTR_TIMESLOTS].append(timeslot)
store_data["schedules"].append(item)
store_data["tags"] = [attr.asdict(entry) for entry in self.tags.values()]
if self.time_shutdown:
store_data["time_shutdown"] = self.time_shutdown
return store_data
async def async_delete(self):
"""Delete config."""
_LOGGER.warning("Removing scheduler configuration data!")
self.schedules = {}
self.tags = {}
await self._store.async_remove()
@callback
def async_get_schedule(self, entity_id) -> dict:
"""Get an existing ScheduleEntry by id."""
res = self.schedules.get(entity_id)
return attr.asdict(res) if res else None
@callback
def async_get_schedules(self) -> dict:
"""Get an existing ScheduleEntry by id."""
res = {}
for (key, val) in self.schedules.items():
res[key] = attr.asdict(val)
return res
@callback
def async_create_schedule(self, data: dict) -> ScheduleEntry:
"""Create a new ScheduleEntry."""
if const.ATTR_SCHEDULE_ID in data:
schedule_id = data[const.ATTR_SCHEDULE_ID]
del data[const.ATTR_SCHEDULE_ID]
if schedule_id in self.schedules:
return
else:
schedule_id = secrets.token_hex(3)
while schedule_id in self.schedules:
schedule_id = secrets.token_hex(3)
data = parse_schedule_data(data)
new_schedule = ScheduleEntry(**data, schedule_id=schedule_id)
self.schedules[schedule_id] = new_schedule
self.async_schedule_save()
return new_schedule
@callback
def async_delete_schedule(self, schedule_id: str) -> None:
"""Delete ScheduleEntry."""
if schedule_id in self.schedules:
del self.schedules[schedule_id]
self.async_schedule_save()
return True
return False
@callback
def async_update_schedule(self, schedule_id: str, changes: dict) -> ScheduleEntry:
"""Update existing ScheduleEntry."""
old = self.schedules[schedule_id]
changes = parse_schedule_data(changes)
new = self.schedules[schedule_id] = attr.evolve(old, **changes)
self.async_schedule_save()
return new
@callback
def async_get_tag(self, name: str) -> dict:
"""Get an existing TagEntry by id."""
res = self.tags.get(name)
return attr.asdict(res) if res else None
@callback
def async_get_tags(self) -> dict:
"""Get an existing TagEntry by id."""
res = {}
for (key, val) in self.tags.items():
res[key] = attr.asdict(val)
return res
@callback
def async_create_tag(self, data: dict) -> TagEntry:
"""Create a new TagEntry."""
name = data[ATTR_NAME] if ATTR_NAME in data else None
if not name or name in data:
return None
new_tag = TagEntry(**data)
self.tags[name] = new_tag
self.async_schedule_save()
return new_tag
@callback
def async_delete_tag(self, name: str) -> None:
"""Delete TagEntry."""
if name in self.tags:
del self.tags[name]
self.async_schedule_save()
return True
return False
@callback
def async_update_tag(self, name: str, changes: dict) -> TagEntry:
"""Update existing TagEntry."""
old = self.tags[name]
changes = parse_schedule_data(changes)
new = self.tags[name] = attr.evolve(old, **changes)
self.async_schedule_save()
return new
@callback
def async_get_time_shutdown(self) -> dict:
"""Get the shutdown time and clear the stored value afterwards."""
res = self.time_shutdown
self.time_shutdown = None
self.async_schedule_save()
return res
@callback
async def async_set_time_shutdown(self, value: str):
"""Set the shutdown time and store it immediately."""
self.time_shutdown = value
await self.async_save()
@bind_hass
async def async_get_registry(hass: HomeAssistant) -> ScheduleStorage:
"""Return alarmo storage instance."""
task = hass.data.get(DATA_REGISTRY)
if task is None:
async def _load_reg() -> ScheduleStorage:
registry = ScheduleStorage(hass)
await registry.async_load()
return registry
task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg())
return cast(ScheduleStorage, await task)