Skip to content

Interactive Elements

Interactive elements allow users to provide input and trigger actions on your device. These elements create commands that are synchronized between the cloud and your application.

Import

from pydoover.ui import (
    # Classes
    Interaction,
    Action,
    StateCommand,
    Slider,
    Parameter,
    NumericParameter,
    TextParameter,
    BooleanParameter,
    DateTimeParameter,
    WarningIndicator,
    HiddenValue,

    # Decorators
    action,
    state_command,
    slider,
    numeric_parameter,
    text_parameter,
    boolean_parameter,
    datetime_parameter,
    warning_indicator,
    hidden_value,
    callback,
)

Interaction (Base Class)

The Interaction class is the base class for all interactive elements. It provides common functionality for handling user input and callbacks.

Constructor

Interaction(
    name: str,
    display_name: str = None,
    current_value: Any = NotSet,
    default: Any = None,
    callback: callable = None,
    transform_check: callable = None,
    show_activity: bool = None,
    **kwargs
)

Parameters

ParameterTypeDefaultDescription
namestrRequiredUnique identifier for the interaction
display_namestrNoneHuman-readable label
current_valueAnyNotSetCurrent value of the interaction
defaultAnyNoneDefault value when not set
callbackcallableNoneFunction called when value changes
transform_checkcallableNoneFunction to validate/transform new values
show_activityboolNoneWhether to log this interaction's activity

Common Methods

coerce()

Programmatically sets the interaction's value.

my_interaction.coerce(new_value)

# Mark as critical for immediate sync
my_interaction.coerce(new_value, critical=True)

Action

Actions are button elements that trigger callbacks when pressed. They are ideal for commands like "Restart", "Reset", or "Refresh".

Constructor

Action(
    name: str,
    display_name: str,
    colour: Colour = Colour.blue,
    requires_confirm: bool = True,
    disabled: bool = False,
    **kwargs
)

Parameters

ParameterTypeDefaultDescription
namestrRequiredUnique identifier
display_namestrRequiredButton label
colourColourColour.blueButton color
requires_confirmboolTrueShow confirmation dialog before execution
disabledboolFalseWhether the button is disabled

Class Example

from pydoover.ui import Action, Colour

restart_button = Action(
    name="restart",
    display_name="Restart Device",
    colour=Colour.red,
    requires_confirm=True,
    disabled=False
)

def on_restart(new_value):
    print("Restart triggered!")
    # Perform restart logic

restart_button.callback = on_restart

ui.add_children(restart_button)

Decorator Example

from pydoover.ui import action, Colour

class DeviceController:
    @action("restart", "Restart Device", colour=Colour.red, requires_confirm=True)
    def restart(self, new_value):
        print("Restarting device...")
        # Restart logic here

    @action("refresh", "Refresh Data", colour=Colour.blue, requires_confirm=False)
    def refresh_data(self, new_value):
        print("Refreshing data...")
        # Refresh logic here

controller = DeviceController()
ui.register_interactions(controller)

Real-World Example: Motor Control Actions

This example from a production motor control application shows context-aware action buttons:

from pydoover.ui import Action, Colour

# Start/stop actions with visibility control
start_now = Action(
    name="start_now",
    display_name="Start Motor",
    colour=Colour.green,
    requires_confirm=True
)

stop_now = Action(
    name="stop_now",
    display_name="Stop Motor",
    colour=Colour.red,
    requires_confirm=True
)

clear_error = Action(
    name="clear_error",
    display_name="Clear Error",
    colour=Colour.orange,
    requires_confirm=False
)

# Show only relevant actions based on state
def update_ui(self, estopped, is_running, error=None):
    # Hide all first
    self.start_now.hidden = True
    self.stop_now.hidden = True
    self.clear_error.hidden = True

    # Show based on current state
    if estopped:
        pass  # No actions available during e-stop
    elif error:
        self.clear_error.hidden = False
    elif is_running:
        self.stop_now.hidden = False
    else:
        self.start_now.hidden = False

