Skip to content

Adapter interface

An adapter knows one external platform: how to receive its events, how to send commands and how to list its devices. It never looks inside a device payload. Adapters live in shared/connectivity/adapters/<provider>/ and are registered in shared/connectivity/registry.py. Provider-specific code lives nowhere else; a test enforces this. Use examples/adapters/example_platform/ as the starting point.

Contract

class Adapter(Protocol):
    key: ClassVar[str]  # "chirpstack"
    label: ClassVar[str]
    default_capabilities: ClassVar[AdapterCapabilities]
    default_link_templates: ClassVar[dict[str, str]]  # OPEN_DEVICE and so on
    config_schema: ClassVar[dict[str, Any]]  # JSON schema of the data source config

    def event_connector(self, source: DataSourceContext) -> EventConnector | None: ...
    def parse_webhook(
        self, source: DataSourceContext, body: Any, headers: dict[str, str]
    ) -> list[InboundMessage]: ...

Push platforms implement parse_webhook; the API calls it with the body of POST /api/v1/ingest/http/{data_source_id}. Platforms that need a long-running connection implement event_connector, which returns an object whose run(emit) blocks and calls emit(InboundMessage) per message; the ingest service runs it and restarts it when it ends. The transports under shared/connectivity/transports/ (MQTT with reconnect, polling, websocket, HTTP helpers) do the generic part.

InboundMessage

external_id (the provider's device identifier), event_type (uplink, join, ack, status, ...), payload (the provider's message as a JSON object), acquisition_channel, ingestion_method, provider_metadata, and the provenance times the platform knows (network_received_at and the satellite, BLE and file times). The adapter fills what the platform exposes and leaves the rest empty. It never invents a device time.

AdapterCapabilities (architecture 8.2) is stored per data source; the adapter's defaults are the starting point and administrators adjust them for an account that exposes less. default_link_templates are URL templates with placeholders from the external identity, used for "Open in ..." links.

Errors

A malformed body raises ApplicationError with PAYLOAD_DECODE_FAILED; the API answers 422. A connector that loses its connection reconnects; one that crashes is restarted after ten seconds and the crash is logged.

Runbook

Every production adapter ships a runbook under docs/integrations/<provider>/: setup, authentication, uplink and downlink flow, timestamps, troubleshooting, example payloads (architecture 28.7).