Device Types Guide¶
This guide covers advanced device type features beyond the basics shown in the Quick Start.
Anatomy of a DeviceType¶
A DeviceType is a plain Python class that uses a metaclass (DeviceTypeMeta) to collect field descriptors and auto-register itself in the global registry. When you call create_models(), it generates three concrete Django models from your declaration.
from django_devicehub import DeviceType, Reading, Command, StateField, reading_types
class WeatherStation(DeviceType):
class Meta:
protocol = "mqtt"
broker = "default"
auth = "token"
heartbeat_interval = 300
topic_template = "{prefix}/{type_name}/{device_id}/{message_type}"
# Readings -- become columns on the Reading model
temperature = Reading(type=reading_types.FLOAT, unit="C", range=(-40, 80))
humidity = Reading(type=reading_types.FLOAT, unit="%", range=(0, 100))
# Commands -- can be sent to devices via MQTT
reboot = Command()
set_interval = Command(payload={"interval": int})
# State fields -- become columns on the Device model
recording_mode = StateField(type="string", default="continuous")
Meta Options¶
The inner Meta class controls protocol behavior. All options have defaults, so Meta is optional.
| Option | Type | Default | Description |
|---|---|---|---|
protocol |
str |
"mqtt" |
Communication protocol |
broker |
str |
"default" |
Which broker from DJANGO_DEVICEHUB["BROKERS"] to use |
auth |
str |
"token" |
Authentication mechanism |
heartbeat_interval |
int |
300 |
Expected heartbeat interval in seconds |
topic_template |
str |
"{prefix}/{type_name}/{device_id}/{message_type}" |
MQTT topic format string |
Access resolved meta values programmatically:
meta = WeatherStation.get_meta()
# {'protocol': 'mqtt', 'broker': 'default', 'auth': 'token',
# 'heartbeat_interval': 300, 'topic_template': '...'}
Meta values are merged with defaults, so you only need to specify what you want to override:
class LoRaNode(DeviceType):
class Meta:
heartbeat_interval = 900 # 15 minutes for low-power devices
broker = "lorawan" # use a separate broker
Reading Fields¶
A Reading declares a sensor data column on the generated Reading model. Each reading becomes a nullable field in a wide time-series table -- one row can contain values for multiple readings.
Constructor parameters¶
Reading(
type=reading_types.FLOAT, # data type (required)
unit="C", # unit label (stored as help_text)
range=(-40, 80), # min/max validators
required=False, # whether the field is required
verbose_name="Temperature", # human-readable label
)
Supported reading types¶
| Constant | Django field | Python type |
|---|---|---|
reading_types.FLOAT |
FloatField |
float |
reading_types.INTEGER |
IntegerField |
int |
reading_types.BOOLEAN |
BooleanField |
bool |
reading_types.STRING |
CharField(max_length=255) |
str |
reading_types.JSON |
JSONField |
dict / list |
reading_types.BINARY |
BinaryField |
bytes |
Range validation¶
When range is provided as a 2-tuple, MinValueValidator and MaxValueValidator are added to the generated field. Either bound can be None to leave it open:
# Only upper bound
wind_speed = Reading(type=reading_types.FLOAT, unit="m/s", range=(None, 200))
# Only lower bound
pressure = Reading(type=reading_types.FLOAT, unit="hPa", range=(0, None))
Command Fields¶
A Command declares an action that can be sent to a device over MQTT. Commands are published to the cmd channel of the device's topic.
Constructor parameters¶
Command(
payload={"interval": int}, # expected payload schema (type-checked)
description="Set interval", # human-readable description
qos=1, # MQTT QoS level (0, 1, or 2)
)
Payload validation¶
When a command has a payload dict, each key maps to an expected Python type. Validation runs automatically when send_command() is called:
class Thermostat(DeviceType):
set_target = Command(payload={"target_temp": float, "mode": str})
# Later:
device.send_command("set_target", {"target_temp": 22.5, "mode": "heat"})
# Raises ValueError if a key is missing:
device.send_command("set_target", {"target_temp": 22.5})
# ValueError: Missing required payload field: mode
# Raises TypeError if a value has the wrong type:
device.send_command("set_target", {"target_temp": "warm", "mode": "heat"})
# TypeError: Field target_temp: expected float, got str
Commands with no payload are simple triggers:
reboot = Command() # no payload required
calibrate = Command(qos=2) # use QoS 2 for critical commands
Sending commands¶
Use the send_command() method on any device instance:
device = WeatherStationDevice.objects.get(device_id="STATION-001")
device.send_command("reboot")
device.send_command("set_interval", {"interval": 60})
This publishes a JSON message to {prefix}/weatherstation/STATION-001/cmd:
The device_command_sent signal fires after publishing.
StateField¶
A StateField declares a persistent field on the generated Device model (beyond the base fields like status, battery_level, etc.). Use state fields for device configuration or mode tracking that should be stored in the database.
Constructor parameters¶
StateField(
type="string", # data type
default="continuous", # default value
choices=["continuous", "motion", "scheduled"], # allowed values
verbose_name="Recording Mode", # human-readable label
)
Supported types¶
| Type string | Django field |
|---|---|
"string" |
CharField(max_length=255) |
"float" |
FloatField |
"integer" |
IntegerField |
"boolean" |
BooleanField |
"json" |
JSONField |
"datetime" |
DateTimeField |
Behavior¶
- If
defaultis provided, the field isNOT NULLwith the given default. - If
defaultisNone(the default), the field is nullable. - If
choicesis provided, they are converted to Django'schoicesformat:[("continuous", "continuous"), ...].
Example: camera with state¶
class SmartCamera(DeviceType):
class Meta:
heartbeat_interval = 60
# Readings
frame_rate = Reading(type=reading_types.FLOAT, unit="fps")
motion_detected = Reading(type=reading_types.BOOLEAN)
# State (persisted on the Device model)
recording_mode = StateField(
type="string",
default="continuous",
choices=["continuous", "motion", "scheduled"],
)
night_vision = StateField(type="boolean", default=False)
sensitivity = StateField(type="integer", default=50)
# Commands
start_recording = Command()
set_mode = Command(payload={"mode": str})
The generated SmartCameraDevice model will have all BaseDevice fields plus recording_mode, night_vision, and sensitivity columns.
Lifecycle Hooks¶
Override these methods on your DeviceType to run custom logic when events occur. Hooks are called by the MessageRouter after signals are fired.
on_data(self, device, readings)¶
Called every time a device publishes data. The readings parameter is a dict of field names to values.
class WeatherStation(DeviceType):
temperature = Reading(type=reading_types.FLOAT, unit="C", range=(-40, 80))
def on_data(self, device, readings):
temp = readings.get("temperature")
if temp is not None and temp > 50:
# Send alert, log event, trigger notification, etc.
logger.warning("High temperature on %s: %s", device.device_id, temp)
on_status_change(self, device, old_status, new_status)¶
Called when a device's status changes (e.g., "offline" to "online").
def on_status_change(self, device, old_status, new_status):
if new_status == "error":
notify_ops_team(device)
on_connect(self, device)¶
Called when a device connects.
on_disconnect(self, device)¶
Called when a device disconnects or its heartbeat times out.
on_command_response(self, device, command, response)¶
Called when a device responds to a command.
def on_command_response(self, device, command, response):
logger.info("Device %s responded to %s: %s",
device.device_id, command, response)
Inheritance¶
DeviceType supports single inheritance. Readings, commands, and state fields from base classes are inherited and can be overridden:
class BaseSensor(DeviceType):
_abstract = True # prevents registration in the registry
battery = Reading(type=reading_types.FLOAT, unit="%")
firmware_update = Command()
class TemperatureSensor(BaseSensor):
temperature = Reading(type=reading_types.FLOAT, unit="C")
# Inherits: battery reading, firmware_update command
class HumiditySensor(BaseSensor):
humidity = Reading(type=reading_types.FLOAT, unit="%")
# Inherits: battery reading, firmware_update command
Set _abstract = True on intermediate classes to prevent them from being registered in the device type registry. Only concrete device types should be registered.
Multiple Device Types¶
You can define as many device types as you need, each in its own devices.py or all together. The auto-discovery system (autodiscover_modules("devices")) finds all devices.py files in your installed apps.
# sensors/devices.py
class TemperatureSensor(DeviceType):
temperature = Reading(type=reading_types.FLOAT, unit="C")
class PressureSensor(DeviceType):
pressure = Reading(type=reading_types.FLOAT, unit="hPa")
# sensors/models.py
from .devices import TemperatureSensor, PressureSensor
TemperatureSensorDevice, TemperatureSensorReading, TemperatureSensorMessage = (
TemperatureSensor.create_models()
)
PressureSensorDevice, PressureSensorReading, PressureSensorMessage = (
PressureSensor.create_models()
)
Each device type gets its own set of database tables, MQTT topics, and API endpoints. They are all managed through the global registry.
Accessing the Registry¶
The registry provides programmatic access to all registered device types:
from django_devicehub import registry
# List all registered types
for dt_cls in registry.all():
print(dt_cls.__name__)
# Look up by name (case-insensitive)
ws = registry.get("weatherstation")
# Get models for a device type
DeviceModel, ReadingModel, MessageModel = registry.get_models("weatherstation")
# List all types with their models
for dt_cls, device_model, reading_model, message_model in registry.all_with_models():
print(f"{dt_cls.__name__}: {device_model._meta.db_table}")