# Callbacks
def on_start(self, new_value):
    self.user_run_start()  # Trigger state machine transition

def on_stop(self, new_value):
    self.user_run_stop()

start_now.callback = on_start
stop_now.callback = on_stop

Source: small-motor-control

Real-World Example: Alert Button with Coerce Reset

This example from a Doover application template shows an Action button that publishes to a channel and resets itself using .coerce(None):

from pydoover import ui
from pydoover.docker import Application

class SampleUI:
    def __init__(self):
        self.send_alert = ui.Action(
            "send_alert",
            "Send message as alert",
            position=1
        )
        self.test_output = ui.TextVariable(
            "test_output",
            "This is message we got"
        )

class SampleApplication(Application):
    @ui.callback("send_alert")
    async def on_send_alert(self, new_value):
        # Publish the current message to the alerts channel
        await self.publish_to_channel(
            "significantAlerts",
            self.ui.test_output.current_value
        )
        # Reset the action to prevent re-triggering on reconnect
        self.ui.send_alert.coerce(None)

Source: app-template

StateCommand

State commands present a dropdown menu of options. Users select from predefined choices, and your callback receives the selected value.

Constructor

StateCommand(
    name: str,
    display_name: str = None,
    user_options: list[Option] = None,
    **kwargs
)

Parameters

ParameterTypeDefaultDescription
namestrRequiredUnique identifier
display_namestrNoneDropdown label
user_optionslist[Option]NoneAvailable choices

Class Example

from pydoover.ui import StateCommand, Option

mode_selector = StateCommand(
    name="operating_mode",
    display_name="Operating Mode",
    user_options=[
        Option("auto", "Automatic"),
        Option("manual", "Manual"),
        Option("standby", "Standby"),
    ]
)

def on_mode_change(new_value):
    print(f"Mode changed to: {new_value}")

mode_selector.callback = on_mode_change

ui.add_children(mode_selector)

Decorator Example

from pydoover.ui import state_command, Option

class DeviceController:
    @state_command(
        "fan_speed",
        display_name="Fan Speed",
        user_options=[
            Option("off", "Off"),
            Option("low", "Low"),
            Option("medium", "Medium"),
            Option("high", "High"),
        ]
    )
    def set_fan_speed(self, new_value):
        print(f"Fan speed set to: {new_value}")
        # Apply fan speed setting

controller = DeviceController()
ui.register_interactions(controller)

Adding Options Dynamically

mode_selector.add_user_options(
    Option("eco", "Eco Mode"),
    Option("turbo", "Turbo Mode")
)

Real-World Example: Battery Charge Mode

This example from a Doover application template shows a StateCommand for selecting battery charge mode:

from pydoover import ui

class SampleUI:
    def __init__(self):
        # Charge mode dropdown with three options
        self.battery_charge_mode = ui.StateCommand(
            "battery_charge_mode",
            "Charge Mode",
            user_options=[
                ui.Option("charge", "Charging"),
                ui.Option("discharge", "Discharging"),
                ui.Option("idle", "Idle"),
            ]
        )

        # Group under battery submodule
        self.battery = ui.Submodule("battery", "Battery Module")
        self.battery.add_children(self.battery_charge_mode)

Source: app-template

Real-World Example: Pump Control Command

This example from a production pump controller shows a StateCommand for remote pump control with verbose_str for activity logging:

from pydoover.ui import StateCommand, Option

# State command with verbose identifier for logging
state_command = StateCommand(
    "pumpState",
    "Pump Should Be",
    user_options=[
        Option("shutdown", "Shutdown"),
        Option("running", "Running"),
    ],
    verbose_str=self.config.name.value,  # e.g., "Pump 1" - used in activity logs
    hidden=not self.pump_command_enabled,  # Only show when in remote mode
)

