Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 99 additions & 0 deletions src/labthings_fastapi/webthing_subprotocol.py
Original file line number Diff line number Diff line change
@@ -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

Check warning on line 13 in src/labthings_fastapi/webthing_subprotocol.py

View workflow job for this annotation

GitHub Actions / coverage

11-13 lines are not covered with tests

from pydantic import BaseModel, Field

Check warning on line 15 in src/labthings_fastapi/webthing_subprotocol.py

View workflow job for this annotation

GitHub Actions / coverage

15 line is not covered with tests

from labthings_fastapi.middleware.url_for import URLFor

Check warning on line 17 in src/labthings_fastapi/webthing_subprotocol.py

View workflow job for this annotation

GitHub Actions / coverage

17 line is not covered with tests


class WebsocketMessage(BaseModel):

Check warning on line 20 in src/labthings_fastapi/webthing_subprotocol.py

View workflow job for this annotation

GitHub Actions / coverage

20 line is not covered with tests
"""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)

Check warning on line 32 in src/labthings_fastapi/webthing_subprotocol.py

View workflow job for this annotation

GitHub Actions / coverage

27-32 lines are not covered with tests


class ObservePropertyMessage(WebsocketMessage):

Check warning on line 35 in src/labthings_fastapi/webthing_subprotocol.py

View workflow job for this annotation

GitHub Actions / coverage

35 line is not covered with tests
"""A base model for messages related to observing a property."""

name: str
operation: Literal["observeproperty"] = "observeproperty"

Check warning on line 39 in src/labthings_fastapi/webthing_subprotocol.py

View workflow job for this annotation

GitHub Actions / coverage

38-39 lines are not covered with tests


class ObservePropertyNotification(ObservePropertyMessage):

Check warning on line 42 in src/labthings_fastapi/webthing_subprotocol.py

View workflow job for this annotation

GitHub Actions / coverage

42 line is not covered with tests
"""A model for property change messages."""

messageType: Literal["notification"] = "notification"
value: Any

Check warning on line 46 in src/labthings_fastapi/webthing_subprotocol.py

View workflow job for this annotation

GitHub Actions / coverage

45-46 lines are not covered with tests


class ObserveActionMessage(WebsocketMessage):

Check warning on line 49 in src/labthings_fastapi/webthing_subprotocol.py

View workflow job for this annotation

GitHub Actions / coverage

49 line is not covered with tests
"""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
Loading