Tags
Tags are named values an application publishes to the Doover cloud. The runtime maintains two views of every tag:
an aggregate — the current value, refreshed continuously and used by dashboards and other consumers; and
a log — a stream of timestamped channel messages used for graphs and historical analysis.
By default, docker applications batch tag updates into the aggregate
once per main-loop iteration and emit one log message at most every 15
minutes. Cloud processors flush at the end of each invocation. Both
behaviours can be overridden per call via log=True (see below).
Declaring tags
Tags are declared as class attributes on a Tags subclass.
Use the typed classes — Number, Boolean,
String — for every declaration:
from pydoover.tags import Tags, Number, Boolean, String, Cross, AnyChange, Enter, Exit
class MyTags(Tags):
voltage = Number(default=0.0, log_on=Cross(90, 110, deadband=2))
fault = Boolean(default=False, log_on=AnyChange())
state = String(log_on=[Enter("error"), Exit("error")])
Inside an application the runtime exposes a BoundTag proxy:
await self.tags.voltage.set(102)
current = self.tags.voltage.get()
await self.tags.fault.delete()
Logging an update immediately
Pass log=True to BoundTag.set() (or increment()
/ decrement()) to mark the update for an immediate
logged data point — published as a channel message at the end of the
current main-loop iteration in docker apps, or at the end of the
current invocation in processors:
await self.tags.voltage.set(120, log=True)
Multiple log=True calls within the same loop coalesce into one
channel message. The same path is also reused by the trigger system
described below, so anything fired automatically follows the same flush
cadence.
Deleting a tag
Use BoundTag.delete() to remove a tag from the aggregate channel.
This is the documented way to express deletion intent — prefer it over
tag.set(None):
await self.tags.voltage.delete()
await self.tags.voltage.delete(log=True) # also record the deletion
Threshold-driven auto-logging
The typed tag classes take a single log_on= parameter that accepts
one descriptor or a list of descriptors. When any descriptor fires, the
update is automatically promoted to log=True.
Numeric tags
Number accepts Cross, Rise, Fall,
and Delta. Crossing descriptors take one or more thresholds as
positional arguments plus an optional deadband keyword:
voltage = Number(log_on=Cross(100, deadband=2)) # both directions
multi = Number(log_on=Cross(90, 100, 110)) # multiple thresholds
pressure = Number(log_on=Rise(110)) # high alarm
fuel = Number(log_on=Fall(10)) # low alarm
pump = Number(log_on=[Rise(110), Fall(10)]) # high + low
A crossing fires when the value moves from one side of a threshold to the other. The implicit prior side is “below”, so an initial value above any threshold also logs (whereas an initial value below it does not).
deadband widens each threshold into a hysteresis band: a crossing
only fires once the value moves at least deadband / 2 past the
threshold, suppressing repeat logs while the value oscillates close to
it. The “silent” opposite direction still updates the internal state
machine, so subsequent crossings the other way work correctly.
Delta logs whenever the value moves far enough from the last
value this descriptor logged on — useful for analog signals that
drift continuously rather than crossing fixed thresholds. Specify
exactly one of amount= (absolute change) or percent= (relative
change against the last logged value):
flow = Number(log_on=Delta(amount=5)) # log on ±5 unit moves
rpm = Number(log_on=Delta(percent=10)) # log on ±10% swings
The first set always fires so graphs have a baseline data point.
Combine descriptors freely — e.g. log_on=[Cross(100), Delta(amount=5)]
captures both alarm transitions and significant drift.
Boolean and string tags
Boolean and String accept AnyChange,
Enter, and Exit. Enter and Exit
each take a single value — combine them in a list to react to multiple
values or both directions:
fault = Boolean(log_on=AnyChange()) # every transition
state = String(log_on=Enter("error")) # only entering "error"
state = String(log_on=Exit("ok")) # only exiting "ok"
# Bidirectional on a single value:
state = String(log_on=[Enter("error"), Exit("error")])
# Multiple "interesting" values, both directions:
state = String(log_on=[
Enter("error"), Exit("error"),
Enter("ok"), Exit("ok"),
])
# Asymmetric: log entry to "error" but exit from "ok":
state = String(log_on=[Enter("error"), Exit("ok")])
Type validation
Each typed tag accepts only its corresponding descriptors —
Number(log_on=AnyChange()) raises TypeError at class
definition time, surfacing the mistake before the application runs.
API reference
- class pydoover.tags.Tag(tag_type: str, default: Any = <class 'pydoover.tags.NotSet'>, name: str | None = None, log_on: _LogTrigger | list[_LogTrigger] | None = None, live: bool = False)[source]
Bases:
objectRepresents a single declared tag definition.
The
log_onparameter accepts the same descriptor objects as the typed subclasses (Number,Boolean,String) so the legacyTag("number", log_on=...)form supports auto-logging too. Descriptors are validated againsttag_type: numerics acceptCross/Rise/Fall/Delta, and booleans/strings acceptAnyChange/Enter/Exit.
- class pydoover.tags.Number(*, default: Any = <class 'pydoover.tags.NotSet'>, name: str | None = None, log_on: _LogTrigger | list[_LogTrigger] | None = None, live: bool = False)[source]
Bases:
TagA numeric tag declaration with optional crossing-based auto-logging.
- Parameters:
default – Value returned by reads when the manager has nothing stored.
name – Optional explicit declaration name (otherwise inherits the attribute name on the owning
Tagssubclass).log_on – One
Cross/Rise/Fall/Deltadescriptor, or a list of them. Each describes a rule that promotes the update to an immediate log when fired.
- class pydoover.tags.Boolean(*, default: Any = <class 'pydoover.tags.NotSet'>, name: str | None = None, log_on: _LogTrigger | list[_LogTrigger] | None = None, live: bool = False)[source]
Bases:
TagA boolean tag declaration with optional state-transition auto-logging.
- class pydoover.tags.String(*, default: Any = <class 'pydoover.tags.NotSet'>, name: str | None = None, log_on: _LogTrigger | list[_LogTrigger] | None = None, live: bool = False)[source]
Bases:
TagA string tag declaration with optional state-transition auto-logging.
See
Booleanfor the meaning oflog_on.
- class pydoover.tags.RemoteTag(tag_type: str, *, reference_name: str, republish_locally: bool = True, default: Any = <class 'pydoover.tags.NotSet'>, name: str | None = None, optional: bool = False, live: bool = False)[source]
Bases:
TagA tag declaration that resolves to a tag published by another application.
The runtime target is described by a
pydoover.config.TagRefconfig element whosereference_namematches this tag’sreference_name. The binding happens at setup time viaTags._resolve_remote_tags().When
republish_locallyis true (the default), the resolved upstream value is mirrored into this app’s own tag namespace under thereference_namekey — so other consumers on the device (UIs, downstream tags) can read it as if it were a local tag.Cross-agent references (
agent_idset on the underlyingTagRef) are accepted in the schema but raiseNotImplementedErrorat runtime: the wiring is deferred so the schema does not need to change later.- Parameters:
tag_type – Asserted by the developer (matches the upstream type).
reference_name – Local handle; must match a
TagRefconfig element’sreference_name.republish_locally – Mirror upstream changes into this app’s namespace under
reference_name. Defaults toTrue. No-op on managers that do not support subscriptions (e.g. processor contexts).default – Returned when the manager has no value for the upstream tag.
name – Optional explicit declaration name (otherwise inherits the attribute name on the owning
Tagssubclass).
Trigger descriptors
- class pydoover.tags.Cross(*thresholds: float, deadband: float = 0.0)[source]
Log on crossing any of the given threshold(s) in either direction.
- Parameters:
thresholds – A single threshold, or an iterable of thresholds.
deadband – Hysteresis band — crossings only fire when the value moves at least
deadband / 2beyond the threshold, suppressing repeat logs while the value oscillates near it. Defaults to0.
- class pydoover.tags.Rise(*thresholds: float, deadband: float = 0.0)[source]
Log only on rising crossings (value moving from below to above).
Use for high-side alarms. See
Crossfor parameter details.
- class pydoover.tags.Fall(*thresholds: float, deadband: float = 0.0)[source]
Log only on falling crossings (value moving from above to below).
Use for low-side alarms. See
Crossfor parameter details.
- class pydoover.tags.Delta(*, amount: float | None = None, percent: float | None = None)[source]
Log when the value moves far enough from the last logged value.
Unlike
Cross,Deltadoesn’t track threshold sides — it compares each new value against the last value this descriptor fired on. The first set fires unconditionally (and seeds the baseline) so graphs always have an initial data point.Exactly one of
amount=orpercent=must be provided.- Parameters:
amount – Minimum absolute change required to fire — e.g.
amount=5fires on any move of at least 5 units up or down from the last logged value.percent – Minimum percentage change required to fire, computed against the magnitude of the last logged value.
percent=10fires on a 10% swing. When the last logged value is0, any non-zero new value fires.
- class pydoover.tags.Tags(app_key: str, tag_manager: TagsManager, config: Schema)[source]
Base class for declarative tag definitions.
Subclasses declare available tags as class attributes. Instances expose manager-backed
BoundTagproxies and may also mutate their available tag set at runtime viaadd_tag()andremove_tag().- add_tag(name: str, tag: Tag) → Tag[source]
Add a tag definition to this instance.
This is primarily intended for config-dependent tag factories, where the available tag set needs to be customized before user code runs.
- find_tag(name: str) → BoundTag | None[source]
Return the bound runtime proxy for a tag, if it exists.
- get_definition(name: str) → Tag | None[source]
Return the declared
Tagdefinition for a tag, if it exists.
- get_live_tag_keys() → list[tuple[str | None, str]][source]
Return
(app_key, tag_name)pairs for everylive=Truetag.The application framework calls this after
setup()and_resolve_remote_tags()to register the live-tag set with the tag manager, which republishes those values as one-shot messages each main-loop iteration. Unresolved optionalRemoteTagdeclarations are skipped.
- get_tag(name: str) → BoundTag[source]
Return the bound runtime proxy for a tag.
- Raises:
KeyError – If the tag does not exist on this collection.
- async update(values: dict[str, Any]) → None[source]
Update multiple tag values through their bound runtime proxies.
- property values: dict[str, Any]
The current manager-backed values for all declared tags.
- Type:
dict[str, Any]
- class pydoover.tags.BoundTag(tags: Tags, declaration: _DeclaredTag)[source]
Manager-backed runtime view of a declared tag.
A
BoundTagis what you interact with on aTagsinstance. Reads and writes are delegated to the registered tag manager.- async decrement(amount: int | float = 1, log: bool = False) → Any[source]
Decrement a numeric tag and return the new value.
- property default: Any
The default value returned when no runtime value exists.
- Type:
Any
- async delete(log: bool = False) → None[source]
Delete this tag from the cloud.
Removes the tag’s entry from the aggregate channel. Prefer this over
tag.set(None)to make the deletion intent explicit.
- async increment(amount: int | float = 1, log: bool = False) → Any[source]
Increment a numeric tag and return the new value.
- property live: bool
Whether the underlying tag was declared with
live=True.- Type:
bool
- property name: str
The resolved runtime name of this tag.
- Type:
str
- async set(value: Any, log: bool = False) → None[source]
Set this tag’s value via the registered manager.
- Parameters:
value – The new value.
log – When
True, the update is also recorded as a logged data point (a channel message) as soon as possible — typically at the end of the current main-loop iteration in docker apps, or at the end of the current invocation in processors — rather than waiting for the next periodic log flush (up to 15 minutes).
- property tag_type: str
The declared tag type.
- Type:
str