1 """Provide a way to categorize things within a defined scope."""
3 from __future__
import annotations
5 from collections.abc
import Iterable
7 from dataclasses
import dataclass, field
8 from datetime
import datetime
9 from typing
import Any, Literal, TypedDict
17 from .registry
import BaseRegistry
18 from .singleton
import singleton
19 from .storage
import Store
20 from .typing
import UNDEFINED, UndefinedType
22 DATA_REGISTRY: HassKey[CategoryRegistry] =
HassKey(
"category_registry")
23 EVENT_CATEGORY_REGISTRY_UPDATED: EventType[EventCategoryRegistryUpdatedData] = (
26 STORAGE_KEY =
"core.category_registry"
27 STORAGE_VERSION_MAJOR = 1
28 STORAGE_VERSION_MINOR = 2
32 """Data type for individual category. Used in CategoryRegistryStoreData."""
42 """Store data type for CategoryRegistry."""
44 categories: dict[str, list[_CategoryStoreData]]
48 """Event data for when the category registry is updated."""
50 action: Literal[
"create",
"remove",
"update"]
55 type EventCategoryRegistryUpdated = Event[EventCategoryRegistryUpdatedData]
58 @dataclass(slots=True, kw_only=True, frozen=True)
60 """Category registry entry."""
62 category_id: str = field(default_factory=ulid_now)
63 created_at: datetime = field(default_factory=utcnow)
64 icon: str |
None =
None
65 modified_at: datetime = field(default_factory=utcnow)
70 """Store category registry data."""
74 old_major_version: int,
75 old_minor_version: int,
76 old_data: dict[str, dict[str, list[dict[str, Any]]]],
77 ) -> CategoryRegistryStoreData:
78 """Migrate to the new version."""
79 if old_major_version > STORAGE_VERSION_MAJOR:
80 raise ValueError(
"Can't migrate to future version")
82 if old_major_version == 1:
83 if old_minor_version < 2:
86 for categories
in old_data[
"categories"].values():
87 for category
in categories:
88 category[
"created_at"] = category[
"modified_at"] = created_at
94 """Class to hold a registry of categories by scope."""
96 def __init__(self, hass: HomeAssistant) ->
None:
97 """Initialize the category registry."""
99 self.
categoriescategories: dict[str, dict[str, CategoryEntry]] = {}
102 STORAGE_VERSION_MAJOR,
105 minor_version=STORAGE_VERSION_MINOR,
110 self, *, scope: str, category_id: str
111 ) -> CategoryEntry |
None:
112 """Get category by ID."""
119 """Get all categories."""
122 return self.
categoriescategories[scope].values()
130 icon: str |
None =
None,
132 """Create a new category."""
133 self.
hasshass.verify_event_loop_thread(
"category_registry.async_create")
143 self.
categoriescategories[scope][category.category_id] = category
145 self.async_schedule_save()
146 self.
hasshass.bus.async_fire_internal(
147 EVENT_CATEGORY_REGISTRY_UPDATED,
149 action=
"create", scope=scope, category_id=category.category_id
156 """Delete category."""
157 self.
hasshass.verify_event_loop_thread(
"category_registry.async_delete")
158 del self.
categoriescategories[scope][category_id]
159 self.
hasshass.bus.async_fire_internal(
160 EVENT_CATEGORY_REGISTRY_UPDATED,
164 category_id=category_id,
167 self.async_schedule_save()
175 icon: str |
None | UndefinedType = UNDEFINED,
176 name: str | UndefinedType = UNDEFINED,
178 """Update name or icon of the category."""
179 old = self.
categoriescategories[scope][category_id]
180 changes: dict[str, Any] = {}
182 if icon
is not UNDEFINED
and icon != old.icon:
183 changes[
"icon"] = icon
185 if name
is not UNDEFINED
and name != old.name:
186 changes[
"name"] = name
192 changes[
"modified_at"] =
utcnow()
194 self.
hasshass.verify_event_loop_thread(
"category_registry.async_update")
195 new = self.
categoriescategories[scope][category_id] = dataclasses.replace(old, **changes)
197 self.async_schedule_save()
198 self.
hasshass.bus.async_fire_internal(
199 EVENT_CATEGORY_REGISTRY_UPDATED,
201 action=
"update", scope=scope, category_id=category_id
208 """Load the category registry."""
210 category_entries: dict[str, dict[str, CategoryEntry]] = {}
213 for scope, categories
in data[
"categories"].items():
214 category_entries[scope] = {
216 category_id=category[
"category_id"],
217 created_at=datetime.fromisoformat(category[
"created_at"]),
218 icon=category[
"icon"],
219 modified_at=datetime.fromisoformat(category[
"modified_at"]),
220 name=category[
"name"],
222 for category
in categories
229 """Return data of category registry to store in a file."""
234 "category_id": entry.category_id,
235 "created_at": entry.created_at.isoformat(),
237 "modified_at": entry.modified_at.isoformat(),
240 for entry
in entries.values()
242 for scope, entries
in self.
categoriescategories.items()
248 self, scope: str, name: str, category_id: str |
None =
None
250 """Ensure name is available within the scope."""
253 for category
in self.
categoriescategories[scope].values():
255 category.name.casefold() == name.casefold()
256 and category.category_id != category_id
258 raise ValueError(f
"The name '{name}' is already in use")
262 @singleton(DATA_REGISTRY)
264 """Get category registry."""
269 """Load category registry."""
270 assert DATA_REGISTRY
not in hass.data
CategoryRegistryStoreData _async_migrate_func(self, int old_major_version, int old_minor_version, dict[str, dict[str, list[dict[str, Any]]]] old_data)
CategoryEntry|None async_get_category(self, *str scope, str category_id)
Iterable[CategoryEntry] async_list_categories(self, *str scope)
CategoryEntry async_update(self, *str scope, str category_id, str|None|UndefinedType icon=UNDEFINED, str|UndefinedType name=UNDEFINED)
CategoryRegistryStoreData _data_to_save(self)
None __init__(self, HomeAssistant hass)
None _async_ensure_name_is_available(self, str scope, str name, str|None category_id=None)
None async_delete(self, *str scope, str category_id)
CategoryEntry async_create(self, *str name, str scope, str|None icon=None)
web.Response get(self, web.Request request, str config_key)
None async_load(HomeAssistant hass)
CategoryRegistry async_get(HomeAssistant hass)