diff --git a/pico-dashboard/comms/communication_service.py b/pico-dashboard/comms/communication_service.py new file mode 100644 index 0000000..32a2de1 --- /dev/null +++ b/pico-dashboard/comms/communication_service.py @@ -0,0 +1,48 @@ +from .protocol import ( + make_status_request, + make_set_relay, + is_status_response, + is_relay_response, + is_error, +) + + +class CommunicationService: + def __init__(self, uart_client, app_state): + self.uart_client = uart_client + self.app_state = app_state + self.last_messages = [] + + def request_status(self): + self.uart_client.send_message(make_status_request()) + + def set_relay(self, relay, enabled): + self.uart_client.send_message(make_set_relay(relay, enabled)) + + def poll(self): + messages = self.uart_client.read_available_messages() + self.last_messages = messages + + for message in messages: + self.handle_message(message) + + return messages + + def handle_message(self, message): + if is_status_response(message): + self.app_state.update_from_status(message) + return + + if is_relay_response(message): + self.app_state.update_from_relay_response(message) + return + + if is_error(message): + self.app_state.set_error(message) + return + + self.app_state.set_error({ + "type": "error", + "message": "unknown_message_type", + "raw": message, + }) diff --git a/tests/test_pico_core.py b/tests/test_pico_core.py index 1b121a4..53943b7 100644 --- a/tests/test_pico_core.py +++ b/tests/test_pico_core.py @@ -150,3 +150,81 @@ def test_uart_client_handles_invalid_json(): assert messages[0]["type"] == "error" assert messages[0]["message"] == "invalid_json" + + +def test_communication_service_requests_status(): + from comms.uart_client import UartClient + from comms.communication_service import CommunicationService + + fake = FakeUart() + state = AppState() + service = CommunicationService(UartClient(fake), state) + + service.request_status() + + assert fake.writes == [b'{"type":"status_request"}\n'] + + +def test_communication_service_sends_relay_command(): + from comms.uart_client import UartClient + from comms.communication_service import CommunicationService + + fake = FakeUart() + state = AppState() + service = CommunicationService(UartClient(fake), state) + + service.set_relay("fridge", False) + + assert fake.writes == [b'{"type":"set_relay","relay":"fridge","enabled":false}\n'] + + +def test_communication_service_updates_state_from_status(): + from comms.uart_client import UartClient + from comms.communication_service import CommunicationService + + fake = FakeUart() + fake.read_chunks = [ + b'{"type":"status_response","timestamp":99,"battery":{"soc":75},"network":{"uart_connected":true}}\n' + ] + + state = AppState() + service = CommunicationService(UartClient(fake), state) + + messages = service.poll() + + assert messages[0]["type"] == "status_response" + assert state.last_status_timestamp == 99 + assert state.battery["soc"] == 75 + assert state.network["uart_connected"] is True + + +def test_communication_service_updates_state_from_relay_response(): + from comms.uart_client import UartClient + from comms.communication_service import CommunicationService + + fake = FakeUart() + fake.read_chunks = [ + b'{"type":"relay_response","relay":"starlink","enabled":true,"ok":true}\n' + ] + + state = AppState() + service = CommunicationService(UartClient(fake), state) + + service.poll() + + assert state.relays["starlink"] is True + + +def test_communication_service_records_errors(): + from comms.uart_client import UartClient + from comms.communication_service import CommunicationService + + fake = FakeUart() + fake.read_chunks = [b'{"type":"error","message":"invalid_json"}\n'] + + state = AppState() + service = CommunicationService(UartClient(fake), state) + + service.poll() + + assert state.last_error["message"] == "invalid_json"