Skip to content

Interactive Elements

Interactive elements accept user input from the Doover portal and trigger actions on the device. All interactive elements inherit from Interaction, which provides the base value management and handler routing.

Interaction Base

Every interactive element shares these capabilities from the Interaction base class:

  • value property -- reads the current value from the UICommandsManager
  • set(value, max_age, log_update) -- updates the element's value in the portal
  • handler(ctx, payload) -- called when the user interacts with the element; default implementation calls set(payload)
  • requires_confirm -- when set to True or a ConfirmDialog, shows a confirmation dialog before executing
  • global_interaction -- marks the interaction as device-global rather than app-scoped

Button

A clickable button that fires a handler when pressed.

from pydoover.ui import Button

restart_btn = Button("Restart System")

# With disabled state and label
fetch_btn = Button(
    "Fetch Data",
    disabled=False,
    label_string="Click to fetch latest data",
)
ParameterDescription
display_nameLabel shown on the button
disabledWhether the button appears greyed out
label_stringAdditional descriptive text shown near the button

Switch

A toggle switch for on/off control. Inherits directly from Interaction with no additional parameters.

from pydoover.ui import Switch

pump_toggle = Switch("Pump Control")

The value is typically a boolean. When the user toggles the switch, the handler receives the new state.

Select

A dropdown menu with a fixed set of options.

from pydoover.ui import Select, Option

mode_select = Select(
    "Operating Mode",
    options=[
        Option("Automatic"),
        Option("Manual"),
        Option("Standby"),
    ],
)

Each Option has a display_name shown in the dropdown. The name (the internal key) is auto-generated by sanitising the display name. When the user selects an option, the handler receives the option's name as the payload.

ParameterDescription
display_nameLabel for the dropdown
optionsList of Option instances

Slider

A range input with configurable bounds, step size, and visual options.

from pydoover.ui import Slider

brightness = Slider(
    "Brightness",
    min_val=0,
    max_val=100,
    step_size=1,
    dual_slider=False,
    inverted=False,
)
ParameterDescription
display_nameLabel for the slider
min_valMinimum value (default: 0)
max_valMaximum value (default: 100)
step_sizeIncrement size (default: 0.1)
dual_sliderWhether the slider has two handles for range selection (default: True)
invertedWhether the slider direction is inverted (default: True)
coloursColour string for the slider track
Information Circle
Example — Dual vs Single Slider

A monitoring dashboard uses dual sliders for alarm thresholds (min/max range) and a single slider for a device setting:

from pydoover import ui

alarms = ui.Submodule(
    "Alarms",
    children=[
        ui.Slider(
            "Humidity Alarm",
            units="%",
            min_val=30,
            max_val=95,
            step_size=0.1,
            dual_slider=True,
            inverted=False,
            icon="fa-regular fa-bell",
            show_activity=False,
            default=[30, 95],
            name="humidity_alarm",
        ),
        ui.Slider(
            "Temperature Alarm",
            units="°C",
            min_val=-10,
            max_val=50,
            step_size=0.1,
            dual_slider=True,
            inverted=False,
            icon="fa-regular fa-bell",
            show_activity=False,
            default=[0, 30],
            name="temp_alarm",
        ),
    ],
)

sleep_slider = ui.Slider(
    "Sleep Time",
    units="hrs",
    min_val=0.25,
    max_val=12,
    step_size=0.25,
    dual_slider=False,
    icon="fa-regular fa-bed",
    show_activity=True,
    default=6,
    name="sleep_time",
)

When dual_slider=True, the default value is a list of two numbers [min, max], and the handler receives a tuple. When dual_slider=False, it's a single number.

WarningIndicator

Displays a warning state that the user can optionally dismiss.

from pydoover.ui import WarningIndicator

alarm = WarningIndicator("High Temperature Alarm", can_cancel=True)
ParameterDescription
display_nameWarning message text
can_cancelWhether the user can dismiss the warning (default: True)

FloatInput

A numeric input field with optional min/max bounds.

from pydoover.ui import FloatInput

setpoint = FloatInput(
    "Temperature Setpoint",
    min_val=0,
    max_val=100,
    default=25,
)
ParameterDescription
display_nameLabel for the input
min_valMinimum accepted value
max_valMaximum accepted value
defaultDefault value when no user value exists

TextInput

A text input field, optionally rendered as a larger text area.

from pydoover.ui import TextInput

device_name = TextInput("Device Name")

notes = TextInput("Notes", is_text_area=True)
ParameterDescription
display_nameLabel for the input
is_text_areaWhen True, renders as a multi-line text area (default: False)

DatetimeInput

A date and time picker. Internally stores values as epoch seconds in UTC.

from pydoover.ui import DatetimeInput

schedule_start = DatetimeInput("Schedule Start", include_time=True)

date_only = DatetimeInput("Calibration Date", include_time=False)
ParameterDescription
display_nameLabel for the picker
include_timeWhether to show the time picker in addition to date (default: False)

