Tags
Tags are persistent, observable state variables scoped to an application. They provide a declarative way to define typed key-value state that survives restarts, synchronises to the cloud, and can trigger automatic data logging when values change in meaningful ways.
Under the hood, tag values are stored in a special tag_values channel aggregate managed by the Doover platform. Your application code never interacts with this channel directly. Instead, you define a Tags subclass, declare your tags as class attributes, and access them through a clean runtime API.
Why Tags?
Tags solve three problems at once:
- Persistent state: Tag values survive application restarts. When your app starts up, the tag manager loads the last known values from the cloud.
- Observable changes: Other applications on the same device can subscribe to tag changes, enabling loose coupling between apps.
- Automatic logging: Tags can be configured to log data points automatically when values cross thresholds, change state, or move by a minimum amount, without any manual logging code.
Defining Tags
Tags are defined declaratively by subclassing Tags and assigning typed tag instances as class attributes.
from pydoover.tags import Tags, Number, Boolean, String
class MyTags(Tags):
temperature = Number(default=0.0)
pump_running = Boolean(default=False)
status_message = String(default="idle")
Each class attribute becomes a tag with the attribute name as its key. The default parameter sets the value returned when no runtime value exists yet.
Connecting Tags to an Application
In a Docker application, you assign your Tags subclass to the tags_cls class attribute. The framework handles instantiation and wiring automatically.
from pydoover.docker import Application
class MyApp(Application):
tags_cls = MyTags
async def main_loop(self):
# Read a tag value
current_temp = self.tags.temperature.get()
# Update a tag value
await self.tags.temperature.set(22.5)
The framework creates an instance of your Tags subclass, connects it to the appropriate tag manager, and makes it available as self.tags.
Reading and Writing Tags
At runtime, declared Tag objects become BoundTag proxies that delegate reads and writes to the underlying tag manager. You never interact with the raw Tag definitions at runtime.
# Read the current value
value = self.tags.temperature.get()
# Alternative: use the .value property
value = self.tags.temperature.value
# Set a new value
await self.tags.temperature.set(25.0)
# Set a value and immediately log a data point
await self.tags.temperature.set(25.0, log=True)
# Check if a tag has been set
if self.tags.temperature.is_set():
print("Temperature has a value")
Declared Tag objects (the class attributes) cannot be used directly at runtime. Attempting to call .get(), .set(), or access .value on a class-level Tag raises a RuntimeError. Always access tags through a Tags instance (e.g., self.tags.temperature), which returns a BoundTag proxy.
The BoundTag API
Every tag access on a Tags instance returns a BoundTag with these methods:
| Method | Description |
|---|---|
get() | Return the current value from the tag manager |
value | Property alias for get() |
set(value, log=False) | Set a new value; optionally log immediately |
delete(log=False) | Remove the tag from the cloud aggregate |
increment(amount=1, log=False) | Add to a numeric tag and return the new value |
decrement(amount=1, log=False) | Subtract from a numeric tag and return the new value |
clear() | Reset the tag to its declared default |
is_set() | Return True if the tag has a concrete value |
BoundTag also supports comparison operators and type coercion (int(), float(), bool(), str()), so you can use tags directly in expressions:
if self.tags.temperature > 30.0:
await self.tags.pump_running.set(True)
Dynamic Tag Management
Beyond declarative definitions, you can add and remove tags at runtime through the Tags instance. This is useful when the set of tags depends on configuration.
from pydoover.tags import Number
class MyTags(Tags):
async def setup(self):
# Add tags based on config
for i in range(self.config.num_sensors.value):
self.add_tag(f"sensor_{i}", Number(default=0.0))
The setup() method runs before the tags are bound to the manager, making it the right place for dynamic tag creation.
| Method | Description |
|---|---|
add_tag(name, tag) | Add a new tag definition to this instance |
remove_tag(name) | Remove a tag definition from this instance |
find_tag(name) | Return the BoundTag for a name, or None |
get_tag(name) | Return the BoundTag for a name, or raise KeyError |
update(values_dict) | Set multiple tag values at once |
Schema Generation
Tags support schema generation through to_schema() and to_dict(). The schema describes the declared tag types and defaults, which the platform uses for validation and UI generation.
tags = MyTags(app_key="my_app", tag_manager=manager, config=config)
schema = tags.to_schema()
# {"temperature": {"type": "number", "default": 0.0}, ...}
Next Steps
- Tag Types and Auto-Logging -- typed tags with automatic data logging triggers
- Tag Management -- tag manager implementations, buffering, and cross-app access