diff --git a/src/labthings_fastapi/webthing_subprotocol.py b/src/labthings_fastapi/webthing_subprotocol.py new file mode 100644 index 00000000..df24406f --- /dev/null +++ b/src/labthings_fastapi/webthing_subprotocol.py @@ -0,0 +1,99 @@ +"""WebThing WebSocket subprotocol models. + +This module defines models for the messages sent over websockets, which are aligned with +the ``webthingprotocol`` subprotocol as set out in the `community group draft report`_. + +.. _community group draft report: https://w3c.github.io/web-thing-protocol/ + +(c) Richard Bowman October 2026, released under MIT license +""" + +from datetime import datetime +from typing import Any, Literal +from uuid import UUID, uuid4 + +from pydantic import BaseModel, Field + +from labthings_fastapi.middleware.url_for import URLFor + + +class WebsocketMessage(BaseModel): + """A base model for all websocket messages. + + This will auto-generate the messageID and timestamp if they + are not supplied. + """ + + thingID: str | URLFor + messageID: UUID = Field(default_factory=uuid4) + messageType: Literal["request", "response", "notification"] + operation: str + correlationID: UUID | None = None + timestamp: datetime | None = Field(default_factory=datetime.now) + + +class ObservePropertyMessage(WebsocketMessage): + """A base model for messages related to observing a property.""" + + name: str + operation: Literal["observeproperty"] = "observeproperty" + + +class ObservePropertyNotification(ObservePropertyMessage): + """A model for property change messages.""" + + messageType: Literal["notification"] = "notification" + value: Any + + +class ObserveActionMessage(WebsocketMessage): + """A base model for messages related to observing an action.""" + + name: str + operation: Literal["observeaction"] = "observeaction" + + +class ObserveActionNotification(ObserveActionMessage): + """A model for action notification messages. + + This is not part of the webthingprotocol draft, so should be considered + at risk of summary removal. + """ + + messageType: Literal["notification"] = "notification" + actionID: UUID + state: Literal["pending", "running", "completed", "failed"] + + +class ActionStatus(BaseModel): + """The status of an action invocation.""" + + actionID: UUID + state: Literal["pending", "running", "completed", "failed"] + output: Any | None = None + error: "ProblemDetails | None" = None + timeRequested: datetime | None = None + timeEnded: datetime | None = None + + +class ProblemDetails(BaseModel): + """Details of an error. + + This follows RFC9457_. + + .. _RFC9457: https://datatracker.ietf.org/doc/html/rfc9457 + """ + + status: int + type: str + title: str + detail: str | None = None + + +class ObservationErrorResponse(WebsocketMessage): + """A websocket error response for observing an action or property.""" + + messageType: Literal["response"] = "response" + operation: Literal["observeaction", "observeproperty"] + name: str + error: ProblemDetails