# Programmatic state control via UIManager
def reset_command_to_shutdown(self):
    self.ui_manager.coerce_command("pumpState", "shutdown")

def reset_command_to_running(self):
    self.ui_manager.coerce_command("pumpState", "running")

# Reading current command value
@property
def pump_running_command(self) -> bool:
    return self.state_command.current_value == "running"

Source: pump_station_controller

Slider

Sliders allow users to select numeric values within a range. They support single or dual handles for range selection.

Constructor

Slider(
    name: str,
    display_name: str = None,
    min_val: int = 0,
    max_val: int = 100,
    step_size: float = 0.1,
    dual_slider: bool = True,
    inverted: bool = True,
    icon: str = None,
    colours: str = None,
    **kwargs
)

Parameters

ParameterTypeDefaultDescription
namestrRequiredUnique identifier
display_namestrNoneSlider label
min_valint0Minimum value
max_valint100Maximum value
step_sizefloat0.1Increment step
dual_sliderboolTrueEnable dual handles for range selection
invertedboolTrueInvert slider direction
iconstrNoneIcon to display
coloursstrNoneComma-separated colors (e.g., "red,green,blue")

Class Example

from pydoover.ui import Slider

brightness_slider = Slider(
    name="brightness",
    display_name="Brightness",
    min_val=0,
    max_val=100,
    step_size=5,
    dual_slider=False,  # Single handle
    inverted=False
)

def on_brightness_change(new_value):
    print(f"Brightness set to: {new_value}%")

brightness_slider.callback = on_brightness_change

ui.add_children(brightness_slider)

Decorator Example

from pydoover.ui import slider

class DeviceController:
    @slider(
        "temperature_setpoint",
        display_name="Temperature Setpoint",
        min_val=15,
        max_val=30,
        step_size=0.5,
        dual_slider=False
    )
    def set_temperature(self, new_value):
        print(f"Temperature setpoint: {new_value}C")
        # Apply temperature setting

    @slider(
        "operating_range",
        display_name="Operating Range",
        min_val=0,
        max_val=100,
        step_size=1,
        dual_slider=True  # Select a range
    )
    def set_operating_range(self, new_value):
        # new_value will be a list [min, max] for dual slider
        print(f"Operating range: {new_value[0]} to {new_value[1]}")

controller = DeviceController()
ui.register_interactions(controller)

Parameters

Parameters are form inputs for collecting specific data types from users.

NumericParameter

Accepts numeric input with optional min/max constraints.

NumericParameter(
    name: str,
    display_name: str,
    min_val: Union[int, float] = None,
    max_val: Union[int, float] = None,
    **kwargs
)

Class Example

from pydoover.ui import NumericParameter

threshold_param = NumericParameter(
    name="threshold",
    display_name="Alert Threshold",
    min_val=0,
    max_val=100
)

def on_threshold_change(new_value):
    print(f"Threshold set to: {new_value}")

threshold_param.callback = on_threshold_change

Decorator Example

from pydoover.ui import numeric_parameter

class Settings:
    @numeric_parameter(
        "timeout",
        display_name="Timeout (seconds)",
        min_val=1,
        max_val=3600
    )
    def set_timeout(self, value: float):
        print(f"Timeout set to: {value} seconds")

Real-World Example: Engine RPM Target

This example from a production pump controller shows a NumericParameter for setting engine RPM with bounded values and verbose logging:

from pydoover.ui import NumericParameter

# RPM target with safe limits
target_rpm = NumericParameter(
    "pumpTargetRPM",
    "Target RPM",
    min_val=800,    # Minimum safe engine speed
    max_val=2000,   # Maximum rated speed
    default=900,    # Safe starting value
    verbose_str=f"{self.config.name.value} RPM",  # e.g., "Pump 1 RPM"
    hidden=not self.target_rpm_enabled,  # Hide when not in auto mode
)

