Skip to content

Applications Overview

The pydoover Docker application framework provides a structured way to build IoT applications that run on Doover-powered devices. Applications built with this framework can communicate with the Doover cloud, interact with hardware I/O, and communicate with Modbus devices.

Architecture

A pydoover application consists of several key components that work together:

  • Application Class: The main entry point that orchestrates your application logic
  • DeviceAgentInterface: Handles communication with the Doover cloud via the Device Agent
  • PlatformInterface: Provides access to hardware I/O (digital inputs/outputs, analog inputs/outputs)
  • ModbusInterface: Enables communication with Modbus devices over serial or TCP

Core Concepts

Application Lifecycle

Every pydoover application follows a consistent lifecycle:

  1. Initialization: The application is created with a configuration schema
  2. Setup: The setup() method is called once to initialize resources
  3. Main Loop: The main_loop() method runs continuously at a configurable interval
  4. Shutdown: Cleanup occurs when the application terminates

Configuration

Applications use a configuration schema to define their settings. This allows for runtime configuration through the Doover cloud:

from pydoover.config import Schema, Integer, Boolean, String

class MyAppConfig(Schema):
    def __init__(self):
        self.poll_interval = Integer("Poll Interval", default=5, min_val=1)
        self.enable_logging = Boolean("Enable Logging", default=True)
        self.device_name = String("Device Name", default="sensor-1")

Tags

Tags are key-value pairs that provide a simple way to share state between applications and with the cloud:

# Set a tag for the current application
self.set_tag("temperature", 25.5)

# Get a tag value
temp = self.get_tag("temperature")

# Get a tag from another application
other_value = self.get_tag("status", app_key="other-app-key")

# Set a global tag (shared across all applications)
self.set_global_tag("system_status", "online")

Creating Applications with doover-cli

The recommended way to create new applications is using the doover-cli tool:

# Install doover-cli
uv tool install doover-cli

# Create a new application
doover app create

The interactive wizard will guide you through setting up:

  • Application name and description
  • Container registry configuration
  • Organization settings

This creates a complete project structure including Docker configuration, simulators for local testing, and deployment scripts. See the Doover CLI documentation for the full development workflow.

Getting Started

Minimal Application

Here is a minimal pydoover application:

from pydoover.docker import Application, run_app
from pydoover.config import Schema

class MinimalApp(Application):
    def setup(self):
        print("Application starting...")

    def main_loop(self):
        print("Running main loop...")

if __name__ == "__main__":
    run_app(MinimalApp(config=Schema()))

Application with Configuration

A more complete example with configuration:

from pydoover.docker import Application, run_app
from pydoover.config import Schema, Integer, Boolean

class SensorConfig(Schema):
    def __init__(self):
        self.num_sensors = Integer("Sensor Count", default=4, min_val=1)
        self.output_enabled = Boolean("Output Enabled", default=True)

class SensorApp(Application):
    config: SensorConfig  # Type hint for IDE autocomplete

    def setup(self):
        self.set_tag("app_ready", True)
        print(f"Monitoring {self.config.num_sensors.value} sensors")

    def main_loop(self):
        for i in range(self.config.num_sensors.value):
            status = self.get_di(i)
            print(f"Sensor {i}: {'active' if status else 'inactive'}")

if __name__ == "__main__":
    run_app(SensorApp(config=SensorConfig()))

Application Attributes

The Application class provides access to several important attributes:

AttributeTypeDescription
configSchemaThe configuration schema for the application
device_agentDeviceAgentInterfaceInterface for cloud communication
platform_ifacePlatformInterfaceInterface for hardware I/O
modbus_ifaceModbusInterfaceInterface for Modbus communication
ui_managerUIManagerManager for user interface elements
app_keystrUnique identifier for this application
loop_target_periodfloatTarget interval for main loop execution (default: 1 second)

Loop Timing

You can control how fast the main loop runs by setting loop_target_period:

class FastApp(Application):
    def setup(self):
        self.loop_target_period = 0.5  # Run every 500ms

    def main_loop(self):
        # This runs every 500ms
        pass

The framework will automatically adjust timing to maintain the target interval, and will log warnings if the loop consistently runs slower than the target.

Async Support

Both setup() and main_loop() can be synchronous or asynchronous:

class AsyncApp(Application):
    async def setup(self):
        await self.some_async_initialization()

    async def main_loop(self):
        result = await self.fetch_data_async()
        await self.set_tag_async("result", result)

Shutdown Handling

Applications can implement custom shutdown logic:

class SafeApp(Application):
    async def on_shutdown_at(self, dt):
        """Called when a shutdown is scheduled."""
        print(f"Shutdown scheduled for {dt}")
        # Perform pre-shutdown tasks

    async def check_can_shutdown(self) -> bool:
        """Called to check if the application can safely shutdown."""
        # Check if it's safe to shutdown
        if self.get_do(0):  # If output is still high
            return False  # Don't shutdown yet
        return True

Next Steps