# 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.