Skip to content

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
        )

Related