Advanced Configuration Patterns
This guide covers advanced configuration patterns in pydoover, including nested structures, dynamic references, and complex validation scenarios.
Nested Object Configuration
Objects can contain other objects to create deeply nested configuration structures:
from pydoover import config
class NetworkConfig(config.Schema):
def __init__(self):
# Top-level object
self.mqtt = config.Object(
"MQTT Settings",
description="MQTT broker configuration"
)
# Add nested properties
self.mqtt.add_elements(
config.String("Broker Host", default="localhost"),
config.Integer("Port", default=1883, minimum=1, maximum=65535),
)
# Nested object within object
self.mqtt.auth = config.Object(
"Authentication",
collapsible=True,
default_collapsed=True
)
self.mqtt.auth.add_elements(
config.String("Username", default=""),
config.String("Password", default="", hidden=True),
)
Accessing Nested Values
class MyApp(Application):
config = NetworkConfig()
def setup(self):
host = self.config.mqtt.broker_host.value
port = self.config.mqtt.port.value
username = self.config.mqtt.auth.username.value
password = self.config.mqtt.auth.password.value
Arrays of Objects
Combine Array and Object elements for lists of complex items:
class SensorArrayConfig(config.Schema):
def __init__(self):
# Define the item template
sensor_template = config.Object("Sensor")
sensor_template.add_elements(
config.String("Name", description="Sensor identifier"),
config.Integer("Pin", minimum=0, maximum=40),
config.Enum(
"Type",
choices=["Temperature", "Humidity", "Pressure"],
default="Temperature"
),
config.Number("Calibration Offset", default=0.0),
)
# Array of sensor objects
self.sensors = config.Array(
"Sensors",
element=sensor_template,
min_items=1,
max_items=8,
description="Configure up to 8 sensors"
)
Iterating Over Object Arrays
class MyApp(Application):
config = SensorArrayConfig()
def setup(self):
for sensor in self.config.sensors.elements:
name = sensor.name.value
pin = sensor.pin.value
sensor_type = sensor.type.value
offset = sensor.calibration_offset.value
print(f"Configuring {name} on pin {pin}")
Dynamic Variables
Use Variable to create references that resolve at deployment time:
class AppConfig(config.Schema):
def __init__(self):
# Reference device-level settings
self.device_name = config.String(
"Device Name",
default=config.Variable("device", "name"),
description="Inherits from device configuration"
)
# Reference another application's config
self.data_endpoint = config.String(
"Data Endpoint",
default=config.Variable("data_collector", "api_url")
)
Variable Resolution
Variables follow the format $scope.key:
scope: The application name or context (e.g., "device", "my_app")key: The configuration key within that scope
The Doover platform resolves these references when deploying the application.
Application References
Reference other installed applications using the Application element:
class DataProcessorConfig(config.Schema):
def __init__(self):
# Select a data source application
self.source_app = config.Application(
"Data Source",
description="Application that provides input data"
)
# Select a destination application
self.destination_app = config.Application(
"Data Destination",
description="Application to send processed data to"
)
This renders as a dropdown in the UI showing all installed applications.
Conditional Configuration
While pydoover does not have built-in conditional logic in schemas, you can handle conditional configuration in your application code:
class PumpConfig(config.Schema):
def __init__(self):
self.pump_type = config.Enum(
"Pump Type",
choices=["DC", "Stepper", "Servo"],
default="DC"
)
# DC pump settings
self.dc_settings = config.Object("DC Settings")
self.dc_settings.add_elements(
config.Integer("PWM Pin", default=18),
config.Number("Max Duty Cycle", default=0.8, minimum=0, maximum=1),
)
# Stepper settings
self.stepper_settings = config.Object("Stepper Settings")
self.stepper_settings.add_elements(
config.Integer("Step Pin", default=12),
config.Integer("Direction Pin", default=13),
config.Integer("Steps Per Rev", default=200),
)
class PumpApp(Application):
config = PumpConfig()
def setup(self):
pump_type = self.config.pump_type.value
if pump_type == "DC":
pwm_pin = self.config.dc_settings.pwm_pin.value
max_duty = self.config.dc_settings.max_duty_cycle.value
# Initialize DC pump
elif pump_type == "Stepper":
step_pin = self.config.stepper_settings.step_pin.value
dir_pin = self.config.stepper_settings.direction_pin.value
# Initialize stepper
Validation Patterns
Range Validation
Use numeric constraints for validated ranges:
class CalibrationConfig(config.Schema):
def __init__(self):
# Temperature must be between -40 and 85
self.temp_min = config.Number(
"Minimum Temperature",
default=-40.0,
minimum=-40.0,
maximum=85.0
)
# Must be greater than 0 (exclusive)
self.sample_rate = config.Number(
"Sample Rate",
default=1.0,
exclusive_minimum=0.0,
description="Samples per second (must be positive)"
)
# Must be a multiple of 10
self.batch_size = config.Integer(
"Batch Size",
default=100,
multiple_of=10,
minimum=10
)
Pattern Validation
Use regex patterns for string validation:
class NetworkConfig(config.Schema):
def __init__(self):
# IPv4 address pattern
self.ip_address = config.String(
"IP Address",
default="192.168.1.1",
pattern=r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
)
# MAC address pattern
self.mac_address = config.String(
"MAC Address",
pattern=r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$"
)
# Semantic version pattern
self.firmware_version = config.String(
"Firmware Version",
pattern=r"^\d+\.\d+\.\d+$",
description="Format: major.minor.patch"
)
UI Customization
Collapsible Sections
Use Object elements with collapsible settings to organize complex configurations:
class ComplexConfig(config.Schema):
def __init__(self):
# Always visible settings
self.device_name = config.String("Device Name", default="Device-01")
# Collapsible advanced section (starts expanded)
self.network = config.Object(
"Network Settings",
collapsible=True,
default_collapsed=False
)
self.network.add_elements(
config.String("SSID"),
config.String("Password", hidden=True),
)
# Collapsed by default (advanced users only)
self.debug = config.Object(
"Debug Options",
collapsible=True,
default_collapsed=True,
description="Advanced debugging options"
)
self.debug.add_elements(
config.Boolean("Verbose Logging", default=False),
config.Integer("Log Level", default=1, minimum=0, maximum=5),
)
Hidden Elements
Use hidden=True for internal configuration that should not appear in the UI:
class AppConfig(config.Schema):
def __init__(self):
# Visible to users
self.name = config.String("Name", default="My Device")
# Hidden from UI but still configurable via API/deployment
self.internal_id = config.String(
"Internal ID",
default="auto-generated",
hidden=True
)
# Deprecated but still functional
self.legacy_setting = config.Integer(
"Legacy Setting",
default=0,
deprecated=True,
description="Use 'new_setting' instead"
)
Element Positioning
Override automatic ordering with explicit positions:
class OrderedConfig(config.Schema):
def __init__(self):
# These will appear in position order, not definition order
self.third = config.String("Third Item", default="c", position=2)
self.first = config.String("First Item", default="a", position=0)
self.second = config.String("Second Item", default="b", position=1)
Complete Example
Here is a comprehensive example combining multiple advanced patterns:
import enum
from pydoover import config
class PumpType(enum.Enum):
CENTRIFUGAL = "Centrifugal"
PERISTALTIC = "Peristaltic"
DIAPHRAGM = "Diaphragm"
class IrrigationConfig(config.Schema):
def __init__(self):
# Basic settings
self.system_name = config.String(
"System Name",
default="Irrigation Controller"
)
# Zone configuration (array of objects)
zone_template = config.Object("Zone")
zone_template.add_elements(
config.String("Name"),
config.Integer("Valve Pin", minimum=0, maximum=40),
config.Number("Flow Rate", default=1.0, minimum=0.1),
config.Boolean("Enabled", default=True),
)
self.zones = config.Array(
"Irrigation Zones",
element=zone_template,
min_items=1,
max_items=16
)
# Pump settings (nested object)
self.pump = config.Object(
"Pump Configuration",
collapsible=True
)
self.pump.add_elements(
config.Enum("Type", choices=PumpType, default=PumpType.CENTRIFUGAL),
config.Integer("Control Pin", minimum=0, maximum=40),
config.Number("Prime Duration", default=5.0, minimum=0),
)
# Schedule reference
self.schedule_app = config.Application(
"Schedule Source",
description="Application providing irrigation schedule"
)
# Advanced settings (collapsed by default)
self.advanced = config.Object(
"Advanced Settings",
collapsible=True,
default_collapsed=True
)
self.advanced.add_elements(
config.Number(
"Pressure Threshold",
default=2.5,
minimum=0,
maximum=10,
description="Bar"
),
config.Boolean("Auto Shutoff", default=True),
config.Integer("Retry Count", default=3, minimum=0, maximum=10),
)
# Hidden internal config
self.firmware_version = config.String(
"Firmware Version",
default=config.Variable("device", "firmware_version"),
hidden=True
)