# Coerce RPM to prevent dangerous jumps
def coerce_rpm_cmd_to_current(self):
    """Prevent large RPM changes by coercing to current value."""
    curr_rpm = self.k37_state.rpm
    target = self.target_rpm.current_value or 0

    if curr_rpm is None or curr_rpm < 800:
        curr_rpm = 800

    # Only coerce if difference is significant
    if abs(target - curr_rpm) > 30:
        self.target_rpm.coerce(curr_rpm)

Source: pump_station_controller

TextParameter

Accepts text input, optionally as a multi-line text area.

TextParameter(
    name: str,
    display_name: str,
    is_text_area: bool = False,
    **kwargs
)

Class Example

from pydoover.ui import TextParameter

name_param = TextParameter(
    name="device_name",
    display_name="Device Name",
    is_text_area=False
)

notes_param = TextParameter(
    name="notes",
    display_name="Notes",
    is_text_area=True  # Multi-line input
)

Decorator Example

from pydoover.ui import text_parameter

class Settings:
    @text_parameter("description", display_name="Description", is_text_area=True)
    def set_description(self, value: str):
        print(f"Description updated: {value}")

Real-World Example: Message Input with Callback

This example from a Doover application template shows a TextParameter that updates a display variable when the user enters text:

from pydoover import ui
from pydoover.docker import Application

class SampleUI:
    def __init__(self):
        self.test_message = ui.TextParameter(
            "test_message",
            "Put in a message"
        )
        self.test_output = ui.TextVariable(
            "test_output",
            "This is message we got"
        )

class SampleApplication(Application):
    @ui.callback("test_message")
    async def on_text_parameter_change(self, new_value):
        # Update the display variable with the new message
        self.ui.test_output.update(new_value)

Source: app-template

DateTimeParameter

Accepts date/time input via a picker interface.

DateTimeParameter(
    name: str,
    display_name: str,
    include_time: bool = False,
    **kwargs
)

Properties

  • current_value: Returns the current value as a datetime object (or None)

Class Example

from pydoover.ui import DateTimeParameter

schedule_time = DateTimeParameter(
    name="schedule",
    display_name="Schedule Time",
    include_time=True  # Include time picker
)

date_only = DateTimeParameter(
    name="start_date",
    display_name="Start Date",
    include_time=False  # Date only
)

Decorator Example

from pydoover.ui import datetime_parameter
from datetime import datetime

class Scheduler:
    @datetime_parameter(
        "next_run",
        display_name="Next Run Time",
        include_time=True
    )
    def set_next_run(self, value: datetime):
        print(f"Next run scheduled for: {value}")

BooleanParameter

Note: This parameter type is not yet implemented in the Doover site and will raise NotImplementedError if used.

WarningIndicator

Displays a warning that can optionally be dismissed by the user.

Constructor

WarningIndicator(
    name: str,
    display_name: str,
    can_cancel: bool = True,
    **kwargs
)

Example

from pydoover.ui import WarningIndicator, warning_indicator

# Class-based
warning = WarningIndicator(
    name="low_battery_warning",
    display_name="Low Battery",
    can_cancel=True
)

# Decorator-based
class Alerts:
    @warning_indicator("overheat", display_name="Overheat Warning", can_cancel=False)
    def on_overheat_dismissed(self, new_value):
        print("Overheat warning acknowledged")

Real-World Example: Motor Control Warnings

This example from a production motor control application shows warnings with visibility tied to application state:

from pydoover.ui import WarningIndicator, Submodule

# E-stop warning - cannot be dismissed (hardware must be cleared)
estop_warning = WarningIndicator(
    name="estop_warning",
    display_name="Emergency Stop Active",
    can_cancel=False  # User cannot dismiss - requires physical action
)

# Error warning - can be cleared after resolving issue
error_warning = WarningIndicator(
    name="error_warning",
    display_name="Motor Error - Start Timeout",
    can_cancel=True
)

# Group warnings
warnings_module = Submodule(
    name="warnings",
    display_name="Warnings",
    children=[estop_warning, error_warning]
)

