Files
app/docs/code/connectors.md
T
2026-10-13 10:02:20 +02:00

131 lines
3.9 KiB
Markdown

# Writing a connector
A connector adds a device or service integration to the node palette. It is
a Python package with a `ConnectorNode` subclass and an entry point.
This example works without hardware or another repository.
## Create the package
Make this directory beside your project:
```text
fluksio-connector-random/
pyproject.toml
src/
fluksio_connector_random/
__init__.py
```
Put this in `pyproject.toml`:
```toml
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "fluksio-connector-random"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["fluksio>=0.1,<0.2"]
[project.entry-points."fluksio.node_types"]
random_sensor = "fluksio_connector_random:RandomSensor"
[tool.hatch.build.targets.wheel]
packages = ["src/fluksio_connector_random"]
```
Put this in `src/fluksio_connector_random/__init__.py`:
```python
import random
from pydantic import Field
from fluksio.flow.connector import CONTRACT_VERSION, ConnectorNode
class RandomSensor(ConnectorNode):
contract = CONTRACT_VERSION
title = "Random sensor"
description = "Emits a random reading. No sensor required."
class Params(ConnectorNode.Params):
ceiling: float = Field(default=1.0, gt=0)
async def poll(self):
return {"reading": random.random() * self.config.ceiling}
```
The entry point name, `random_sensor`, becomes the stored node type.
Choose a unique name; the engine skips names that are already registered.
## Install and try it
Use the **engine's Python environment**, the one where you installed Fluksio:
```sh
pip install -e /path/to/fluksio-connector-random
```
Restart the engine from its original project directory so it keeps using
the same instance data:
```sh
fluksio serve
```
Restart an already running engine after installing. In the browser:
1. Add a **Random sensor** node to a live flow.
2. Set `poll_interval` to `5` seconds.
3. Add an output port named `reading`, type `float`.
4. Publish the flow and watch the value change.
The palette shows the package name and version. Code edits need an engine
restart; metadata changes also need another editable install.
For Docker, install the package in the backend image and rebuild it.
The Modules screen installs packages for **node code**, so it does not
install connectors into the engine.
## Connect a real device
Replace `poll()` with your device read. Return a dictionary keyed by declared
output port, or `None` when there is nothing new. The base class polls at
`poll_interval`, publishes changed values, and reports failures.
Keep blocking I/O off the event loop with `asyncio.to_thread`. For pushed
updates, manage the connection in `start()` and `stop()`, leave
`poll_interval=0`, and call `self.inject({...})` when data arrives.
Mark credential parameters with
`Field(default=None, json_schema_extra={"x-secret": True})`. The editor
then stores a secret reference rather than the credential in the flow.
To send commands, implement `write(self, **ports)`: it receives values keyed
by input port. Set `idempotent = False` for commands that must not be replayed
after a crash.
## Media and persistent state
Use `self.save_artifact(data, name, media_type=...)` for bytes such as camera
frames. Return the reference through an `image`, `audio`, or `video` port.
For disposable live frames, pass `volatile=True`.
Use `self.store` for short strings that must survive a restart, such as a
pairing token. Include the device address in the key: all nodes from the
same top-level package share this store.
## Before sharing
- Declare `contract = CONTRACT_VERSION` on the class itself.
- Return only declared output ports, with values matching their types.
- Mark credentials as secrets.
- Keep `stop()` safe after a failed start and when called twice.
- Report connection health if you manage your own subscription.
See [the connector contract](../reference/connector-contract.md) for lifecycle,
storage limits, and certificate pinning.