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"
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
class Parameter(UUIdentifable):
id: UUID
name: str
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
valid_values: dict[int | float | str, str] | None = None # value → display label
fields: list["Parameter"] | None = None # schema for data_type=struct
default_value: bytes | None = None
integration_data: Any
@property
def can_be_main_parameter(self) -> bool:
return bool(
self.data_type in (ParameterDataType.bool, ParameterDataType.none)
or self.default_value is not None
)
class ParameterState(Parameter):
value: bytes
main_parameter and default_value¶
Device.main_parameter points at the Parameter used for the quick tap-action on the
room view (toggle in most cases — e.g. OnOff for a light, not Brightness). Not every
parameter is a sensible tap target: can_be_main_parameter is True when a parameter is
either inherently binary/actionless (bool/none data type, like a toggle or a button)
or has a default_value set — a value to send when the parameter itself doesn't carry an
obvious "activate" semantic (e.g. a Thermostat.SetpointRaiseLower command needs some
concrete mode/amount to be a usable one-tap action). Set default_value on the
ParameterState you pick as main_parameter whenever its own data_type isn't
bool/none — otherwise the Hub would have nothing to send when the user taps it.
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.