Files
sensorpajen/src/sensorpajen/tui/app.py

219 lines
6.9 KiB
Python

from textual.app import App, ComposeResult
from textual.widgets import Header, Footer, TabbedContent, TabPane, DataTable
from textual.containers import Container
from textual import on
from ..discovery_manager import DiscoveryManager
from ..config import SensorConfig
from .modals import InputModal
class SensorpajenApp(App):
"""A Textual app to manage Bluetooth sensors."""
CSS = """
Screen {
background: $surface;
}
DataTable {
height: 1fr;
margin: 1;
}
#modal-container {
width: 50;
height: auto;
background: $panel;
border: thick $primary;
padding: 1;
align: center middle;
}
#modal-buttons {
margin-top: 1;
height: auto;
align: center middle;
}
#modal-buttons Button {
margin: 0 1;
}
"""
BINDINGS = [
("q", "quit", "Quit"),
("d", "toggle_dark", "Toggle dark mode"),
("r", "refresh", "Refresh data"),
("a", "approve", "Approve"),
("i", "ignore", "Ignore"),
("e", "edit", "Edit"),
("u", "unignore", "Unignore"),
("delete", "remove", "Remove"),
]
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.discovery_manager = DiscoveryManager()
self.sensor_config = SensorConfig()
def compose(self) -> ComposeResult:
"""Create child widgets for the app."""
yield Header()
with TabbedContent(initial="discovery"):
with TabPane("Discovery", id="discovery"):
yield DataTable(id="discovery-table", cursor_type="row")
with TabPane("Configured", id="configured"):
yield DataTable(id="configured-table", cursor_type="row")
with TabPane("Ignored", id="ignored"):
yield DataTable(id="ignored-table", cursor_type="row")
yield Footer()
def on_mount(self) -> None:
"""Handle app mount event."""
self.refresh_data()
def action_refresh(self) -> None:
"""Refresh all tables."""
self.refresh_data()
async def action_approve(self) -> None:
"""Approve the selected discovered sensor."""
if self.query_one(TabbedContent).active != "discovery":
return
table = self.query_one("#discovery-table", DataTable)
if table.cursor_row is None:
return
row = table.get_row_at(table.cursor_row)
mac = row[0]
default_name = row[1]
name = await self.push_screen(InputModal("Enter sensor name", initial_value=default_name))
if name:
self.sensor_config.add_sensor(mac, name)
self.discovery_manager.approve(mac)
self.notify(f"Approved {mac} as {name}")
self.refresh_data()
async def action_ignore(self) -> None:
"""Ignore the selected discovered sensor."""
if self.query_one(TabbedContent).active != "discovery":
return
table = self.query_one("#discovery-table", DataTable)
if table.cursor_row is None:
return
row = table.get_row_at(table.cursor_row)
mac = row[0]
reason = await self.push_screen(InputModal("Enter ignore reason (optional)"))
if reason is not None: # Allow empty string but not None (Cancel)
self.discovery_manager.ignore(mac, reason if reason else None)
self.notify(f"Ignored {mac}")
self.refresh_data()
async def action_edit(self) -> None:
"""Edit the selected configured sensor."""
if self.query_one(TabbedContent).active != "configured":
return
table = self.query_one("#configured-table", DataTable)
if table.cursor_row is None:
return
row = table.get_row_at(table.cursor_row)
mac = row[0]
current_name = row[1]
name = await self.push_screen(InputModal("Edit sensor name", initial_value=current_name))
if name:
self.sensor_config.add_sensor(mac, name)
self.notify(f"Updated {mac} to {name}")
self.refresh_data()
def action_remove(self) -> None:
"""Remove the selected configured sensor."""
if self.query_one(TabbedContent).active != "configured":
return
table = self.query_one("#configured-table", DataTable)
if table.cursor_row is None:
return
row = table.get_row_at(table.cursor_row)
mac = row[0]
self.sensor_config.remove_sensor(mac)
self.notify(f"Removed {mac}")
self.refresh_data()
def action_unignore(self) -> None:
"""Unignore the selected sensor."""
if self.query_one(TabbedContent).active != "ignored":
return
table = self.query_one("#ignored-table", DataTable)
if table.cursor_row is None:
return
row = table.get_row_at(table.cursor_row)
mac = row[0]
self.discovery_manager.unignore(mac)
self.notify(f"Unignored {mac}")
self.refresh_data()
def refresh_data(self) -> None:
"""Load data from managers and update tables."""
self._update_discovery_table()
self._update_configured_table()
self._update_ignored_table()
def _update_discovery_table(self) -> None:
table = self.query_one("#discovery-table", DataTable)
table.clear(columns=True)
table.add_columns("MAC", "Name", "RSSI", "Last Seen", "Count", "Temp", "Humidity")
sensors = self.discovery_manager.get_pending()
for s in sensors:
table.add_row(
s.mac,
s.name,
str(s.rssi),
s.last_seen.split("T")[1].split(".")[0], # Just time
str(s.count),
f"{s.sample_reading.get('temperature', 0):.1f}°C",
f"{s.sample_reading.get('humidity', 0)}%"
)
def _update_configured_table(self) -> None:
table = self.query_one("#configured-table", DataTable)
table.clear(columns=True)
table.add_columns("MAC", "Name")
for mac, name in self.sensor_config.sensors.items():
table.add_row(mac, name)
def _update_ignored_table(self) -> None:
table = self.query_one("#ignored-table", DataTable)
table.clear(columns=True)
table.add_columns("MAC", "Name", "Ignored At", "Reason")
sensors = self.discovery_manager.get_ignored()
for s in sensors:
table.add_row(
s.mac,
s.name,
s.ignored_at.split("T")[0] if s.ignored_at else "N/A",
s.ignore_reason or ""
)
def main():
app = SensorpajenApp()
app.run()
if __name__ == "__main__":
main()