Skip to content

Tag Types and Auto-Logging

pydoover provides three typed tag classes and a RemoteTag for cross-app references. Each typed tag accepts an optional log_on parameter that configures automatic data logging triggers. When a trigger fires, the value update is promoted to an immediate logged data point instead of waiting for the next periodic log flush.

Tag (Generic)

Tag is the base class for all tag types. Use it directly when you need a tag without typed auto-logging, or for complex types like objects and arrays.

from pydoover.tags import Tag, Tags

class DeviceTags(Tags):
    battery_voltage = Tag("number", default=None)
    last_alert_time = Tag("number", default=None)
    prev_sensor_readings = Tag("object", default={})
    app_display_name = Tag("string", default="My App")
ParameterDescription
tag_typeThe type string: "number", "integer", "string", "boolean", or "object"
defaultDefault value when no runtime value has been set
nameOptional override for the tag key (defaults to the attribute name)

Use "integer" when the tag stores whole numbers — pydoover coerces float-typed JSON values (e.g., 3.0) back to int automatically when the tag type is "integer".

Tag does not support log_on triggers — use the typed subclasses (Number, Boolean, String) when you need auto-logging.

Number

Number represents a numeric tag. It supports crossing-based auto-logging triggers that fire when values pass thresholds in specific directions.

from pydoover.tags import Tags, Number, Cross

class SensorTags(Tags):
    temperature = Number(default=0.0)
    pressure = Number(default=101.3, log_on=Cross(50, 100, 150))

The default parameter is the value returned when no runtime value has been set. The name parameter optionally overrides the attribute name used as the tag key.

Crossing Triggers

Crossing triggers monitor numeric values against thresholds. All crossing triggers accept a deadband parameter that adds hysteresis to suppress repeat logs when values oscillate near a threshold.

Cross

Fires when the value crosses any threshold in either direction (rising or falling).

from pydoover.tags import Number, Cross

# Log when temperature crosses 30 or 50 in either direction
temperature = Number(log_on=Cross(30, 50))

# With deadband: the value must move at least 1.0 beyond the
# threshold before a crossing is registered
temperature = Number(log_on=Cross(30, 50, deadband=1.0))

With a deadband of 1.0 and a threshold of 30, a rising crossing only fires when the value reaches 30.5 (threshold + deadband/2). A subsequent falling crossing only fires when the value drops to 29.5 (threshold - deadband/2). This prevents rapid toggling when the value hovers near a threshold.

Rise

Fires only on rising crossings (value moving from below to above a threshold). Useful for high-side alarms.

from pydoover.tags import Number, Rise

# Log when pressure rises above 150
pressure = Number(log_on=Rise(150))

Fall

Fires only on falling crossings (value moving from above to below a threshold). Useful for low-side alarms.

from pydoover.tags import Number, Fall

# Log when battery voltage drops below 11.5
battery_voltage = Number(log_on=Fall(11.5, deadband=0.2))

Delta

Fires when the value moves far enough from the last logged value. Unlike crossing triggers, Delta does not track threshold sides. It compares each new value against the last value that caused a log. The first set() always fires to seed the baseline.

Exactly one of amount or percent must be provided.

from pydoover.tags import Number, Delta

# Log when the value changes by at least 5 units
flow_rate = Number(log_on=Delta(amount=5))

# Log when the value changes by at least 10%
humidity = Number(log_on=Delta(percent=10))

When using percent and the last logged value is 0, any non-zero new value fires. This ensures the baseline shifts off zero so percentage comparisons can resume normally.

Combining Triggers

You can combine multiple triggers by passing a list to log_on. An update fires an immediate log if any trigger in the list returns True. Every trigger is always evaluated so its internal state stays consistent.

from pydoover.tags import Number, Rise, Fall, Delta

# Log on high alarm, low alarm, or significant change
tank_level = Number(
    default=0.0,
    log_on=[
        Rise(90),           # High level alarm
        Fall(10),           # Low level alarm
        Delta(percent=5),   # Log any 5% change
    ],
)

Boolean

Boolean represents a true/false tag. It supports state transition triggers.

from pydoover.tags import Tags, Boolean, AnyChange

