zone.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475
  1. # -*- coding: utf-8 -*-
  2. """
  3. Zone-Management für Satellites.
  4. Zonen sind hierarchische Gruppierungen von Räumen/Bereichen
  5. für koordinierte Aktionen über mehrere Räume hinweg.
  6. """
  7. from __future__ import annotations
  8. from dataclasses import dataclass, field
  9. from typing import Any, Iterator, TYPE_CHECKING
  10. if TYPE_CHECKING:
  11. from trixy_core.satellite.satellite import Satellite
  12. from trixy_core.satellite.groups.group import SatelliteGroup
  13. @dataclass
  14. class ZoneConfig:
  15. """
  16. Konfiguration einer Zone.
  17. Attributes:
  18. priority: Priorität der Zone (höher = wichtiger)
  19. fallback_zone: Fallback-Zone wenn keine Satellites verfügbar
  20. audio_sync: Audio zwischen Räumen synchronisieren
  21. propagate_commands: Befehle an alle Räume senden
  22. """
  23. priority: int = 0
  24. fallback_zone: str | None = None
  25. audio_sync: bool = False
  26. propagate_commands: bool = True
  27. class Zone:
  28. """
  29. Hierarchische Zone aus mehreren Räumen.
  30. Ermöglicht die Verwaltung von Bereichen wie "Erdgeschoss",
  31. "Obergeschoss", "Außenbereich" etc.
  32. Example:
  33. zone = Zone(
  34. zone_id="erdgeschoss",
  35. name="Erdgeschoss",
  36. rooms=["wohnzimmer", "küche", "flur"]
  37. )
  38. # Alle Satellites in der Zone
  39. satellites = zone.get_all_satellites()
  40. # Prüfen ob Raum in Zone
  41. if "wohnzimmer" in zone:
  42. print("Wohnzimmer ist im Erdgeschoss")
  43. """
  44. def __init__(
  45. self,
  46. zone_id: str,
  47. name: str = "",
  48. rooms: list[str] | None = None,
  49. config: ZoneConfig | None = None,
  50. parent_zone: "Zone | None" = None,
  51. ) -> None:
  52. """
  53. Initialisiert eine Zone.
  54. Args:
  55. zone_id: Eindeutige Zone-ID
  56. name: Anzeigename
  57. rooms: Liste der Raum-IDs
  58. config: Zone-Konfiguration
  59. parent_zone: Optionale übergeordnete Zone
  60. """
  61. self._id = zone_id
  62. self._name = name or zone_id
  63. self._rooms: set[str] = set(rooms or [])
  64. self._config = config or ZoneConfig()
  65. self._parent = parent_zone
  66. self._children: dict[str, "Zone"] = {}
  67. self._metadata: dict[str, Any] = {}
  68. # Gruppen-Referenzen (werden vom ZoneManager gesetzt)
  69. self._room_groups: dict[str, "SatelliteGroup"] = {}
  70. @property
  71. def id(self) -> str:
  72. """Zone-ID."""
  73. return self._id
  74. @property
  75. def name(self) -> str:
  76. """Anzeigename."""
  77. return self._name
  78. @name.setter
  79. def name(self, value: str) -> None:
  80. self._name = value
  81. @property
  82. def config(self) -> ZoneConfig:
  83. """Zone-Konfiguration."""
  84. return self._config
  85. @property
  86. def rooms(self) -> set[str]:
  87. """Räume in dieser Zone (nur direkte)."""
  88. return self._rooms.copy()
  89. @property
  90. def all_rooms(self) -> set[str]:
  91. """Alle Räume inklusive Kinder-Zonen."""
  92. result = self._rooms.copy()
  93. for child in self._children.values():
  94. result.update(child.all_rooms)
  95. return result
  96. @property
  97. def parent(self) -> "Zone | None":
  98. """Übergeordnete Zone."""
  99. return self._parent
  100. @property
  101. def children(self) -> list["Zone"]:
  102. """Untergeordnete Zonen."""
  103. return list(self._children.values())
  104. @property
  105. def is_root(self) -> bool:
  106. """Prüft, ob dies eine Root-Zone ist."""
  107. return self._parent is None
  108. @property
  109. def depth(self) -> int:
  110. """Tiefe in der Hierarchie (Root = 0)."""
  111. if self._parent is None:
  112. return 0
  113. return self._parent.depth + 1
  114. @property
  115. def metadata(self) -> dict[str, Any]:
  116. """Zusätzliche Metadaten."""
  117. return self._metadata
  118. def add_room(self, room_id: str) -> None:
  119. """Fügt einen Raum zur Zone hinzu."""
  120. self._rooms.add(room_id)
  121. def remove_room(self, room_id: str) -> bool:
  122. """
  123. Entfernt einen Raum aus der Zone.
  124. Returns:
  125. True wenn entfernt
  126. """
  127. if room_id in self._rooms:
  128. self._rooms.discard(room_id)
  129. self._room_groups.pop(room_id, None)
  130. return True
  131. return False
  132. def add_child(self, zone: "Zone") -> None:
  133. """Fügt eine Kinder-Zone hinzu."""
  134. zone._parent = self
  135. self._children[zone.id] = zone
  136. def remove_child(self, zone_id: str) -> "Zone | None":
  137. """Entfernt eine Kinder-Zone."""
  138. zone = self._children.pop(zone_id, None)
  139. if zone:
  140. zone._parent = None
  141. return zone
  142. def contains_room(self, room_id: str, recursive: bool = True) -> bool:
  143. """
  144. Prüft, ob ein Raum in der Zone ist.
  145. Args:
  146. room_id: Raum-ID
  147. recursive: Auch in Kinder-Zonen suchen
  148. Returns:
  149. True wenn enthalten
  150. """
  151. if room_id in self._rooms:
  152. return True
  153. if recursive:
  154. for child in self._children.values():
  155. if child.contains_room(room_id, recursive=True):
  156. return True
  157. return False
  158. def get_all_satellites(self) -> list["Satellite"]:
  159. """
  160. Gibt alle Satellites in der Zone zurück.
  161. Beinhaltet alle Räume und Kinder-Zonen.
  162. Returns:
  163. Liste aller Satellites
  164. """
  165. satellites: list["Satellite"] = []
  166. for group in self._room_groups.values():
  167. satellites.extend(group.get_all())
  168. for child in self._children.values():
  169. satellites.extend(child.get_all_satellites())
  170. return satellites
  171. def get_connected_satellites(self) -> list["Satellite"]:
  172. """Gibt alle verbundenen Satellites zurück."""
  173. return [s for s in self.get_all_satellites() if s.is_connected]
  174. def get_path(self) -> list["Zone"]:
  175. """
  176. Gibt den Pfad von der Root-Zone zu dieser Zone.
  177. Returns:
  178. Liste der Zonen von Root bis hier
  179. """
  180. path = [self]
  181. current = self._parent
  182. while current is not None:
  183. path.insert(0, current)
  184. current = current._parent
  185. return path
  186. def __contains__(self, item: str) -> bool:
  187. """Prüft, ob ein Raum in der Zone ist."""
  188. return self.contains_room(item, recursive=True)
  189. def __iter__(self) -> Iterator[str]:
  190. """Iterator über alle Räume."""
  191. return iter(self.all_rooms)
  192. def __len__(self) -> int:
  193. """Anzahl Räume (inklusive Kinder)."""
  194. return len(self.all_rooms)
  195. def to_dict(self) -> dict[str, Any]:
  196. """Konvertiert zu Dictionary."""
  197. return {
  198. "id": self._id,
  199. "name": self._name,
  200. "rooms": list(self._rooms),
  201. "all_rooms": list(self.all_rooms),
  202. "parent": self._parent.id if self._parent else None,
  203. "children": [c.id for c in self._children.values()],
  204. "depth": self.depth,
  205. "config": {
  206. "priority": self._config.priority,
  207. "fallback_zone": self._config.fallback_zone,
  208. "audio_sync": self._config.audio_sync,
  209. "propagate_commands": self._config.propagate_commands,
  210. },
  211. "satellite_count": len(self.get_all_satellites()),
  212. }
  213. def __repr__(self) -> str:
  214. return f"Zone(id={self._id!r}, name={self._name!r}, rooms={len(self._rooms)})"
  215. class ZoneManager:
  216. """
  217. Verwaltet alle Zonen.
  218. Bietet hierarchische Organisation von Zonen und
  219. automatische Zuordnung von Räumen.
  220. Example:
  221. manager = ZoneManager()
  222. # Root-Zone erstellen
  223. gesamt = manager.create("gesamt", "Gesamtes Haus")
  224. # Kind-Zonen
  225. eg = manager.create("eg", "Erdgeschoss", parent="gesamt")
  226. eg.add_room("wohnzimmer")
  227. eg.add_room("küche")
  228. og = manager.create("og", "Obergeschoss", parent="gesamt")
  229. og.add_room("schlafzimmer")
  230. og.add_room("bad")
  231. # Zone für Raum finden
  232. zone = manager.get_zone_for_room("wohnzimmer")
  233. """
  234. def __init__(self) -> None:
  235. """Initialisiert den ZoneManager."""
  236. self._zones: dict[str, Zone] = {}
  237. self._room_mapping: dict[str, str] = {} # room_id -> zone_id
  238. @property
  239. def count(self) -> int:
  240. """Anzahl Zonen."""
  241. return len(self._zones)
  242. def create(
  243. self,
  244. zone_id: str,
  245. name: str = "",
  246. rooms: list[str] | None = None,
  247. config: ZoneConfig | None = None,
  248. parent: str | None = None,
  249. ) -> Zone:
  250. """
  251. Erstellt eine neue Zone.
  252. Args:
  253. zone_id: Eindeutige Zone-ID
  254. name: Anzeigename
  255. rooms: Liste der Raum-IDs
  256. config: Zone-Konfiguration
  257. parent: ID der übergeordneten Zone
  258. Returns:
  259. Die erstellte Zone
  260. Raises:
  261. ValueError: Wenn ID bereits existiert oder Parent nicht gefunden
  262. """
  263. if zone_id in self._zones:
  264. raise ValueError(f"Zone existiert bereits: {zone_id}")
  265. parent_zone = None
  266. if parent:
  267. parent_zone = self._zones.get(parent)
  268. if parent_zone is None:
  269. raise ValueError(f"Parent-Zone nicht gefunden: {parent}")
  270. zone = Zone(
  271. zone_id=zone_id,
  272. name=name,
  273. rooms=rooms,
  274. config=config,
  275. parent_zone=parent_zone,
  276. )
  277. self._zones[zone_id] = zone
  278. if parent_zone is not None:
  279. parent_zone.add_child(zone)
  280. # Room-Mapping aktualisieren
  281. for room_id in zone.rooms:
  282. self._room_mapping[room_id] = zone_id
  283. return zone
  284. def get(self, zone_id: str) -> Zone | None:
  285. """Gibt eine Zone nach ID zurück."""
  286. return self._zones.get(zone_id)
  287. def delete(self, zone_id: str, recursive: bool = False) -> Zone | None:
  288. """
  289. Löscht eine Zone.
  290. Args:
  291. zone_id: ID der zu löschenden Zone
  292. recursive: Auch alle Kinder-Zonen löschen
  293. Returns:
  294. Die gelöschte Zone oder None
  295. """
  296. zone = self._zones.get(zone_id)
  297. if zone is None:
  298. return None
  299. if recursive:
  300. for child in list(zone.children):
  301. self.delete(child.id, recursive=True)
  302. # Aus Parent entfernen
  303. if zone.parent is not None:
  304. zone.parent.remove_child(zone_id)
  305. # Room-Mapping bereinigen
  306. for room_id in zone.rooms:
  307. if self._room_mapping.get(room_id) == zone_id:
  308. del self._room_mapping[room_id]
  309. return self._zones.pop(zone_id, None)
  310. def get_zone_for_room(self, room_id: str) -> Zone | None:
  311. """
  312. Gibt die Zone für einen Raum zurück.
  313. Args:
  314. room_id: Raum-ID
  315. Returns:
  316. Die Zone oder None
  317. """
  318. zone_id = self._room_mapping.get(room_id)
  319. if zone_id:
  320. return self._zones.get(zone_id)
  321. return None
  322. def get_root_zones(self) -> list[Zone]:
  323. """Gibt alle Root-Zonen zurück (ohne Parent)."""
  324. return [z for z in self._zones.values() if z.is_root]
  325. def get_all(self) -> list[Zone]:
  326. """Gibt alle Zonen zurück."""
  327. return list(self._zones.values())
  328. def move_room(self, room_id: str, target_zone_id: str) -> bool:
  329. """
  330. Verschiebt einen Raum in eine andere Zone.
  331. Args:
  332. room_id: Raum-ID
  333. target_zone_id: Ziel-Zone-ID
  334. Returns:
  335. True wenn erfolgreich
  336. """
  337. target = self._zones.get(target_zone_id)
  338. if target is None:
  339. return False
  340. # Aus alter Zone entfernen
  341. old_zone_id = self._room_mapping.get(room_id)
  342. if old_zone_id:
  343. old_zone = self._zones.get(old_zone_id)
  344. if old_zone:
  345. old_zone.remove_room(room_id)
  346. # In neue Zone einfügen
  347. target.add_room(room_id)
  348. self._room_mapping[room_id] = target_zone_id
  349. return True
  350. def get_hierarchy(self) -> dict[str, Any]:
  351. """
  352. Gibt die Zone-Hierarchie als verschachteltes Dictionary zurück.
  353. Returns:
  354. Hierarchie-Dictionary
  355. """
  356. def build_tree(zone: Zone) -> dict[str, Any]:
  357. return {
  358. "id": zone.id,
  359. "name": zone.name,
  360. "rooms": list(zone.rooms),
  361. "children": [build_tree(c) for c in zone.children],
  362. }
  363. return {
  364. "root_zones": [build_tree(z) for z in self.get_root_zones()]
  365. }
  366. def __iter__(self) -> Iterator[Zone]:
  367. """Iterator über alle Zonen."""
  368. return iter(self._zones.values())
  369. def __len__(self) -> int:
  370. """Anzahl Zonen."""
  371. return len(self._zones)
  372. def __contains__(self, zone_id: str) -> bool:
  373. """Prüft, ob eine Zone existiert."""
  374. return zone_id in self._zones
  375. def __getitem__(self, zone_id: str) -> Zone:
  376. """Zugriff per ID."""
  377. if zone_id not in self._zones:
  378. raise KeyError(f"Zone nicht gefunden: {zone_id}")
  379. return self._zones[zone_id]