Skip to content

Data Models Reference

Pairing

class CredentialsType(str, Enum):
    code = "code"      # pin, e.g. 123-45-678 (HomeKit) or 1234-123-1234 (Matter)
    secret = "secret"  # e.g. AES key like in ESPHome
    qr = "qr"          # raw QR data
    none = "none"

    def with_mask(self, code_mask: str) -> CredentialsType:
        """
        mask format: D as digit placeholder, other symbols like dashes remain unchanged,
        e.g. "DDD-DD-DDD" for "123-45-678"
        """
        self.code_mask = code_mask
        return self

type CredentialsValue = str

class ProvidedCredentials(Base):
    type: CredentialsType
    value: CredentialsValue | None = None

class Discovery(Base):
    # technical
    id: UUID
    integration: NonEmptyStr
    expected_credentials_options: list[CredentialsType]
    expiration: datetime | None = None
    # for UX
    transport: NonEmptyStr
    device_manufacturer: str | None
    device_name: NonEmptyStr
    device_category: str | None
    device_icon: str | None
    last_error: str | None = None  # set by the integration on failure, cleared (None) on recovery

A discovery advertises every credentials type it can actually be paired with — expected_credentials_options — since a device can support more than one simultaneously (e.g. a Matter device with both a QR code and a manual pairing code). List every option the device supports, in the order you'd like the app to prefer them.

Pairing requests then send back ProvidedCredentials, pairing the actual value with which type it is, instead of a bare string. Your pair_device() must validate credentials.type is one of discovery.expected_credentials_options before using it — don't trust discovery.credentials from whatever your own discovery-time heuristic guessed; validate against what the caller explicitly asserts.

Device

class DeviceInfo(DevicePatch):
    id: UUID
    name: str
    note: str = ""
    icon: str | None = None
    category: str | None = None
    room_id: UUID
    transport: str
    integration: str
    manufacturer: str | None
    last_seen: datetime | None = None
    available: bool = False
    last_error: str | None = None  # persisted; set by the integration on failure, cleared (None) on recovery
    main_parameter: UUID | None = None  # for the tap action on the room view, toggle in most cases

class Device(DeviceInfo):
    integration_data: SerializeAsAny[dict | Base] = Field(default_factory=Base)

class DeviceState(DeviceInfo):
    parameters: list[ParameterState]

Parameter

class ParameterDataType(StrEnum):
    none = "none"        # e.g. button
    bool = "bool"
    integer = "integer"
    decimal = "decimal"  # python float
    enum = "enum"        # integer with string representation
    string = "string"
    struct = "struct"    # multi-field object (e.g. command arguments); see "Commands with arguments"
    data = "data"        # binary data, base64 encoded at high level

class ParameterUnit(StrEnum):
    plain = "plain"
    percentage = "percentage"
    second = "second"
    hertz = "hertz"
    kilogram = "kilogram"
    arcdegree = "arcdegree"
    meters = "meters"
    mps = "mps"      # meters per second
    mps2 = "mps2"    # meters per second squared
    rpm = "rpm"
    newton = "newton"
    joule = "joule"
    watt = "watt"
    celsius = "celsius"
    kelvin = "kelvin"
    volt = "volt"
    ampere = "ampere"
    lux = "lux"
    pascal = "pascal"
    ppm = "ppm"      # parts per million, air quality
    bytes = "bytes"
    bps = "bps"      # bytes per second

class ParameterRole(StrEnum):
    sensor = 'sensor'   # read-only
    control = 'control' # read-write
    event = 'event'     # fire-and-forget


class ParameterVisibility(StrEnum):
    user = "user"  # main, everyday interaction, device screen widgets (on/off, brightness, volume)
    setting = "setting"  # user-configurable but behind am extra "settings"/"advanced" tap: configured once and rarely touched again; or diagnostic readings (RSSI, firmware version)
    system = "system"  # hidden under-the-hood wirings; not visible to the user

# Generic over its value type V — value, valid_values keys, and default_value all share V
# (an int parameter has int labels and an int default; a str parameter is str throughout).
class Parameter[V](UUIdentifable):
    id: UUID
    name: str
    description: str | None = None   # manufacturer-provided, read-only
    note: str | None = None          # user-editable, MajorDom-side only
    data_type: ParameterDataType
    unit: ParameterUnit = ParameterUnit.plain
    role: ParameterRole
    visibility: ParameterVisibility
    min_value: int | float | None = None
    max_value: int | float | None = None
    min_step: int | float | None = None       # smallest increment for a numeric parameter
    valid_values: dict[V, str] | None = None  # allowed values → labels; for enums (numbers: use min/max/step)
    fields: list["Parameter"] | None = None   # sub-parameters for data_type=struct (e.g. command args)
    default_value: set[V] | V | None = None   # main-parameter tap value(s): one = button, a set = cycle
    integration_data: Any                     # protocol-specific payload, opaque to the Hub

    @property
    def can_be_main_parameter(self) -> bool:
        return self.visibility == ParameterVisibility.user and bool(
            self.data_type in (ParameterDataType.bool, ParameterDataType.none)
            or self.default_value is not None
            or self.valid_values
        )

class ParameterState[V](Parameter[V]):
    value: V | None = None   # pythonic value (int/bool/str/float/dict/...), not bytes

main_parameter and default_value

Device.main_parameter points at the one Parameter behind the room-tile tap (a toggle in most cases — e.g. OnOff for a light, not Brightness). A parameter is eligible (can_be_main_parameter) when it's user-visible and a tap does something meaningful: a bool or none command inherently, an enum through its valid_values, or — for any data type — a default_value.

default_value is what makes an arbitrary parameter tappable (any data type) and curates what a tap sends — set it to one value for a button, or a set for a cycle (e.g. brightness {0, 80} so a dimmer taps like an on/off switch). It shares the parameter's value type V. (valid_values only describes a parameter's allowed values and their labels — it doesn't carry the tap value.)

See Parameter UX for how each case behaves on tap and how to pick the main parameter and good visibility.

Commands with arguments

Some integrations (e.g. Matter) expose commands that take a list of arguments — for those, the command itself is modeled as a Parameter and each of its arguments as a nested Parameter in fields: command = parameter, argument = sub-parameter. This is a convention on top of the generic schema, not a separate concept. If your integration's commands don't take structured arguments, you leave fields unset.