satellites_view.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. # -*- coding: utf-8 -*-
  2. """
  3. Satellites-View: Liste und Verwaltung registrierter Satellites.
  4. Zeigt alle Satellites mit Verbindungsstatus. Buttons fuer
  5. Registration, Loeschen und Trennen. Bei Selektion wird
  6. die Satellite-Detailansicht geoeffnet.
  7. """
  8. from __future__ import annotations
  9. from textual.app import ComposeResult
  10. from textual.containers import Vertical, Horizontal
  11. from textual.widgets import Static, Button, ListView, ListItem, Label
  12. from trixy_core.tui.views.base import MainView
  13. class _SatelliteListItem(ListItem):
  14. """Einzelner Eintrag in der Satellite-Liste."""
  15. def __init__(self, sat_data: dict, **kwargs) -> None:
  16. super().__init__(**kwargs)
  17. self.sat_data = sat_data
  18. def compose(self) -> ComposeResult:
  19. alias = self.sat_data.get("alias", "Unbekannt")
  20. room = self.sat_data.get("room_id", "")
  21. connected = self.sat_data.get("connected", False)
  22. mac = self.sat_data.get("mac_address", "")
  23. iface_type = self.sat_data.get("interface_type", "")
  24. if connected:
  25. status = "[green]Verbunden[/green]"
  26. else:
  27. status = "[red]Getrennt[/red]"
  28. # Interface-Badge
  29. iface_badge = ""
  30. if connected and iface_type:
  31. iface_map = {
  32. "wifi": "[cyan]WLAN[/cyan]",
  33. "ethernet": "[green]LAN[/green]",
  34. "other": "[dim]?[/dim]",
  35. }
  36. iface_badge = f" {iface_map.get(iface_type, '')}"
  37. yield Label(
  38. f" {status}{iface_badge} [bold]{alias}[/bold] ({room}) [{mac}]"
  39. )
  40. class SatellitesView(MainView):
  41. """
  42. F3: Satellites-Ansicht (nur bei Server-Verbindung).
  43. Zeigt alle registrierten Satellites mit Verbindungsstatus.
  44. Bietet Aktionen fuer Registration, Loeschen und Trennen.
  45. """
  46. TITLE = "Satellites"
  47. VIEW_ID = "satellites"
  48. def __init__(self, **kwargs) -> None:
  49. super().__init__(**kwargs)
  50. self._satellites: list[dict] = []
  51. self._selected_index: int = -1
  52. self._registration_mode: bool = False
  53. self._refresh_counter: int = 0
  54. def compose(self) -> ComposeResult:
  55. # Status-Leiste
  56. yield Static("", id="sat-header")
  57. # Satellite-Liste
  58. yield ListView(id="sat-list")
  59. # Aktions-Buttons
  60. with Horizontal(id="sat-actions"):
  61. yield Button("Registrierung", id="sat-btn-register", variant="primary")
  62. yield Button("Trennen", id="sat-btn-disconnect", variant="warning")
  63. yield Button("Loeschen", id="sat-btn-delete", variant="error")
  64. # Status
  65. yield Static("", id="sat-status")
  66. async def on_activate(self) -> None:
  67. """Laedt die Satellite-Liste."""
  68. await self._load_satellites()
  69. async def refresh_data(self) -> None:
  70. """Periodisches Update (alle 10 Sekunden via 2s-Interval)."""
  71. self._refresh_counter += 1
  72. # Nur alle 5 Aufrufe (= 10 Sekunden bei 2s Interval)
  73. if self._refresh_counter % 5 == 0:
  74. await self._load_satellites()
  75. async def _load_satellites(self) -> None:
  76. """Laedt die Satellite-Liste vom Server."""
  77. if not self.connection:
  78. return
  79. try:
  80. response = await self.connection.request_satellites()
  81. except Exception:
  82. self._set_status("Fehler beim Laden der Satellites", error=True)
  83. return
  84. if response is None:
  85. return
  86. self._satellites = response.satellites
  87. self._registration_mode = response.registration_mode
  88. self._update_header(response)
  89. await self._update_list()
  90. def _update_header(self, response) -> None:
  91. """Aktualisiert die Kopfzeile."""
  92. header = self.query_one("#sat-header", Static)
  93. total = len(response.satellites)
  94. connected = sum(1 for s in response.satellites if s.get("connected", False))
  95. reg_status = ""
  96. if response.registration_mode:
  97. if response.permanent_mode:
  98. reg_status = " [yellow]Auto-Registrierung aktiv[/yellow]"
  99. else:
  100. reg_status = " [yellow]Registrierung aktiv[/yellow]"
  101. header.update(
  102. f"[bold]Satellites:[/bold] {connected}/{total} verbunden{reg_status}"
  103. )
  104. # Register-Button-Text anpassen
  105. try:
  106. btn = self.query_one("#sat-btn-register", Button)
  107. if self._registration_mode:
  108. btn.label = "Registrierung aus"
  109. btn.variant = "warning"
  110. else:
  111. btn.label = "Registrierung"
  112. btn.variant = "primary"
  113. except Exception:
  114. pass
  115. async def _update_list(self) -> None:
  116. """Aktualisiert die Satellite-Liste."""
  117. list_view = self.query_one("#sat-list", ListView)
  118. await list_view.clear()
  119. # Verbundene zuerst, dann getrennte
  120. sorted_sats = sorted(
  121. self._satellites,
  122. key=lambda s: (not s.get("connected", False), s.get("alias", "")),
  123. )
  124. for sat_data in sorted_sats:
  125. item = _SatelliteListItem(sat_data)
  126. await list_view.append(item)
  127. def _get_selected_satellite(self) -> dict | None:
  128. """Gibt den aktuell selektierten Satellite zurueck."""
  129. try:
  130. list_view = self.query_one("#sat-list", ListView)
  131. if list_view.highlighted_child is not None:
  132. item = list_view.highlighted_child
  133. if isinstance(item, _SatelliteListItem):
  134. return item.sat_data
  135. except Exception:
  136. pass
  137. return None
  138. async def on_list_view_selected(self, event: ListView.Selected) -> None:
  139. """Oeffnet die Detail-Ansicht fuer den selektierten Satellite."""
  140. if isinstance(event.item, _SatelliteListItem):
  141. sat_data = event.item.sat_data
  142. # SubMainView oeffnen via App
  143. try:
  144. app = self.app
  145. if hasattr(app, "open_satellite_detail"):
  146. await app.open_satellite_detail(sat_data)
  147. except Exception:
  148. pass
  149. async def on_button_pressed(self, event: Button.Pressed) -> None:
  150. """Verarbeitet Button-Klicks."""
  151. btn_id = event.button.id
  152. if not self.connection:
  153. return
  154. if btn_id == "sat-btn-register":
  155. await self._toggle_registration()
  156. elif btn_id == "sat-btn-disconnect":
  157. await self._disconnect_satellite()
  158. elif btn_id == "sat-btn-delete":
  159. await self._delete_satellite()
  160. async def _toggle_registration(self) -> None:
  161. """Schaltet den Registrierungsmodus um."""
  162. if self._registration_mode:
  163. action = "disable_registration"
  164. else:
  165. action = "enable_registration"
  166. result = await self.connection.satellite_action(action=action)
  167. if result and result.success:
  168. self._set_status(result.message)
  169. await self._load_satellites()
  170. else:
  171. error = result.error if result else "Unbekannter Fehler"
  172. self._set_status(f"Fehler: {error}", error=True)
  173. async def _disconnect_satellite(self) -> None:
  174. """Trennt den selektierten Satellite."""
  175. sat = self._get_selected_satellite()
  176. if not sat:
  177. self._set_status("Kein Satellite ausgewaehlt", error=True)
  178. return
  179. if not sat.get("connected", False):
  180. self._set_status("Satellite ist nicht verbunden", error=True)
  181. return
  182. result = await self.connection.satellite_action(
  183. action="disconnect",
  184. satellite_id=sat.get("satellite_id", ""),
  185. )
  186. if result and result.success:
  187. self._set_status(result.message)
  188. await self._load_satellites()
  189. else:
  190. error = result.error if result else "Unbekannter Fehler"
  191. self._set_status(f"Fehler: {error}", error=True)
  192. async def _delete_satellite(self) -> None:
  193. """Loescht die Registrierung des selektierten Satellites."""
  194. sat = self._get_selected_satellite()
  195. if not sat:
  196. self._set_status("Kein Satellite ausgewaehlt", error=True)
  197. return
  198. result = await self.connection.satellite_action(
  199. action="delete",
  200. satellite_id=sat.get("satellite_id", ""),
  201. )
  202. if result and result.success:
  203. self._set_status(result.message)
  204. await self._load_satellites()
  205. else:
  206. error = result.error if result else "Unbekannter Fehler"
  207. self._set_status(f"Fehler: {error}", error=True)
  208. def _set_status(self, text: str, error: bool = False) -> None:
  209. """Setzt die Statuszeile."""
  210. status = self.query_one("#sat-status", Static)
  211. if error:
  212. status.update(f"[red]{text}[/red]")
  213. else:
  214. status.update(f"[green]{text}[/green]")