# Control visibility based on application state
def update_warnings(self, estopped, error=None):
    # Hide all warnings first
    self.estop_warning.hidden = True
    self.error_warning.hidden = True

    # Show relevant warnings
    if estopped:
        self.estop_warning.hidden = False
    elif error:
        self.error_warning.hidden = False
        self.error_warning.display_name = f"Motor Error - {error}"

Source: small-motor-control

HiddenValue

Stores values that are synchronized but not displayed to users. Useful for internal state tracking.

Constructor

HiddenValue(name: str, **kwargs)

Example

from pydoover.ui import HiddenValue, hidden_value

# Class-based
session_id = HiddenValue(name="session_id")
session_id.current_value = "abc123"

# Decorator-based
class SessionManager:
    @hidden_value("last_sync_time")
    def on_sync_time_update(self, new_value):
        print(f"Sync time updated: {new_value}")

Callback Decorator

The @callback decorator registers functions to handle interaction changes. It supports pattern matching for flexible event handling.

Usage

from pydoover.ui import callback
import re

class Controller:
    # Single interaction callback
    @callback("restart_button")
    def on_restart(self, element, new_value):
        print(f"Restart triggered")

    # Regex pattern callback
    @callback(re.compile(r"sensor_\d+_value"))
    def on_any_sensor(self, element, new_value):
        print(f"{element.name} changed to {new_value}")

    # Global interaction (not namespaced by app)
    @callback("camera_get_now", global_interaction=True)
    def on_camera_request(self, element, new_value):
        print("Camera snapshot requested")

controller = Controller()
ui.register_callbacks(controller)

Parameters

ParameterTypeDescription
patternstr or re.PatternString or regex pattern to match interaction names
global_interactionboolIf True, match against global interaction names without app prefix

Complete Example

from pydoover.ui import (
    UIManager,
    Action,
    StateCommand,
    Slider,
    NumericParameter,
    TextParameter,
    DateTimeParameter,
    HiddenValue,
    Submodule,
    Option,
    Colour,
    action,
    state_command,
    slider,
    callback,
)
from datetime import datetime

class DeviceController:
    def __init__(self, client):
        self.ui = UIManager(app_key="device_ctrl", client=client)
        self.setup_ui()
        self.ui.register_interactions(self)
        self.ui.register_callbacks(self)
        self.ui.start_comms()

    def setup_ui(self):
        # Control actions
        controls = Submodule(
            name="controls",
            display_name="Device Controls",
            children=[]
        )
        self.ui.add_children(controls)

    # Action button
    @action("restart", "Restart Device", colour=Colour.red, requires_confirm=True)
    def restart_device(self, new_value):
        print("Restarting device...")

    @action("calibrate", "Calibrate Sensors", colour=Colour.blue)
    def calibrate(self, new_value):
        print("Calibrating...")

    # State command (dropdown)
    @state_command(
        "mode",
        display_name="Operating Mode",
        user_options=[
            Option("auto", "Automatic"),
            Option("manual", "Manual"),
            Option("eco", "Eco Mode"),
        ]
    )
    def set_mode(self, new_value):
        print(f"Mode set to: {new_value}")

    # Slider
    @slider(
        "setpoint",
        display_name="Temperature Setpoint",
        min_val=15,
        max_val=35,
        step_size=0.5,
        dual_slider=False
    )
    def set_temperature(self, new_value):
        print(f"Setpoint: {new_value}C")

    # Generic callback for multiple elements
    @callback("mode")
    def on_mode_callback(self, element, new_value):
        print(f"Callback: mode changed to {new_value}")

# Usage
controller = DeviceController(my_client)

# Main loop
async def main_loop():
    while True:
        await controller.ui.handle_comms_async()
        await asyncio.sleep(1)

Common Properties

Interactive elements share common properties with display elements, including position, hidden, help_str, and verbose_str. See Common Element Properties for details.

See Also