group.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. # -*- coding: utf-8 -*-
  2. """
  3. Satellite-Gruppierung.
  4. Ermöglicht die Organisation von Satellites in logischen Gruppen
  5. für koordinierte Aktionen und Verwaltung.
  6. """
  7. from __future__ import annotations
  8. from dataclasses import dataclass, field
  9. from datetime import datetime
  10. from enum import IntEnum, auto
  11. from typing import Any, Callable, Iterator, TYPE_CHECKING
  12. if TYPE_CHECKING:
  13. from trixy_core.satellite.satellite import Satellite
  14. class GroupType(IntEnum):
  15. """Typ einer Satellite-Gruppe."""
  16. ROOM = auto() # Raum-basierte Gruppe
  17. ZONE = auto() # Zone/Bereich
  18. CUSTOM = auto() # Benutzerdefiniert
  19. DYNAMIC = auto() # Dynamisch basierend auf Kriterien
  20. @dataclass
  21. class GroupConfig:
  22. """
  23. Konfiguration einer Gruppe.
  24. Attributes:
  25. auto_add: Satellites automatisch hinzufügen bei Match
  26. auto_remove: Satellites automatisch entfernen wenn nicht mehr passend
  27. max_members: Maximale Anzahl Mitglieder (0 = unbegrenzt)
  28. priority: Priorität der Gruppe (höher = wichtiger)
  29. """
  30. auto_add: bool = True
  31. auto_remove: bool = True
  32. max_members: int = 0
  33. priority: int = 0
  34. class SatelliteGroup:
  35. """
  36. Logische Gruppe von Satellites.
  37. Ermöglicht koordinierte Aktionen auf mehreren Satellites.
  38. Example:
  39. # Gruppe erstellen
  40. group = SatelliteGroup(
  41. group_id="wohnbereich",
  42. name="Wohnbereich",
  43. group_type=GroupType.ZONE
  44. )
  45. # Satellites hinzufügen
  46. group.add(satellite1)
  47. group.add(satellite2)
  48. # Aktion auf allen Satellites
  49. for satellite in group:
  50. await satellite.speak("Willkommen")
  51. # Filtern
  52. connected = group.filter(lambda s: s.is_connected)
  53. """
  54. def __init__(
  55. self,
  56. group_id: str,
  57. name: str = "",
  58. group_type: GroupType = GroupType.CUSTOM,
  59. config: GroupConfig | None = None,
  60. criteria: Callable[["Satellite"], bool] | None = None,
  61. ) -> None:
  62. """
  63. Initialisiert eine Satellite-Gruppe.
  64. Args:
  65. group_id: Eindeutige Gruppen-ID
  66. name: Anzeigename
  67. group_type: Typ der Gruppe
  68. config: Gruppenkonfiguration
  69. criteria: Optionale Funktion zur automatischen Zuordnung
  70. """
  71. self._id = group_id
  72. self._name = name or group_id
  73. self._type = group_type
  74. self._config = config or GroupConfig()
  75. self._criteria = criteria
  76. self._members: dict[str, "Satellite"] = {}
  77. self._created_at = datetime.now()
  78. self._metadata: dict[str, Any] = {}
  79. @property
  80. def id(self) -> str:
  81. """Gruppen-ID."""
  82. return self._id
  83. @property
  84. def name(self) -> str:
  85. """Anzeigename."""
  86. return self._name
  87. @name.setter
  88. def name(self, value: str) -> None:
  89. self._name = value
  90. @property
  91. def group_type(self) -> GroupType:
  92. """Gruppentyp."""
  93. return self._type
  94. @property
  95. def config(self) -> GroupConfig:
  96. """Gruppenkonfiguration."""
  97. return self._config
  98. @property
  99. def size(self) -> int:
  100. """Anzahl Mitglieder."""
  101. return len(self._members)
  102. @property
  103. def is_empty(self) -> bool:
  104. """Prüft, ob die Gruppe leer ist."""
  105. return len(self._members) == 0
  106. @property
  107. def metadata(self) -> dict[str, Any]:
  108. """Zusätzliche Metadaten."""
  109. return self._metadata
  110. def add(self, satellite: "Satellite") -> bool:
  111. """
  112. Fügt einen Satellite zur Gruppe hinzu.
  113. Args:
  114. satellite: Der hinzuzufügende Satellite
  115. Returns:
  116. True wenn hinzugefügt, False wenn bereits vorhanden oder Limit erreicht
  117. """
  118. if satellite.id in self._members:
  119. return False
  120. if self._config.max_members > 0 and self.size >= self._config.max_members:
  121. return False
  122. self._members[satellite.id] = satellite
  123. return True
  124. def remove(self, satellite_id: str) -> "Satellite | None":
  125. """
  126. Entfernt einen Satellite aus der Gruppe.
  127. Args:
  128. satellite_id: ID des zu entfernenden Satellites
  129. Returns:
  130. Der entfernte Satellite oder None
  131. """
  132. return self._members.pop(satellite_id, None)
  133. def contains(self, satellite_id: str) -> bool:
  134. """Prüft, ob ein Satellite in der Gruppe ist."""
  135. return satellite_id in self._members
  136. def get(self, satellite_id: str) -> "Satellite | None":
  137. """Gibt einen Satellite nach ID zurück."""
  138. return self._members.get(satellite_id)
  139. def get_all(self) -> list["Satellite"]:
  140. """Gibt alle Satellites der Gruppe zurück."""
  141. return list(self._members.values())
  142. def get_connected(self) -> list["Satellite"]:
  143. """Gibt alle verbundenen Satellites zurück."""
  144. return [s for s in self._members.values() if s.is_connected]
  145. def matches(self, satellite: "Satellite") -> bool:
  146. """
  147. Prüft, ob ein Satellite zu dieser Gruppe passt.
  148. Verwendet das Kriterium wenn definiert.
  149. Args:
  150. satellite: Der zu prüfende Satellite
  151. Returns:
  152. True wenn passend
  153. """
  154. if self._criteria is None:
  155. return False
  156. return self._criteria(satellite)
  157. def filter(
  158. self,
  159. predicate: Callable[["Satellite"], bool]
  160. ) -> list["Satellite"]:
  161. """
  162. Filtert Satellites nach einem Prädikat.
  163. Args:
  164. predicate: Filter-Funktion
  165. Returns:
  166. Liste der passenden Satellites
  167. """
  168. return [s for s in self._members.values() if predicate(s)]
  169. def clear(self) -> int:
  170. """
  171. Entfernt alle Satellites aus der Gruppe.
  172. Returns:
  173. Anzahl entfernter Satellites
  174. """
  175. count = len(self._members)
  176. self._members.clear()
  177. return count
  178. def __iter__(self) -> Iterator["Satellite"]:
  179. """Iterator über alle Satellites."""
  180. return iter(self._members.values())
  181. def __len__(self) -> int:
  182. """Anzahl Satellites."""
  183. return len(self._members)
  184. def __contains__(self, item: str | "Satellite") -> bool:
  185. """Prüft Mitgliedschaft."""
  186. if isinstance(item, str):
  187. return item in self._members
  188. return item.id in self._members
  189. def to_dict(self) -> dict[str, Any]:
  190. """Konvertiert zu Dictionary."""
  191. return {
  192. "id": self._id,
  193. "name": self._name,
  194. "type": self._type.name,
  195. "size": self.size,
  196. "members": list(self._members.keys()),
  197. "config": {
  198. "auto_add": self._config.auto_add,
  199. "auto_remove": self._config.auto_remove,
  200. "max_members": self._config.max_members,
  201. "priority": self._config.priority,
  202. },
  203. "created_at": self._created_at.isoformat(),
  204. "metadata": self._metadata,
  205. }
  206. def __repr__(self) -> str:
  207. return f"SatelliteGroup(id={self._id!r}, name={self._name!r}, size={self.size})"
  208. class GroupManager:
  209. """
  210. Verwaltet alle Satellite-Gruppen.
  211. Bietet zentrale Verwaltung von Gruppen und automatische
  212. Zuordnung von Satellites basierend auf Kriterien.
  213. Example:
  214. manager = GroupManager()
  215. # Raum-Gruppe erstellen
  216. manager.create_room_group("wohnzimmer", "Wohnzimmer")
  217. # Zone erstellen
  218. manager.create_zone_group(
  219. "erdgeschoss",
  220. "Erdgeschoss",
  221. criteria=lambda s: s.room_id in ["wohnzimmer", "küche", "flur"]
  222. )
  223. # Satellite automatisch zuordnen
  224. manager.auto_assign(satellite)
  225. # Alle Gruppen eines Satellites
  226. groups = manager.get_groups_for_satellite(satellite.id)
  227. """
  228. def __init__(self) -> None:
  229. """Initialisiert den GroupManager."""
  230. self._groups: dict[str, SatelliteGroup] = {}
  231. self._room_groups: dict[str, str] = {} # room_id -> group_id
  232. @property
  233. def count(self) -> int:
  234. """Anzahl Gruppen."""
  235. return len(self._groups)
  236. def create(
  237. self,
  238. group_id: str,
  239. name: str = "",
  240. group_type: GroupType = GroupType.CUSTOM,
  241. config: GroupConfig | None = None,
  242. criteria: Callable[["Satellite"], bool] | None = None,
  243. ) -> SatelliteGroup:
  244. """
  245. Erstellt eine neue Gruppe.
  246. Args:
  247. group_id: Eindeutige Gruppen-ID
  248. name: Anzeigename
  249. group_type: Typ der Gruppe
  250. config: Konfiguration
  251. criteria: Optionales Kriterium für Auto-Zuordnung
  252. Returns:
  253. Die erstellte Gruppe
  254. Raises:
  255. ValueError: Wenn ID bereits existiert
  256. """
  257. if group_id in self._groups:
  258. raise ValueError(f"Gruppe existiert bereits: {group_id}")
  259. group = SatelliteGroup(
  260. group_id=group_id,
  261. name=name,
  262. group_type=group_type,
  263. config=config,
  264. criteria=criteria,
  265. )
  266. self._groups[group_id] = group
  267. return group
  268. def create_room_group(self, room_id: str, name: str = "") -> SatelliteGroup:
  269. """
  270. Erstellt eine Raum-basierte Gruppe.
  271. Args:
  272. room_id: Raum-ID
  273. name: Anzeigename (Standard: room_id)
  274. Returns:
  275. Die erstellte Gruppe
  276. """
  277. group = self.create(
  278. group_id=f"room:{room_id}",
  279. name=name or room_id,
  280. group_type=GroupType.ROOM,
  281. criteria=lambda s: s.room_id == room_id,
  282. )
  283. self._room_groups[room_id] = group.id
  284. return group
  285. def create_zone_group(
  286. self,
  287. zone_id: str,
  288. name: str = "",
  289. criteria: Callable[["Satellite"], bool] | None = None,
  290. rooms: list[str] | None = None,
  291. ) -> SatelliteGroup:
  292. """
  293. Erstellt eine Zone-Gruppe.
  294. Args:
  295. zone_id: Zone-ID
  296. name: Anzeigename
  297. criteria: Optionales Kriterium
  298. rooms: Liste von Raum-IDs für automatisches Kriterium
  299. Returns:
  300. Die erstellte Gruppe
  301. """
  302. if criteria is None and rooms:
  303. criteria = lambda s: s.room_id in rooms
  304. return self.create(
  305. group_id=f"zone:{zone_id}",
  306. name=name or zone_id,
  307. group_type=GroupType.ZONE,
  308. criteria=criteria,
  309. )
  310. def get(self, group_id: str) -> SatelliteGroup | None:
  311. """Gibt eine Gruppe nach ID zurück."""
  312. return self._groups.get(group_id)
  313. def get_by_room(self, room_id: str) -> SatelliteGroup | None:
  314. """Gibt die Gruppe für einen Raum zurück."""
  315. group_id = self._room_groups.get(room_id)
  316. if group_id:
  317. return self._groups.get(group_id)
  318. return None
  319. def get_all(self) -> list[SatelliteGroup]:
  320. """Gibt alle Gruppen zurück."""
  321. return list(self._groups.values())
  322. def delete(self, group_id: str) -> SatelliteGroup | None:
  323. """
  324. Löscht eine Gruppe.
  325. Args:
  326. group_id: ID der zu löschenden Gruppe
  327. Returns:
  328. Die gelöschte Gruppe oder None
  329. """
  330. group = self._groups.pop(group_id, None)
  331. # Room-Mapping bereinigen
  332. if group and group.group_type == GroupType.ROOM:
  333. room_id = group_id.replace("room:", "")
  334. self._room_groups.pop(room_id, None)
  335. return group
  336. def add_to_group(self, group_id: str, satellite: "Satellite") -> bool:
  337. """
  338. Fügt einen Satellite zu einer Gruppe hinzu.
  339. Args:
  340. group_id: Gruppen-ID
  341. satellite: Der Satellite
  342. Returns:
  343. True wenn hinzugefügt
  344. """
  345. group = self._groups.get(group_id)
  346. if group is None:
  347. return False
  348. return group.add(satellite)
  349. def remove_from_group(self, group_id: str, satellite_id: str) -> bool:
  350. """
  351. Entfernt einen Satellite aus einer Gruppe.
  352. Args:
  353. group_id: Gruppen-ID
  354. satellite_id: Satellite-ID
  355. Returns:
  356. True wenn entfernt
  357. """
  358. group = self._groups.get(group_id)
  359. if group is None:
  360. return False
  361. return group.remove(satellite_id) is not None
  362. def auto_assign(self, satellite: "Satellite") -> list[str]:
  363. """
  364. Ordnet einen Satellite automatisch passenden Gruppen zu.
  365. Args:
  366. satellite: Der zuzuordnende Satellite
  367. Returns:
  368. Liste der Gruppen-IDs zu denen hinzugefügt wurde
  369. """
  370. assigned = []
  371. for group in self._groups.values():
  372. if group.config.auto_add and group.matches(satellite):
  373. if group.add(satellite):
  374. assigned.append(group.id)
  375. return assigned
  376. def auto_remove(self, satellite: "Satellite") -> list[str]:
  377. """
  378. Entfernt einen Satellite aus nicht mehr passenden Gruppen.
  379. Args:
  380. satellite: Der zu prüfende Satellite
  381. Returns:
  382. Liste der Gruppen-IDs aus denen entfernt wurde
  383. """
  384. removed = []
  385. for group in self._groups.values():
  386. if satellite.id in group:
  387. if group.config.auto_remove and not group.matches(satellite):
  388. group.remove(satellite.id)
  389. removed.append(group.id)
  390. return removed
  391. def get_groups_for_satellite(self, satellite_id: str) -> list[SatelliteGroup]:
  392. """
  393. Gibt alle Gruppen zurück, in denen ein Satellite ist.
  394. Args:
  395. satellite_id: Satellite-ID
  396. Returns:
  397. Liste der Gruppen
  398. """
  399. return [g for g in self._groups.values() if satellite_id in g]
  400. def find_groups(
  401. self,
  402. group_type: GroupType | None = None,
  403. min_size: int = 0,
  404. has_connected: bool = False,
  405. ) -> list[SatelliteGroup]:
  406. """
  407. Sucht Gruppen nach Kriterien.
  408. Args:
  409. group_type: Filter nach Typ
  410. min_size: Minimale Gruppengröße
  411. has_connected: Nur Gruppen mit verbundenen Satellites
  412. Returns:
  413. Liste passender Gruppen
  414. """
  415. result = []
  416. for group in self._groups.values():
  417. if group_type is not None and group.group_type != group_type:
  418. continue
  419. if group.size < min_size:
  420. continue
  421. if has_connected and not any(s.is_connected for s in group):
  422. continue
  423. result.append(group)
  424. return result
  425. def __iter__(self) -> Iterator[SatelliteGroup]:
  426. """Iterator über alle Gruppen."""
  427. return iter(self._groups.values())
  428. def __len__(self) -> int:
  429. """Anzahl Gruppen."""
  430. return len(self._groups)
  431. def __contains__(self, group_id: str) -> bool:
  432. """Prüft, ob eine Gruppe existiert."""
  433. return group_id in self._groups
  434. def __getitem__(self, group_id: str) -> SatelliteGroup:
  435. """Zugriff per ID."""
  436. if group_id not in self._groups:
  437. raise KeyError(f"Gruppe nicht gefunden: {group_id}")
  438. return self._groups[group_id]