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
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | Required | Unique identifier for the interaction |
display_name | str | None | Human-readable label |
current_value | Any | NotSet | Current value of the interaction |
default | Any | None | Default value when not set |
callback | callable | None | Function called when value changes |
transform_check | callable | None | Function to validate/transform new values |
show_activity | bool | None | Whether 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
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | Required | Unique identifier |
display_name | str | Required | Button label |
colour | Colour | Colour.blue | Button color |
requires_confirm | bool | True | Show confirmation dialog before execution |
disabled | bool | False | Whether 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
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | Required | Unique identifier |
display_name | str | None | Dropdown label |
user_options | list[Option] | None | Available 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
| Parameter | Type | Default | Description |
|---|---|---|---|
name | str | Required | Unique identifier |
display_name | str | None | Slider label |
min_val | int | 0 | Minimum value |
max_val | int | 100 | Maximum value |
step_size | float | 0.1 | Increment step |
dual_slider | bool | True | Enable dual handles for range selection |
inverted | bool | True | Invert slider direction |
icon | str | None | Icon to display |
colours | str | None | Comma-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 adatetimeobject (orNone)
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
| Parameter | Type | Description |
|---|---|---|
pattern | str or re.Pattern | String or regex pattern to match interaction names |
global_interaction | bool | If 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
- UIManager - Managing the UI and interactions
- Display Elements - Read-only display elements
- Containers - Organizing UI elements
- Styling - Colors and options for styling