Skip to content

Space Update Model

Whatever system you use to send parking space occupancy updates, it must send json payloads to your site's /space/update-state endpoint.

You must create a subclass of SpaceUpdateBaseModel for your specific update payload. This subclass is passed to build_app() and is used to validate requests that hit the endpoint.

I recommended using the excellent tool datamodel-code-generator to automatically generate the pydantic model from a real json payload for your system.

I like running it with these options:

datamodel-codegen \
--input "your-update-payload.json" \
--input-file-type json \
--output "your_update_class.py" \
--class-name "YourUpdateClass" \
--output-model-type pydantic_v2.BaseModel \
--target-python-version 3.13 \
--use-standard-collections \
--use-union-operator \
--reuse-model \
--use-double-quotes \
--enum-field-as-literal all \
--use-one-literal-as-default \
--collapse-root-models \
--wrap-string-literal \
--disable-timestamp

Here's a working example generated for the car sensor I'm using:

nwave_parking_sensor.py
"""
A real working example of a `SpaceUpdateBaseModel` subclass: the Nwave Smart Parking
Sensor G4 SM.
https://www.thethingsnetwork.org/device-repository/devices/nwave/nps405sm/

This is the LoRa device I'm using for the deployment that inspired this
framework. This file was autogenerated from the json payload using `datamodel-codegen`
and then slightly embellished: annotated the timestamps as `AwareDatetime`, made the
model inherit from `SpaceUpdateBaseModel` and defined its subclass methods.

I'm using the free, open-source LoRaWAN network server hosted by The Things Network
(TTN), with a webhook to send the payload on to my app endpoint. Note that it was
necessary to define an Uplink "Payload Formatter" function in TTN console to make the
TTN Application Server automatically parse the raw payload bytes into a human-interpretable
`DecodedPayload` model. TTN keeps a repository of these formatter functions for common devices.
"""

from __future__ import annotations

from typing import Literal

from pydantic import AwareDatetime, BaseModel, Field

from park_it.models.space_update import LowerStr, SpaceUpdateBaseModel


class ApplicationIds(BaseModel):
    application_id: str


class DeviceIds(BaseModel):
    device_id: str
    application_ids: ApplicationIds
    dev_eui: str
    join_eui: str
    dev_addr: str


class Identifier(BaseModel):
    device_ids: DeviceIds


class DecodedPayload(BaseModel):
    """The meat, decoded from raw payload bytes using the default TTN
    payload formatter for this sensor."""

    occupied: bool
    previous_state_duration: int
    previous_state_duration_error: int
    previous_state_duration_overflow: bool
    # TODO maybe operate on the periodic heartbeat message too
    type: Literal["parking_status"]


class GatewayIds(BaseModel):
    gateway_id: str
    eui: str


class Location(BaseModel):
    latitude: float
    longitude: float
    source: str


class RxMetadatum(BaseModel):
    gateway_ids: GatewayIds
    timestamp: int
    rssi: int
    channel_rssi: int
    snr: float
    frequency_offset: str
    location: Location
    uplink_token: str
    channel_index: int
    received_at: AwareDatetime


class Lora(BaseModel):
    bandwidth: int
    spreading_factor: int
    coding_rate: str


class DataRate(BaseModel):
    lora: Lora


class Settings(BaseModel):
    data_rate: DataRate
    frequency: str
    timestamp: int


class NetworkIds(BaseModel):
    net_id: str
    ns_id: str
    tenant_id: str
    cluster_id: str
    cluster_address: str


class UplinkMessage(BaseModel):
    session_key_id: str
    f_port: int
    f_cnt: int
    frm_payload: str
    decoded_payload: DecodedPayload
    rx_metadata: list[RxMetadatum]
    settings: Settings
    received_at: AwareDatetime
    confirmed: bool
    consumed_airtime: str
    network_ids: NetworkIds


class Data(BaseModel):
    field_type: str = Field(..., alias="@type")
    end_device_ids: DeviceIds
    correlation_ids: list[str]
    received_at: AwareDatetime
    uplink_message: UplinkMessage


class Context(BaseModel):
    tenant_id: str = Field(..., alias="tenant-id")


class Visibility(BaseModel):
    rights: list[str]


class NwaveParkingSensorUpdate(SpaceUpdateBaseModel):
    """
    Subclass generated from the full payload bundle for the Nwave Smart Parking Sensor
    G4 SM, with subclass methods implemented for grabbing required data.
    """

    name: str
    time: AwareDatetime
    identifiers: list[Identifier]
    data: Data
    correlation_ids: list[str]
    origin: str
    context: Context
    visibility: Visibility
    unique_id: str

    def sensor_id(self) -> LowerStr:
        return self.identifiers[0].device_ids.device_id

    def occupied(self) -> bool:
        return self.data.uplink_message.decoded_payload.occupied

    def update_time(self) -> AwareDatetime:
        return self.data.uplink_message.received_at