class StatusTags(Tags):
    pump_running = Boolean(default=False, log_on=AnyChange())
    alarm_active = Boolean(default=False)

State Triggers

AnyChange

Fires on every value transition. If the previous value equals the new value, no log is produced.

from pydoover.tags import Boolean, AnyChange

# Log every time the pump turns on or off
pump_running = Boolean(log_on=AnyChange())

Enter

Fires only when the tag transitions to a specific value.

from pydoover.tags import Boolean, Enter

# Log only when the alarm becomes active
alarm_active = Boolean(log_on=Enter(True))

Exit

Fires only when the tag transitions away from a specific value.

from pydoover.tags import Boolean, Exit

# Log when the system leaves maintenance mode
maintenance_mode = Boolean(log_on=Exit(True))

String

String represents a text tag. It supports the same state transition triggers as Boolean: AnyChange, Enter, and Exit.

from pydoover.tags import Tags, String, Enter

class DeviceTags(Tags):
    operating_mode = String(
        default="normal",
        log_on=[Enter("error"), Enter("maintenance")],
    )

This configuration logs a data point whenever the operating mode enters either the "error" or "maintenance" state.

RemoteTag

RemoteTag references a tag published by another application. It enables cross-app data sharing without tight coupling between applications.

from pydoover.tags import Tags, RemoteTag

class MyTags(Tags):
    upstream_temperature = RemoteTag(
        "number",
        reference_name="temp_source",
        republish_locally=True,
    )

How RemoteTag Resolution Works

A RemoteTag does not hard-code which application it reads from. Instead, it uses a reference_name that must match a TagRef element in the application's configuration schema. The TagRef config element holds the actual app name and tag name, which the operator fills in when deploying the application.

At setup time, the framework calls _resolve_remote_tags() which:

  1. Scans the config schema for all TagRef elements
  2. Matches each RemoteTag's reference_name to a TagRef's reference_name
  3. Records the resolved target (app_key and tag_name) for runtime reads and writes

Parameters

ParameterDescription
tag_typeThe expected type of the upstream tag (e.g., "number", "boolean", "string")
reference_nameMust match a TagRef config element's reference_name
republish_locallyWhen True (default), mirrors upstream changes into this app's namespace so other local consumers (UIs, downstream tags) can read it as a local tag
defaultValue returned when the upstream tag has no value
optionalWhen True, gracefully handles missing config. If the matching TagRef is left blank, reads return None (or the declared default) and writes are silently dropped

Optional Remote Tags

Mark a RemoteTag as optional=True when the upstream source may not always be configured. This avoids errors during resolution when the operator has not filled in the corresponding TagRef.

from pydoover.tags import RemoteTag

# This won't raise an error if the TagRef is left blank
weather_data = RemoteTag(
    "number",
    reference_name="weather_source",
    optional=True,
    default=0.0,
)
Information Circle

Cross-agent RemoteTag references (where the TagRef specifies an agent_id targeting a different Doover agent) are accepted in the schema but raise NotImplementedError at runtime. This wiring is reserved for future use.

Complete Example

Here is a complete Tags class demonstrating various tag types and trigger configurations.

from pydoover.tags import (
    Tags, Number, Boolean, String, RemoteTag,
    Cross, Rise, Fall, Delta, AnyChange, Enter, Exit,
)

class PlantTags(Tags):
    # Numeric tags with crossing-based logging
    tank_level = Number(
        default=0.0,
        log_on=[Rise(90), Fall(10), Delta(percent=5)],
    )
    flow_rate = Number(
        default=0.0,
        log_on=Delta(amount=2.0),
    )
    pressure = Number(
        default=101.3,
        log_on=Cross(50, 100, 150, deadband=1.0),
    )

    # Boolean tags with state transition logging
    pump_running = Boolean(default=False, log_on=AnyChange())
    high_alarm = Boolean(default=False, log_on=Enter(True))

    # String tag with selective state logging
    mode = String(
        default="auto",
        log_on=[Enter("manual"), Exit("manual")],
    )

    # Cross-app tag reference
    ambient_temp = RemoteTag(
        "number",
        reference_name="weather_station",
        republish_locally=True,
        optional=True,
    )

Next Steps