Implement a Sensor Plugin¶
This guide shows how to implement a sensor plugin using SensorPlugin as the base class.
When to Use SensorPlugin¶
Use SensorPlugin when your plugin:
- Reads data from a physical or simulated sensor
- Publishes numeric, boolean, or string metrics at regular intervals
- May need to reconnect after an unexpected disconnect
Use PluginBase directly when your plugin handles commands but does not produce
continuous metric data — see Implement a Custom Plugin.
1. Define the Plugin Class¶
Declare your output metrics in Meta.out_metrics, implement connect (setting
self.id) and disconnect:
import asyncio
import time
from tolomeo.metrics import Metric, MetricType, MetricDataType
from tolomeo.plugins import SensorPlugin
class TemperaturePlugin(SensorPlugin):
class Meta:
out_metrics = [
Metric(
name="temperature",
unit="°C",
type=MetricType.TEMPORAL,
data_type=MetricDataType.NUMBER,
)
]
async def connect(self) -> bool:
self.id = "temperature_sensor" # must be set here
# open serial port, BLE connection, etc.
return True
async def disconnect(self) -> bool:
# close connection
return True
2. Push Data¶
SensorPlugin is push-only. Push each reading directly with
await self.push_reading(...) from a background task started in after_setup.
The dict must contain a timestamp key plus values keyed by the namespaced
metric name (f"{self.id}:{metric.name}"):
async def after_setup(self) -> None:
await self.task_manager.add_task("read_sensor", self._read_loop)
async def _read_loop(self) -> None:
while True:
temperature = await self._hw.read_celsius()
await self.push_reading({
"timestamp": round(time.time()),
f"{self.id}:temperature": temperature,
})
await asyncio.sleep(1)
3. Handle Reconnects (Optional)¶
Register a callback on on_disconnect to be notified of unexpected disconnects:
async def after_setup(self) -> None:
self.on_disconnect = self._handle_disconnect
await self.task_manager.add_task("read_sensor", self._read_loop)
async def _handle_disconnect(self) -> None:
from tolomeo.plugins.sensor import SensorState
self._logger.warning("Sensor disconnected — attempting reconnect")
self.state = SensorState.DISCONNECTED
# implement reconnection logic using self.conn_context.reconnect_policy
4. Wire into a Service¶
Use SingleSensorService for the common case of a single plugin, or subclass
SensorService directly for full control:
from tolomeo.services import SensorService
from .plugin import TemperaturePlugin
class TemperatureService(SensorService):
class Meta:
plugin_class = TemperaturePlugin
async def setup(self) -> None:
await super().setup()
plugin = self._plugin_class(logger=self._logger)
async with self.params_sync.condition:
await plugin.attach() # attach inside the condition lock — matches SingleSensorService
self.plugins[plugin.id] = plugin
self.params_sync.is_ready = True
self.params_sync.condition.notify_all()
plugin.attach() placement
Calling attach() inside the condition block (as shown above) ensures no
consumer can observe is_ready = True before the plugin is fully set up.
If connect or setup hooks are long-running and do not need the lock,
you may call attach() before acquiring it — see the
Build Your First Service tutorial for that
alternative pattern.
Published NATS Output¶
Metrics are published to events.data as SenML records:
[{"bn": "urn:cpt:device:sn:abc123:", "n": "temperature_sensor:temperature", "u": "°C", "v": 23.5, "t": 1700000010.0}]