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: object

Represents a single declared tag definition.

The log_on parameter accepts the same descriptor objects as the typed subclasses (Number, Boolean, String) so the legacy Tag("number", log_on=...) form supports auto-logging too. Descriptors are validated against tag_type: numerics accept Cross / Rise / Fall / Delta, and booleans/strings accept AnyChange / Enter / Exit.

to_dict() dict[str, Any][source]

Convert this tag definition to its schema-style dictionary form.

to_schema() dict[str, Any][source]

Alias for to_dict() to match the rest of the declarative API.

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: Tag

A 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 Tags subclass).

  • log_on – One Cross / Rise / Fall / Delta descriptor, 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: Tag

A boolean tag declaration with optional state-transition auto-logging.

Parameters:
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: Tag

A string tag declaration with optional state-transition auto-logging.

See Boolean for the meaning of log_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: Tag

A tag declaration that resolves to a tag published by another application.

The runtime target is described by a pydoover.config.TagRef config element whose reference_name matches this tag’s reference_name. The binding happens at setup time via Tags._resolve_remote_tags().

When republish_locally is true (the default), the resolved upstream value is mirrored into this app’s own tag namespace under the reference_name key — so other consumers on the device (UIs, downstream tags) can read it as if it were a local tag.

Cross-agent references (agent_id set on the underlying TagRef) are accepted in the schema but raise NotImplementedError at 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 TagRef config element’s reference_name.

  • republish_locally – Mirror upstream changes into this app’s namespace under reference_name. Defaults to True. 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 Tags subclass).

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 / 2 beyond the threshold, suppressing repeat logs while the value oscillates near it. Defaults to 0.

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 Cross for 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 Cross for 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, Delta doesn’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= or percent= must be provided.

Parameters:
  • amount – Minimum absolute change required to fire — e.g. amount=5 fires 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=10 fires on a 10% swing. When the last logged value is 0, any non-zero new value fires.

class pydoover.tags.AnyChange[source]

Log on every value transition (for Boolean / String).

class pydoover.tags.Enter(value: Any)[source]

Log only on entering the given value.

class pydoover.tags.Exit(value: Any)[source]

Log only on exiting the given value.

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 BoundTag proxies and may also mutate their available tag set at runtime via add_tag() and remove_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.

property definitions: list[Tag]

The declared tag definitions for this instance.

Type:

list[Tag]

find_tag(name: str) BoundTag | None[source]

Return the bound runtime proxy for a tag, if it exists.

get(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 Tag definition for a tag, if it exists.

get_live_tag_keys() list[tuple[str | None, str]][source]

Return (app_key, tag_name) pairs for every live=True tag.

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 optional RemoteTag declarations 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.

remove_tag(name: str) None[source]

Remove a tag definition from this instance.

async setup()[source]

Mutate this tag collection before it is bound to a manager.

to_dict() dict[str, Any][source]

Return the current manager-backed tag values.

to_schema() dict[str, Any][source]

Return the schema-style definitions for the current tag set.

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 BoundTag is what you interact with on a Tags instance. Reads and writes are delegated to the registered tag manager.

async clear() None[source]

Reset this tag back to its declared default value.

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.

get() Any[source]

Return the current value of this tag from the registered manager.

async increment(amount: int | float = 1, log: bool = False) Any[source]

Increment a numeric tag and return the new value.

is_set() bool[source]

Return True when this tag currently has a concrete 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

property value: Any

Convenience alias for get().

Type:

Any