TimeInput

A time-only picker (no date component). This is a convenience subclass of DatetimeInput with include_time=True.

from pydoover.ui import TimeInput

daily_report_time = TimeInput("Daily Report Time")

Confirmation Dialogs

Any interactive element can require user confirmation before executing — essential for safety-critical controls like engine starts, pump shutdowns, or parameter changes that affect running equipment. Set requires_confirm to True for a default dialog, or pass a ConfirmDialog for customisation.

from pydoover.ui import Button, ConfirmDialog, Colour

shutdown_btn = Button(
    "Shutdown System",
    requires_confirm=ConfirmDialog(
        title="Confirm Shutdown",
        subtitle="The system will go offline",
        warning_reason="This will stop all running processes",
        colour=Colour.red,
        icon="alert-triangle",
    ),
)

See Styling for ConfirmDialog details.

Handling Interactions

When a user interacts with an element in the portal, the event flows to your application through the UICommandsManager. You handle events using the @handler() decorator on your application class.

The @handler() Decorator

Mark an async method as a handler for a specific interaction. The method receives an InteractionContext and the payload (the new value).

from pydoover.docker import Application
from pydoover.ui import handler, InteractionContext, Button

class MyUI(UI):
    restart_btn = Button("Restart System")
    setpoint = FloatInput("Setpoint", min_val=0, max_val=100)

class MyApp(Application):
    ui_cls = MyUI

    @handler("restart_btn")
    async def on_restart(self, ctx: InteractionContext, payload):
        await self.tags.status.set("restarting")
        self.request_shutdown()

    @handler("setpoint", auto_update=False)
    async def on_setpoint(self, ctx: InteractionContext, payload):
        if payload < 10:
            return  # Reject values below 10
        await ctx.set_value(payload)  # Manually update

You can also pass the interaction element directly instead of its name:

@handler(MyUI.restart_btn)
async def on_restart(self, ctx, payload):
    pass

Handler Parameters

ParameterDescription
interactionThe interaction name (string), Interaction instance, or regex pattern to match
parserOptional function to transform the payload before the handler receives it
auto_updateWhen True (default), automatically updates the element's value with the payload after the handler returns successfully

InteractionContext

The handler's ctx argument is an InteractionContext that provides:

Property/MethodDescription
ctx.elementThe Interaction instance that was triggered
ctx.interactionAlias for ctx.element
ctx.methodThe interaction name string
ctx.messageThe raw channel message that triggered the handler
ctx.set_value(value, max_age, log_update)Update the interaction's value in the portal

Module-Qualified Form

The handler decorator is also available as ui.handler() when importing the ui module directly. This form works in both Docker applications and cloud processors.

from pydoover import ui
from pydoover.processor import Application

class MyProcessor(Application):
    @ui.handler("sleep_time", parser=float)
    async def on_sleep_time_change(self, ctx, payload: float):
        await self._apply_sleep_time(max(0.25, payload))

    @ui.handler("selected_crop", parser=str)
    async def on_crop_change(self, ctx, payload: str):
        await self.tags.app_display_name.set(f"Monitor - {payload}")
Information Circle
Example — Docker Application Handlers

An engine controller uses @ui.handler() in a Docker application to send modbus commands when operators press buttons. The parser=int argument converts the slider payload to an integer before the handler receives it:

from pydoover import ui
from pydoover.docker import Application

class EngineControllerApp(Application):
    @ui.handler("start_pump")
    async def on_start_pump(self, ctx, value):
        if not self.config.enable_remote_control.value:
            return
        if await self.write_start():
            await self.create_message(
                "activity_logs",
                {"message": f"{self.app_display_name}: remote START sent"},
            )

    @ui.handler("target_rpm", parser=int)
    async def on_target_rpm(self, ctx, value: int):
        if not self.config.enable_remote_control.value:
            return
        rpm_min = int(self.config.target_rpm_min.value)
        rpm_max = int(self.config.target_rpm_max.value)
        rpm = max(rpm_min, min(rpm_max, value))
        await self.write_target_rpm(rpm)
Information Circle
Example — Cloud Processor Handlers

A cloud processor uses @ui.handler() with parser to handle selection changes and slider adjustments from the portal:

from pydoover import ui

@ui.handler("sleep_time", parser=float)
async def on_sleep_time_change(self, ctx, payload: float):
    await self._publish_downlink_config(sleep_hours=max(0.25, payload))

@ui.handler("operating_mode", parser=str)
async def on_mode_change(self, ctx, payload: str):
    await self.tags.app_display_name.set(f"Monitor - {payload}")
    for elem in self.config.sensor_labels.elements:
        s_id = elem.id.value
        await self.recalculate_derived_values(s_id, payload)

Default Handler

If no @handler() is registered for an interaction, the base Interaction.handler() method is called, which simply sets the element's value to the received payload. This means simple interactions (like a Select that just stores the selected option) work without any custom handler code.

Next Steps