Cleanup, sensible exceptions

This commit is contained in:
Jono Targett 2026-03-15 20:51:55 +10:30
parent 641649d964
commit 85a5f400bc
4 changed files with 53 additions and 34 deletions

View File

@ -1,5 +1,14 @@
import json
import jsonschema import jsonschema
from dataclasses import dataclass from dataclasses import dataclass, asdict
class CommandArgumentError(ValueError):
pass
class CommandExecutionError(RuntimeError):
pass
class Command: class Command:
@ -9,19 +18,25 @@ class Command:
self.handler = handler self.handler = handler
self.additional_properties = kwargs self.additional_properties = kwargs
self._validator = jsonschema.validators.validator_for( def __call__(self, handler_instance, payload):
schema, default=jsonschema.validators.Draft7Validator
)(schema=schema)
def validate(self, o) -> bool:
return self._validator.is_valid(o)
def __call__(self, handler_instance, args):
if not self.validate(args):
raise ValueError(f"Invalid arguments for command '{self.name}'")
if self.handler is None: if self.handler is None:
raise RuntimeError(f"No handler bound for command '{self.name}'") raise NotImplementedError(f"No handler bound for command '{self.name}'")
return self.handler(handler_instance, args)
try:
arguments = json.loads(payload)
jsonschema.validate(arguments, self.schema)
except json.decoder.JSONDecodeError as e:
raise CommandArgumentError(
f"Invalid JSON at line {e.lineno} column {e.colno}: {e.msg}"
)
except jsonschema.ValidationError as e:
raise CommandArgumentError(f"Schema error in {e.json_path}: {e.message}")
try:
return self.handler(handler_instance, arguments)
except Exception as e:
print("Internal command error: ", e)
raise CommandExecutionError(f"Command execution failed internally.")
def command(schema, **kwargs): def command(schema, **kwargs):
@ -36,3 +51,6 @@ class CommandResponse:
success: bool success: bool
message: str = None message: str = None
correlation: str = None correlation: str = None
def __str__(self):
return json.dumps(asdict(self))

View File

@ -1,10 +1,14 @@
import asyncio
import aiomqtt import aiomqtt
import json import asyncio
from command import Command, CommandResponse
import inspect import inspect
from paho.mqtt.properties import Properties import paho
from dataclasses import asdict
from command import (
Command,
CommandResponse,
CommandArgumentError,
CommandExecutionError,
)
class MQTTHandler: class MQTTHandler:
@ -47,9 +51,11 @@ class MQTTHandler:
) )
async def execute_command( async def execute_command(
self, command_name: str, payload: str, properties: Properties = None self,
command_name: str,
payload: str,
properties: paho.mqtt.properties.Properties = None,
): ):
async def respond(success: bool, message: str = None): async def respond(success: bool, message: str = None):
if ( if (
properties is not None properties is not None
@ -63,23 +69,17 @@ class MQTTHandler:
) )
await self.mqtt_client.publish( await self.mqtt_client.publish(
properties.ResponseTopic, properties.ResponseTopic,
json.dumps( str(CommandResponse(success, str(message), correlation)),
asdict(CommandResponse(success, str(message), correlation))
),
qos=1, qos=1,
retain=False, retain=False,
) )
try: try:
command = self.get_available_commands()[command_name] command = self.get_available_commands()[command_name]
argument = json.loads(payload) result = await command(self, payload)
result = await command(self, argument)
await respond(True, result) await respond(True, result)
except (CommandArgumentError, CommandExecutionError) as e:
except json.decoder.JSONDecodeError as e: await respond(False, f"{e}")
await respond(False, f"Failed to parse payload as JSON: {e}")
except ValueError as e:
await respond(False, f"Command payload does not match expected schema: {e}")
except Exception as e: except Exception as e:
print(f"Failed to execute command {command_name} with unknown cause: ", e) print(f"Failed to execute command {command_name} with unknown cause: ", e)
await respond(False, "Unexpected error") await respond(False, "Unexpected error")

View File

@ -5,6 +5,7 @@ import aioserial
import asyncio import asyncio
import paho import paho
import uuid import uuid
import socket
from ubxhandler import UBXHandler from ubxhandler import UBXHandler
@ -12,7 +13,7 @@ BAUD = 115200
async def main(): async def main():
handler_id = "example-gps" handler_id = f"example-gps-{socket.gethostname()}"
mqtt_host = "127.0.0.1" mqtt_host = "127.0.0.1"
mqtt_port = 1883 mqtt_port = 1883
@ -30,7 +31,7 @@ async def main():
timeout=0.05, # 50 ms timeout=0.05, # 50 ms
) )
handler = UBXHandler(mqtt_client, "example-gps", serial_port) handler = UBXHandler(mqtt_client, handler_id, serial_port)
await handler.run() await handler.run()

View File

@ -70,7 +70,7 @@ class UBXHandler(MQTTHandler):
if name.startswith("_"): if name.startswith("_"):
continue continue
topic = f"{self.topic_base}/{message.identity}/{name}" topic = f"{self.property_topic}/{message.identity}/{name}"
await self.mqtt_client.publish(topic, value, qos=1, retain=True) await self.mqtt_client.publish(topic, value, qos=1, retain=True)
else: else:
# print("Unexpected response:", message) # print("Unexpected response:", message)