diff --git a/pico-dashboard/ui/dashboard_view_model.py b/pico-dashboard/ui/dashboard_view_model.py new file mode 100644 index 0000000..3084f51 --- /dev/null +++ b/pico-dashboard/ui/dashboard_view_model.py @@ -0,0 +1,59 @@ +def format_value(value, suffix="", decimals=1, missing="--"): + if value is None: + return missing + + if isinstance(value, int): + return f"{value}{suffix}" + + if isinstance(value, float): + return f"{value:.{decimals}f}{suffix}" + + return f"{value}{suffix}" + + +def format_bool(value, true_text="ON", false_text="OFF"): + return true_text if value else false_text + + +class DashboardViewModel: + def __init__(self, state, alarms=None): + self.state = state + self.alarms = alarms or [] + + def top_bar(self): + soc = format_value(self.state.battery.get("soc"), "%", decimals=0) + uart = "UART OK" if self.state.network.get("uart_connected") else "UART LOST" + alarm_count = len(self.alarms) + + return { + "soc": soc, + "comms": uart, + "alarms": alarm_count, + } + + def battery_summary(self): + return { + "voltage": format_value(self.state.battery.get("voltage"), "V", decimals=1), + "current": format_value(self.state.battery.get("current"), "A", decimals=1), + "runtime": format_value(self.state.battery.get("runtime_hours"), " hr", decimals=1), + } + + def fridge_summary(self): + return { + "zone_1": format_value(self.state.temps.get("fridge_zone_1"), "°F", decimals=1), + "zone_2": format_value(self.state.temps.get("fridge_zone_2"), "°F", decimals=1), + } + + def power_summary(self): + return { + "starlink": format_bool(self.state.relays.get("starlink")), + "fridge": format_bool(self.state.relays.get("fridge")), + } + + def as_dict(self): + return { + "top_bar": self.top_bar(), + "battery": self.battery_summary(), + "fridge": self.fridge_summary(), + "power": self.power_summary(), + } diff --git a/tests/test_pico_core.py b/tests/test_pico_core.py index 1a375b4..ee329d0 100644 --- a/tests/test_pico_core.py +++ b/tests/test_pico_core.py @@ -426,3 +426,59 @@ def test_communication_service_auto_selects_http_fallback_when_uart_down(): assert service.auto_select_transport() is True assert service.use_http_fallback is True + + +def test_dashboard_view_model_formats_dashboard_summary(): + from ui.dashboard_view_model import DashboardViewModel + + state = AppState() + state.update_from_status({ + "battery": { + "soc": 82, + "voltage": 13.2, + "current": -6.4, + "runtime_hours": 12.0, + }, + "temps": { + "fridge_zone_1": 34.5, + "fridge_zone_2": 36.0, + }, + "relays": { + "starlink": False, + "fridge": True, + }, + "network": { + "uart_connected": True, + }, + }) + + vm = DashboardViewModel(state, alarms=[]).as_dict() + + assert vm["top_bar"]["soc"] == "82%" + assert vm["top_bar"]["comms"] == "UART OK" + assert vm["top_bar"]["alarms"] == 0 + + assert vm["battery"]["voltage"] == "13.2V" + assert vm["battery"]["current"] == "-6.4A" + assert vm["battery"]["runtime"] == "12.0 hr" + + assert vm["fridge"]["zone_1"] == "34.5°F" + assert vm["fridge"]["zone_2"] == "36.0°F" + + assert vm["power"]["starlink"] == "OFF" + assert vm["power"]["fridge"] == "ON" + + +def test_dashboard_view_model_handles_missing_values(): + from ui.dashboard_view_model import DashboardViewModel + + state = AppState() + + vm = DashboardViewModel(state, alarms=["communication_lost"]).as_dict() + + assert vm["top_bar"]["soc"] == "--" + assert vm["top_bar"]["comms"] == "UART LOST" + assert vm["top_bar"]["alarms"] == 1 + + assert vm["battery"]["voltage"] == "--" + assert vm["fridge"]["zone_1"] == "--"