196 lines
7.8 KiB
Python
196 lines
7.8 KiB
Python
from azure.iot.hub import IoTHubRegistryManager
|
|
from azure.iot.hub.protocol.models import QuerySpecification, Module
|
|
from azure.iot.hub.models import CloudToDeviceMethod, CloudToDeviceMethodResult
|
|
from dotenv import load_dotenv
|
|
from isight_device import iSightDevice
|
|
|
|
from textual.app import App, ComposeResult
|
|
from textual.widgets import Header, Footer, ListView, ListItem, Label, Input, Button, Static, DataTable
|
|
from textual.containers import Vertical, VerticalScroll
|
|
from textual.binding import Binding
|
|
from textual.screen import Screen
|
|
from textual import on
|
|
|
|
from datetime import datetime
|
|
|
|
import json
|
|
import os
|
|
|
|
load_dotenv()
|
|
|
|
class ObjectListItem(ListItem):
|
|
"""A widget to display an iSightDevice object in the ListView."""
|
|
|
|
def __init__(self, device: iSightDevice) -> None:
|
|
super().__init__()
|
|
self.device = device
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Create the displayable content of the list item."""
|
|
# This line is customized to display the attributes of your iSightDevice object.
|
|
# It shows site, number, device_id, and version.
|
|
yield Label(f"{self.device.getDeviceId()} | {self.device.getSite()} | {self.device.getNumber()} ({self.device.getVersion()})")
|
|
|
|
class ModuleScreen(Screen):
|
|
|
|
BINDINGS = [Binding("escape", "app.pop_screen", "Go Back")]
|
|
|
|
def __init__(self, device: iSightDevice, device_modules: list[Module]):
|
|
super().__init__()
|
|
self.device = device
|
|
self.device_modules = device_modules
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Header(f"IoT Edge modules of {self.device.getDeviceId}")
|
|
yield DataTable()
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
table = self.query_one(DataTable)
|
|
table.add_columns("Module", "Status", "Since", "Last activity")
|
|
for module in self.device_modules:
|
|
local_connection_time = datetime.fromisoformat(str(module.connection_state_updated_time)).astimezone().replace(microsecond=0).isoformat()
|
|
local_activity_time = datetime.fromisoformat(str(module.last_activity_time)).astimezone().replace(microsecond=0).isoformat()
|
|
table.add_row(f"{module.module_id}", f"{module.connection_state}", f"{local_connection_time}", f"{local_activity_time}")
|
|
|
|
|
|
## NEW: The Screen for showing device details and actions
|
|
class DetailScreen(Screen):
|
|
"""A screen to display details and actions for a single device."""
|
|
|
|
BINDINGS = [
|
|
Binding("escape", "app.pop_screen", "Go Back"),
|
|
]
|
|
|
|
def __init__(self, device: iSightDevice, registry_manager: IoTHubRegistryManager) -> None:
|
|
super().__init__()
|
|
self.device = device
|
|
self.registry_manager = registry_manager
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield Header(name=f"Details for {self.device.getDeviceId()}")
|
|
|
|
# Use VerticalScroll so the layout works on small terminals
|
|
with VerticalScroll(id="details-container"):
|
|
# Static, non-interactive information section
|
|
yield Static(f"[b]Site:[/b] {self.device.getSite()}", markup=True)
|
|
yield Static(f"[b]Number:[/b] {self.device.getNumber()}", markup=True)
|
|
yield Static(f"[b]Version:[/b] {self.device.getVersion()}", markup=True)
|
|
yield Static(f"[b]Device ID:[/b] {self.device.getDeviceId()}", markup=True)
|
|
|
|
# Interactive action buttons
|
|
yield Static("\n[b]Actions:[/b]", markup=True)
|
|
yield Button("Check IoT Edge modules", variant="primary", id="modules")
|
|
|
|
yield Footer()
|
|
|
|
## NEW: Handlers for button presses
|
|
@on(Button.Pressed, "#modules")
|
|
def check_modules(self) -> None:
|
|
device_modules = self.registry_manager.get_modules(self.device.getDeviceId())
|
|
device_modules.sort(key=lambda m: m.module_id)
|
|
self.app.push_screen(ModuleScreen(self.device, device_modules))
|
|
|
|
# @on(Button.Pressed, "#logs")
|
|
# def check_logs(self) -> None:
|
|
# # Placeholder for your logging logic
|
|
# self.app.notify(f"Fetching logs for {self.device.getDeviceId()}...")
|
|
|
|
# @on(Button.Pressed, "#diag")
|
|
# def run_diagnostics(self) -> None:
|
|
# # Placeholder for your diagnostics logic
|
|
# self.app.notify(f"Running diagnostics on {self.device.getDeviceId()}...")
|
|
|
|
# @on(Button.Pressed, "#delete")
|
|
# def delete_device(self) -> None:
|
|
# # Placeholder for delete logic
|
|
# self.app.notify(f"Deleting {self.device.getDeviceId()} is not yet implemented.", severity="error")
|
|
# # You would likely want a confirmation dialog here in a real app.
|
|
|
|
|
|
class DeviceTUI(App):
|
|
"""An interactive TUI to list and filter iSightDevice objects."""
|
|
|
|
BINDINGS = [
|
|
Binding("q", "quit", "Quit"),
|
|
]
|
|
|
|
def __init__(self, devices: list[iSightDevice], registry_manager: IoTHubRegistryManager):
|
|
super().__init__()
|
|
self.all_devices = devices
|
|
self.filtered_devices = self.all_devices[:]
|
|
self.registry_manager = registry_manager
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Create child widgets for the app."""
|
|
yield Header(name="iSight Device Viewer")
|
|
yield Input(placeholder="Filter by site...")
|
|
with Vertical(id="list-container"):
|
|
yield ListView(*[ObjectListItem(dev) for dev in self.filtered_devices], id="device-list")
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
"""Called when the app is first mounted."""
|
|
self.query_one(Input).focus()
|
|
|
|
def on_input_changed(self, event: Input.Changed) -> None:
|
|
"""Handle changes to the input field and filter the list."""
|
|
filter_text = event.value.lower()
|
|
self.filter_devices(filter_text)
|
|
|
|
def filter_devices(self, filter_text: str):
|
|
"""Filter the list of devices based on the site."""
|
|
if filter_text:
|
|
self.filtered_devices = [
|
|
dev for dev in self.all_devices if filter_text in dev.getSite().lower()
|
|
]
|
|
else:
|
|
self.filtered_devices = self.all_devices[:]
|
|
|
|
list_view = self.query_one(ListView)
|
|
list_view.clear()
|
|
for dev in self.filtered_devices:
|
|
list_view.append(ObjectListItem(dev))
|
|
|
|
def on_list_view_selected(self, event: ListView.Selected) -> None:
|
|
"""Handle item selection in the ListView."""
|
|
selected_item = event.item
|
|
if isinstance(selected_item, ObjectListItem):
|
|
selected_device = selected_item.device
|
|
# Instead of a notification, we push the new detail screen
|
|
self.push_screen(DetailScreen(device=selected_device, registry_manager=registry_manager))
|
|
|
|
if __name__ == "__main__":
|
|
CONNECTION_STRING = str(os.getenv("CONNECTION_STRING_INOX_PROD"))
|
|
if CONNECTION_STRING == "":
|
|
print("Provide a connection string for the Iot Hub before running the script!")
|
|
exit(13)
|
|
|
|
print(f"Connecting to IoT Hub ", end="")
|
|
try:
|
|
registry_manager = IoTHubRegistryManager.from_connection_string(CONNECTION_STRING)
|
|
except Exception as e:
|
|
print(f"❌")
|
|
print(f"Error {e}")
|
|
exit
|
|
print(f"✅")
|
|
|
|
print(f"Getting list of IoT Edge devices ", end="")
|
|
try:
|
|
query_spec = QuerySpecification(query="SELECT * FROM devices WHERE capabilities.iotEdge = true ")
|
|
query_result = registry_manager.query_iot_hub(query_spec)
|
|
except Exception as e:
|
|
print(f"❌")
|
|
print(f"Error {e}")
|
|
exit
|
|
print(f"✅")
|
|
|
|
print(f"Sorting {len(query_result.items)} devices ", end="")
|
|
devices = []
|
|
for item in query_result.items:
|
|
devices.append(iSightDevice(str(item.device_id), str(item.tags['site']), int(item.tags['number']), str(item.tags['version'])))
|
|
devices.sort(key = lambda d: (d.site, d.number))
|
|
print(f"✅")
|
|
|
|
app = DeviceTUI(devices=devices, registry_manager=registry_manager)
|
|
app.run() |