219 lines
6.9 KiB
Python
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()
|