diff --git a/pico-dashboard/ui/alarm_view_model.py b/pico-dashboard/ui/alarm_view_model.py new file mode 100644 index 0000000..86adc84 --- /dev/null +++ b/pico-dashboard/ui/alarm_view_model.py @@ -0,0 +1,74 @@ +from alarms.alarm_definitions import ( + BATTERY_SOC_LOW, + BATTERY_VOLTAGE_LOW, + FRIDGE_ZONE_1_HIGH, + FRIDGE_ZONE_2_HIGH, + SENSOR_FAILURE, + COMMUNICATION_LOST, +) + + +ALARM_LABELS = { + BATTERY_SOC_LOW: "Battery SOC Low", + BATTERY_VOLTAGE_LOW: "Battery Voltage Low", + FRIDGE_ZONE_1_HIGH: "Fridge Zone 1 High Temp", + FRIDGE_ZONE_2_HIGH: "Fridge Zone 2 High Temp", + COMMUNICATION_LOST: "ESP32 Communication Lost", +} + + +CRITICAL_ALARMS = { + BATTERY_SOC_LOW, + BATTERY_VOLTAGE_LOW, + FRIDGE_ZONE_1_HIGH, + FRIDGE_ZONE_2_HIGH, + COMMUNICATION_LOST, +} + + +class AlarmViewModel: + def __init__(self, alarms): + self.alarms = alarms or [] + + def count(self): + return len(self.alarms) + + def has_alarms(self): + return self.count() > 0 + + def has_critical(self): + return any(self._base_alarm(alarm) in CRITICAL_ALARMS for alarm in self.alarms) + + def should_buzz(self): + return self.has_critical() + + def primary_alarm(self): + if not self.alarms: + return None + + return self.format_alarm(self.alarms[0]) + + def as_list(self): + return [self.format_alarm(alarm) for alarm in self.alarms] + + def format_alarm(self, alarm): + base = self._base_alarm(alarm) + + if base == SENSOR_FAILURE: + sensor = alarm.split(":", 1)[1] if ":" in alarm else "unknown" + return { + "code": alarm, + "severity": "warning", + "title": "Sensor Failure", + "detail": sensor.replace("_", " ").title(), + } + + return { + "code": alarm, + "severity": "critical" if base in CRITICAL_ALARMS else "info", + "title": ALARM_LABELS.get(base, alarm), + "detail": None, + } + + def _base_alarm(self, alarm): + return alarm.split(":", 1)[0] diff --git a/tests/test_pico_core.py b/tests/test_pico_core.py index c5a3e75..72d4195 100644 --- a/tests/test_pico_core.py +++ b/tests/test_pico_core.py @@ -583,3 +583,48 @@ def test_system_view_model(): assert vm["wifi_api"] == "Available" assert vm["sensors"] == "3 / 4 OK" assert vm["ignition"] == "Off" + + +def test_alarm_view_model_no_alarms(): + from ui.alarm_view_model import AlarmViewModel + + vm = AlarmViewModel([]) + + assert vm.count() == 0 + assert vm.has_alarms() is False + assert vm.has_critical() is False + assert vm.should_buzz() is False + assert vm.primary_alarm() is None + assert vm.as_list() == [] + + +def test_alarm_view_model_critical_alarm(): + from ui.alarm_view_model import AlarmViewModel + + vm = AlarmViewModel(["fridge_zone_1_high"]) + + assert vm.count() == 1 + assert vm.has_alarms() is True + assert vm.has_critical() is True + assert vm.should_buzz() is True + + primary = vm.primary_alarm() + + assert primary["severity"] == "critical" + assert primary["title"] == "Fridge Zone 1 High Temp" + + +def test_alarm_view_model_sensor_failure(): + from ui.alarm_view_model import AlarmViewModel + + vm = AlarmViewModel(["sensor_failure:outside"]) + + assert vm.has_alarms() is True + assert vm.has_critical() is False + assert vm.should_buzz() is False + + alarm = vm.primary_alarm() + + assert alarm["severity"] == "warning" + assert alarm["title"] == "Sensor Failure" + assert alarm["detail"] == "Outside"