2019-01-28 23:52:42 +00:00
|
|
|
"""Provide a way to connect devices to one physical location."""
|
2021-03-17 17:34:19 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2024-01-23 07:14:28 +00:00
|
|
|
from collections import UserDict
|
|
|
|
from collections.abc import Iterable, ValuesView
|
2024-01-22 13:45:27 +00:00
|
|
|
import dataclasses
|
2024-01-05 17:40:34 +00:00
|
|
|
from typing import Any, Literal, TypedDict, cast
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2024-02-18 13:57:15 +00:00
|
|
|
from homeassistant.core import HomeAssistant, callback
|
2020-12-01 11:45:56 +00:00
|
|
|
from homeassistant.util import slugify
|
2019-03-27 14:06:20 +00:00
|
|
|
|
2021-12-23 19:14:47 +00:00
|
|
|
from . import device_registry as dr, entity_registry as er
|
2022-05-17 16:45:57 +00:00
|
|
|
from .storage import Store
|
2021-10-27 17:11:05 +00:00
|
|
|
from .typing import UNDEFINED, UndefinedType
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
DATA_REGISTRY = "area_registry"
|
|
|
|
EVENT_AREA_REGISTRY_UPDATED = "area_registry_updated"
|
|
|
|
STORAGE_KEY = "core.area_registry"
|
2022-12-20 10:41:35 +00:00
|
|
|
STORAGE_VERSION_MAJOR = 1
|
2024-02-17 20:21:15 +00:00
|
|
|
STORAGE_VERSION_MINOR = 5
|
2019-01-28 23:52:42 +00:00
|
|
|
SAVE_DELAY = 10
|
|
|
|
|
|
|
|
|
2024-01-05 17:40:34 +00:00
|
|
|
class EventAreaRegistryUpdatedData(TypedDict):
|
|
|
|
"""EventAreaRegistryUpdated data."""
|
|
|
|
|
|
|
|
action: Literal["create", "remove", "update"]
|
|
|
|
area_id: str
|
|
|
|
|
|
|
|
|
2024-01-22 13:45:27 +00:00
|
|
|
@dataclasses.dataclass(frozen=True, kw_only=True, slots=True)
|
2019-01-28 23:52:42 +00:00
|
|
|
class AreaEntry:
|
|
|
|
"""Area Registry Entry."""
|
|
|
|
|
2024-01-22 13:45:27 +00:00
|
|
|
aliases: set[str]
|
2024-02-17 20:21:15 +00:00
|
|
|
floor_id: str | None
|
2024-01-24 18:11:03 +00:00
|
|
|
icon: str | None
|
2024-01-22 13:45:27 +00:00
|
|
|
id: str
|
|
|
|
name: str
|
|
|
|
normalized_name: str
|
|
|
|
picture: str | None
|
2019-01-28 23:52:42 +00:00
|
|
|
|
|
|
|
|
2024-01-23 07:14:28 +00:00
|
|
|
class AreaRegistryItems(UserDict[str, AreaEntry]):
|
|
|
|
"""Container for area registry items, maps area id -> entry.
|
|
|
|
|
|
|
|
Maintains an additional index:
|
|
|
|
- normalized name -> entry
|
|
|
|
"""
|
|
|
|
|
|
|
|
def __init__(self) -> None:
|
|
|
|
"""Initialize the container."""
|
|
|
|
super().__init__()
|
|
|
|
self._normalized_names: dict[str, AreaEntry] = {}
|
|
|
|
|
|
|
|
def values(self) -> ValuesView[AreaEntry]:
|
|
|
|
"""Return the underlying values to avoid __iter__ overhead."""
|
|
|
|
return self.data.values()
|
|
|
|
|
|
|
|
def __setitem__(self, key: str, entry: AreaEntry) -> None:
|
|
|
|
"""Add an item."""
|
|
|
|
data = self.data
|
|
|
|
normalized_name = normalize_area_name(entry.name)
|
|
|
|
|
|
|
|
if key in data:
|
|
|
|
old_entry = data[key]
|
|
|
|
if (
|
|
|
|
normalized_name != old_entry.normalized_name
|
|
|
|
and normalized_name in self._normalized_names
|
|
|
|
):
|
|
|
|
raise ValueError(
|
|
|
|
f"The name {entry.name} ({normalized_name}) is already in use"
|
|
|
|
)
|
|
|
|
del self._normalized_names[old_entry.normalized_name]
|
|
|
|
data[key] = entry
|
|
|
|
self._normalized_names[normalized_name] = entry
|
|
|
|
|
|
|
|
def __delitem__(self, key: str) -> None:
|
|
|
|
"""Remove an item."""
|
|
|
|
entry = self[key]
|
|
|
|
normalized_name = normalize_area_name(entry.name)
|
|
|
|
del self._normalized_names[normalized_name]
|
|
|
|
super().__delitem__(key)
|
|
|
|
|
|
|
|
def get_area_by_name(self, name: str) -> AreaEntry | None:
|
|
|
|
"""Get area by name."""
|
|
|
|
return self._normalized_names.get(normalize_area_name(name))
|
|
|
|
|
|
|
|
|
2022-12-21 07:44:44 +00:00
|
|
|
class AreaRegistryStore(Store[dict[str, list[dict[str, Any]]]]):
|
2022-12-20 10:41:35 +00:00
|
|
|
"""Store area registry data."""
|
|
|
|
|
|
|
|
async def _async_migrate_func(
|
|
|
|
self,
|
|
|
|
old_major_version: int,
|
|
|
|
old_minor_version: int,
|
2022-12-21 07:44:44 +00:00
|
|
|
old_data: dict[str, list[dict[str, Any]]],
|
2022-12-20 10:41:35 +00:00
|
|
|
) -> dict[str, Any]:
|
|
|
|
"""Migrate to the new version."""
|
|
|
|
if old_major_version < 2:
|
|
|
|
if old_minor_version < 2:
|
|
|
|
# Version 1.2 implements migration and freezes the available keys
|
|
|
|
for area in old_data["areas"]:
|
|
|
|
# Populate keys which were introduced before version 1.2
|
|
|
|
area.setdefault("picture", None)
|
|
|
|
|
2022-12-21 07:44:44 +00:00
|
|
|
if old_minor_version < 3:
|
|
|
|
# Version 1.3 adds aliases
|
|
|
|
for area in old_data["areas"]:
|
|
|
|
area["aliases"] = []
|
|
|
|
|
2024-01-24 18:11:03 +00:00
|
|
|
if old_minor_version < 4:
|
|
|
|
# Version 1.4 adds icon
|
|
|
|
for area in old_data["areas"]:
|
|
|
|
area["icon"] = None
|
|
|
|
|
2024-02-17 20:21:15 +00:00
|
|
|
if old_minor_version < 5:
|
|
|
|
# Version 1.5 adds floor_id
|
|
|
|
for area in old_data["areas"]:
|
|
|
|
area["floor_id"] = None
|
|
|
|
|
2022-12-20 10:41:35 +00:00
|
|
|
if old_major_version > 1:
|
|
|
|
raise NotImplementedError
|
|
|
|
return old_data
|
|
|
|
|
|
|
|
|
2019-01-28 23:52:42 +00:00
|
|
|
class AreaRegistry:
|
|
|
|
"""Class to hold a registry of areas."""
|
|
|
|
|
2024-01-23 07:14:28 +00:00
|
|
|
areas: AreaRegistryItems
|
|
|
|
_area_data: dict[str, AreaEntry]
|
|
|
|
|
2021-03-27 11:55:24 +00:00
|
|
|
def __init__(self, hass: HomeAssistant) -> None:
|
2019-01-28 23:52:42 +00:00
|
|
|
"""Initialize the area registry."""
|
|
|
|
self.hass = hass
|
2022-12-20 10:41:35 +00:00
|
|
|
self._store = AreaRegistryStore(
|
|
|
|
hass,
|
|
|
|
STORAGE_VERSION_MAJOR,
|
|
|
|
STORAGE_KEY,
|
|
|
|
atomic_writes=True,
|
|
|
|
minor_version=STORAGE_VERSION_MINOR,
|
2022-07-09 20:32:57 +00:00
|
|
|
)
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2019-03-04 17:51:12 +00:00
|
|
|
@callback
|
2021-03-17 17:34:19 +00:00
|
|
|
def async_get_area(self, area_id: str) -> AreaEntry | None:
|
2024-01-23 07:14:28 +00:00
|
|
|
"""Get area by id.
|
|
|
|
|
|
|
|
We retrieve the DeviceEntry from the underlying dict to avoid
|
|
|
|
the overhead of the UserDict __getitem__.
|
|
|
|
"""
|
|
|
|
return self._area_data.get(area_id)
|
2019-03-04 17:51:12 +00:00
|
|
|
|
2021-02-20 05:34:33 +00:00
|
|
|
@callback
|
2021-03-17 17:34:19 +00:00
|
|
|
def async_get_area_by_name(self, name: str) -> AreaEntry | None:
|
2021-02-20 05:34:33 +00:00
|
|
|
"""Get area by name."""
|
2024-01-23 07:14:28 +00:00
|
|
|
return self.areas.get_area_by_name(name)
|
2021-02-20 05:34:33 +00:00
|
|
|
|
2019-01-28 23:52:42 +00:00
|
|
|
@callback
|
2019-02-07 21:34:14 +00:00
|
|
|
def async_list_areas(self) -> Iterable[AreaEntry]:
|
2019-01-28 23:52:42 +00:00
|
|
|
"""Get all areas."""
|
|
|
|
return self.areas.values()
|
|
|
|
|
2021-02-20 05:34:33 +00:00
|
|
|
@callback
|
|
|
|
def async_get_or_create(self, name: str) -> AreaEntry:
|
|
|
|
"""Get or create an area."""
|
2021-10-31 17:32:17 +00:00
|
|
|
if area := self.async_get_area_by_name(name):
|
2021-02-20 05:34:33 +00:00
|
|
|
return area
|
|
|
|
return self.async_create(name)
|
|
|
|
|
2019-01-28 23:52:42 +00:00
|
|
|
@callback
|
2022-12-21 07:44:44 +00:00
|
|
|
def async_create(
|
|
|
|
self,
|
|
|
|
name: str,
|
|
|
|
*,
|
|
|
|
aliases: set[str] | None = None,
|
2024-02-17 20:21:15 +00:00
|
|
|
floor_id: str | None = None,
|
2024-01-24 18:11:03 +00:00
|
|
|
icon: str | None = None,
|
2022-12-21 07:44:44 +00:00
|
|
|
picture: str | None = None,
|
|
|
|
) -> AreaEntry:
|
2019-01-28 23:52:42 +00:00
|
|
|
"""Create a new area."""
|
2021-02-20 05:34:33 +00:00
|
|
|
normalized_name = normalize_area_name(name)
|
|
|
|
|
|
|
|
if self.async_get_area_by_name(name):
|
|
|
|
raise ValueError(f"The name {name} ({normalized_name}) is already in use")
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2024-01-22 13:45:27 +00:00
|
|
|
area_id = self._generate_area_id(name)
|
2022-12-21 07:44:44 +00:00
|
|
|
area = AreaEntry(
|
2024-01-22 13:45:27 +00:00
|
|
|
aliases=aliases or set(),
|
2024-02-17 20:21:15 +00:00
|
|
|
floor_id=floor_id,
|
2024-01-24 18:11:03 +00:00
|
|
|
icon=icon,
|
2024-01-22 13:45:27 +00:00
|
|
|
id=area_id,
|
|
|
|
name=name,
|
|
|
|
normalized_name=normalized_name,
|
|
|
|
picture=picture,
|
2022-12-21 07:44:44 +00:00
|
|
|
)
|
2020-12-01 11:45:56 +00:00
|
|
|
assert area.id is not None
|
2019-01-28 23:52:42 +00:00
|
|
|
self.areas[area.id] = area
|
2020-12-01 11:45:56 +00:00
|
|
|
self.async_schedule_save()
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.bus.async_fire(
|
2020-12-01 11:45:56 +00:00
|
|
|
EVENT_AREA_REGISTRY_UPDATED, {"action": "create", "area_id": area.id}
|
2019-07-31 19:25:30 +00:00
|
|
|
)
|
2020-12-01 11:45:56 +00:00
|
|
|
return area
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2021-02-12 16:00:35 +00:00
|
|
|
@callback
|
|
|
|
def async_delete(self, area_id: str) -> None:
|
2019-01-28 23:52:42 +00:00
|
|
|
"""Delete area."""
|
2021-02-12 16:00:35 +00:00
|
|
|
device_registry = dr.async_get(self.hass)
|
|
|
|
entity_registry = er.async_get(self.hass)
|
2019-01-28 23:52:42 +00:00
|
|
|
device_registry.async_clear_area_id(area_id)
|
2020-10-24 19:25:28 +00:00
|
|
|
entity_registry.async_clear_area_id(area_id)
|
2019-01-28 23:52:42 +00:00
|
|
|
|
|
|
|
del self.areas[area_id]
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.bus.async_fire(
|
|
|
|
EVENT_AREA_REGISTRY_UPDATED, {"action": "remove", "area_id": area_id}
|
|
|
|
)
|
2019-05-08 03:04:57 +00:00
|
|
|
|
2019-01-28 23:52:42 +00:00
|
|
|
self.async_schedule_save()
|
|
|
|
|
|
|
|
@callback
|
2021-10-27 17:11:05 +00:00
|
|
|
def async_update(
|
|
|
|
self,
|
|
|
|
area_id: str,
|
2022-12-21 07:44:44 +00:00
|
|
|
*,
|
|
|
|
aliases: set[str] | UndefinedType = UNDEFINED,
|
2024-02-17 20:21:15 +00:00
|
|
|
floor_id: str | None | UndefinedType = UNDEFINED,
|
2024-01-24 18:11:03 +00:00
|
|
|
icon: str | None | UndefinedType = UNDEFINED,
|
2021-10-27 17:11:05 +00:00
|
|
|
name: str | UndefinedType = UNDEFINED,
|
|
|
|
picture: str | None | UndefinedType = UNDEFINED,
|
|
|
|
) -> AreaEntry:
|
2019-05-08 03:04:57 +00:00
|
|
|
"""Update name of area."""
|
2022-12-21 07:44:44 +00:00
|
|
|
updated = self._async_update(
|
2024-01-24 18:11:03 +00:00
|
|
|
area_id,
|
|
|
|
aliases=aliases,
|
2024-02-17 20:21:15 +00:00
|
|
|
floor_id=floor_id,
|
2024-01-24 18:11:03 +00:00
|
|
|
icon=icon,
|
|
|
|
name=name,
|
|
|
|
picture=picture,
|
2022-12-21 07:44:44 +00:00
|
|
|
)
|
2019-07-31 19:25:30 +00:00
|
|
|
self.hass.bus.async_fire(
|
|
|
|
EVENT_AREA_REGISTRY_UPDATED, {"action": "update", "area_id": area_id}
|
|
|
|
)
|
2019-05-08 03:04:57 +00:00
|
|
|
return updated
|
|
|
|
|
|
|
|
@callback
|
2021-10-27 17:11:05 +00:00
|
|
|
def _async_update(
|
|
|
|
self,
|
|
|
|
area_id: str,
|
2022-12-21 07:44:44 +00:00
|
|
|
*,
|
|
|
|
aliases: set[str] | UndefinedType = UNDEFINED,
|
2024-02-17 20:21:15 +00:00
|
|
|
floor_id: str | None | UndefinedType = UNDEFINED,
|
2024-01-24 18:11:03 +00:00
|
|
|
icon: str | None | UndefinedType = UNDEFINED,
|
2021-10-27 17:11:05 +00:00
|
|
|
name: str | UndefinedType = UNDEFINED,
|
|
|
|
picture: str | None | UndefinedType = UNDEFINED,
|
|
|
|
) -> AreaEntry:
|
2019-01-28 23:52:42 +00:00
|
|
|
"""Update name of area."""
|
|
|
|
old = self.areas[area_id]
|
|
|
|
|
2022-12-21 07:44:44 +00:00
|
|
|
new_values = {}
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2022-12-21 07:44:44 +00:00
|
|
|
for attr_name, value in (
|
|
|
|
("aliases", aliases),
|
2024-01-24 18:11:03 +00:00
|
|
|
("icon", icon),
|
2022-12-21 07:44:44 +00:00
|
|
|
("picture", picture),
|
2024-02-17 20:21:15 +00:00
|
|
|
("floor_id", floor_id),
|
2022-12-21 07:44:44 +00:00
|
|
|
):
|
|
|
|
if value is not UNDEFINED and value != getattr(old, attr_name):
|
|
|
|
new_values[attr_name] = value
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2022-04-10 21:03:01 +00:00
|
|
|
if name is not UNDEFINED and name != old.name:
|
2022-12-21 07:44:44 +00:00
|
|
|
new_values["name"] = name
|
2024-01-23 07:14:28 +00:00
|
|
|
new_values["normalized_name"] = normalize_area_name(name)
|
2021-10-27 17:11:05 +00:00
|
|
|
|
2022-12-21 07:44:44 +00:00
|
|
|
if not new_values:
|
2021-10-27 17:11:05 +00:00
|
|
|
return old
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2024-01-22 13:45:27 +00:00
|
|
|
new = self.areas[area_id] = dataclasses.replace(old, **new_values) # type: ignore[arg-type]
|
2021-02-20 05:34:33 +00:00
|
|
|
|
2019-01-28 23:52:42 +00:00
|
|
|
self.async_schedule_save()
|
|
|
|
return new
|
|
|
|
|
|
|
|
async def async_load(self) -> None:
|
|
|
|
"""Load the area registry."""
|
2024-02-17 20:21:15 +00:00
|
|
|
self._async_setup_cleanup()
|
|
|
|
|
2019-01-28 23:52:42 +00:00
|
|
|
data = await self._store.async_load()
|
|
|
|
|
2024-01-23 07:14:28 +00:00
|
|
|
areas = AreaRegistryItems()
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2022-07-09 20:32:57 +00:00
|
|
|
if data is not None:
|
2019-07-31 19:25:30 +00:00
|
|
|
for area in data["areas"]:
|
2022-07-09 20:32:57 +00:00
|
|
|
assert area["name"] is not None and area["id"] is not None
|
2021-02-20 05:34:33 +00:00
|
|
|
normalized_name = normalize_area_name(area["name"])
|
|
|
|
areas[area["id"]] = AreaEntry(
|
2022-12-21 07:44:44 +00:00
|
|
|
aliases=set(area["aliases"]),
|
2024-02-17 20:21:15 +00:00
|
|
|
floor_id=area["floor_id"],
|
2024-01-24 18:11:03 +00:00
|
|
|
icon=area["icon"],
|
2021-10-27 17:11:05 +00:00
|
|
|
id=area["id"],
|
2022-12-20 10:41:35 +00:00
|
|
|
name=area["name"],
|
2021-10-27 17:11:05 +00:00
|
|
|
normalized_name=normalized_name,
|
2022-12-20 10:41:35 +00:00
|
|
|
picture=area["picture"],
|
2021-02-20 05:34:33 +00:00
|
|
|
)
|
2019-01-28 23:52:42 +00:00
|
|
|
|
|
|
|
self.areas = areas
|
2024-01-23 07:14:28 +00:00
|
|
|
self._area_data = areas.data
|
2019-01-28 23:52:42 +00:00
|
|
|
|
|
|
|
@callback
|
|
|
|
def async_schedule_save(self) -> None:
|
|
|
|
"""Schedule saving the area registry."""
|
|
|
|
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
|
|
|
|
|
|
|
|
@callback
|
2022-12-21 07:44:44 +00:00
|
|
|
def _data_to_save(self) -> dict[str, list[dict[str, Any]]]:
|
2019-01-28 23:52:42 +00:00
|
|
|
"""Return data of area registry to store in a file."""
|
|
|
|
data = {}
|
|
|
|
|
2019-07-31 19:25:30 +00:00
|
|
|
data["areas"] = [
|
2022-12-21 07:44:44 +00:00
|
|
|
{
|
|
|
|
"aliases": list(entry.aliases),
|
2024-02-17 20:21:15 +00:00
|
|
|
"floor_id": entry.floor_id,
|
2024-01-24 18:11:03 +00:00
|
|
|
"icon": entry.icon,
|
2022-12-21 07:44:44 +00:00
|
|
|
"id": entry.id,
|
2024-01-24 18:11:03 +00:00
|
|
|
"name": entry.name,
|
2022-12-21 07:44:44 +00:00
|
|
|
"picture": entry.picture,
|
|
|
|
}
|
2021-02-20 05:34:33 +00:00
|
|
|
for entry in self.areas.values()
|
2019-01-28 23:52:42 +00:00
|
|
|
]
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
2024-01-22 13:45:27 +00:00
|
|
|
def _generate_area_id(self, name: str) -> str:
|
|
|
|
"""Generate area ID."""
|
|
|
|
suggestion = suggestion_base = slugify(name)
|
|
|
|
tries = 1
|
|
|
|
while suggestion in self.areas:
|
|
|
|
tries += 1
|
|
|
|
suggestion = f"{suggestion_base}_{tries}"
|
|
|
|
return suggestion
|
|
|
|
|
2024-02-17 20:21:15 +00:00
|
|
|
@callback
|
|
|
|
def _async_setup_cleanup(self) -> None:
|
|
|
|
"""Set up the area registry cleanup."""
|
|
|
|
# pylint: disable-next=import-outside-toplevel
|
|
|
|
from . import floor_registry as fr # Circular dependency
|
|
|
|
|
|
|
|
@callback
|
2024-02-18 13:57:15 +00:00
|
|
|
def _floor_removed_from_registry_filter(
|
|
|
|
event: fr.EventFloorRegistryUpdated,
|
|
|
|
) -> bool:
|
2024-02-17 20:21:15 +00:00
|
|
|
"""Filter all except for the remove action from floor registry events."""
|
2024-02-18 13:57:15 +00:00
|
|
|
return event.data["action"] == "remove"
|
2024-02-17 20:21:15 +00:00
|
|
|
|
|
|
|
@callback
|
2024-02-18 13:57:15 +00:00
|
|
|
def _handle_floor_registry_update(event: fr.EventFloorRegistryUpdated) -> None:
|
2024-02-17 20:21:15 +00:00
|
|
|
"""Update areas that are associated with a floor that has been removed."""
|
|
|
|
floor_id = event.data["floor_id"]
|
|
|
|
for area_id, area in self.areas.items():
|
|
|
|
if floor_id == area.floor_id:
|
|
|
|
self.async_update(area_id, floor_id=None)
|
|
|
|
|
|
|
|
self.hass.bus.async_listen(
|
|
|
|
event_type=fr.EVENT_FLOOR_REGISTRY_UPDATED,
|
2024-02-18 13:57:15 +00:00
|
|
|
event_filter=_floor_removed_from_registry_filter, # type: ignore[arg-type]
|
|
|
|
listener=_handle_floor_registry_update, # type: ignore[arg-type]
|
2024-02-17 20:21:15 +00:00
|
|
|
)
|
|
|
|
|
2019-01-28 23:52:42 +00:00
|
|
|
|
2021-02-11 16:36:19 +00:00
|
|
|
@callback
|
2021-03-27 11:55:24 +00:00
|
|
|
def async_get(hass: HomeAssistant) -> AreaRegistry:
|
2021-02-11 16:36:19 +00:00
|
|
|
"""Get area registry."""
|
|
|
|
return cast(AreaRegistry, hass.data[DATA_REGISTRY])
|
2019-03-27 14:06:20 +00:00
|
|
|
|
|
|
|
|
2021-03-27 11:55:24 +00:00
|
|
|
async def async_load(hass: HomeAssistant) -> None:
|
2021-02-11 16:36:19 +00:00
|
|
|
"""Load area registry."""
|
|
|
|
assert DATA_REGISTRY not in hass.data
|
|
|
|
hass.data[DATA_REGISTRY] = AreaRegistry(hass)
|
|
|
|
await hass.data[DATA_REGISTRY].async_load()
|
2019-01-28 23:52:42 +00:00
|
|
|
|
|
|
|
|
2024-02-17 20:21:15 +00:00
|
|
|
@callback
|
|
|
|
def async_entries_for_floor(registry: AreaRegistry, floor_id: str) -> list[AreaEntry]:
|
|
|
|
"""Return entries that match an floor."""
|
|
|
|
return [area for area in registry.areas.values() if floor_id == area.floor_id]
|
|
|
|
|
|
|
|
|
2021-02-20 05:34:33 +00:00
|
|
|
def normalize_area_name(area_name: str) -> str:
|
|
|
|
"""Normalize an area name by removing whitespace and case folding."""
|
|
|
|
return area_name.casefold().replace(" ", "")
|