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")
| Parameter | Description |
|---|---|
tag_type | The type string: "number", "integer", "string", "boolean", or "object" |
default | Default value when no runtime value has been set |
name | Optional 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:
- Scans the config schema for all
TagRefelements - Matches each
RemoteTag'sreference_nameto aTagRef'sreference_name - Records the resolved target (app_key and tag_name) for runtime reads and writes
Parameters
| Parameter | Description |
|---|---|
tag_type | The expected type of the upstream tag (e.g., "number", "boolean", "string") |
reference_name | Must match a TagRef config element's reference_name |
republish_locally | When True (default), mirrors upstream changes into this app's namespace so other local consumers (UIs, downstream tags) can read it as a local tag |
default | Value returned when the upstream tag has no value |
optional | When 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,
)
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
- Tags Overview -- introduction to the tag system
- Tag Management -- tag manager implementations and cross-app access