Skip to content
raspihatsPublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ConduIOT

A field-I/O gateway daemon. It owns the hardware buses on a Linux host, maps physical I/O points to Home Assistant entities over MQTT, and keeps a minimal set of control rules running locally so the installation still works when the network, the broker, or Home Assistant is unavailable.

Status: alpha, and it drives real hardware. conduiot run commissions raspihats I²C-HATs, publishes a device tree to a real broker, reports interrupt-driven inputs (~23 ms measured), runs local rules, and fails safe. Verified on a bench: with the daemon SIGKILLed and the bus silent, a commissioned board took its masked channel to the safe value on its own and held the channel configured to hold.

Only the raspihats/* and sim drivers exist. The CANopen, GPIO and I²C-expander drivers are specified in SPEC.md and not written. Missing CLI commands: status, monitor, set, canopen lss.

Nothing is published yet — no PyPI release, no container image in a registry, no add-on repository. The packaging builds and has been installed, run, upgraded and purged on a Raspberry Pi, but the first tagged release is what puts it anywhere you can download it from.

Why

Home Assistant no longer accepts integrations that talk to devices over GPIO, I²C or SPI (ADR-0019), and running a bus from inside a container was never comfortable anyway. MQTT is the interface that will not be deprecated out from under you: it works on every install type, and it works when Home Assistant is on a different machine from the I/O.

So ConduIOT is a network peer, not a co-process. Home Assistant on one host, one or more gateways on others, a broker between them.

What makes it different from a generic MQTT bridge

Safe state is a first-class concept, and it is honest about who enforces it. Every feature is declared as board, gateway or none:

fault_response link_loss
Enforced by the device, autonomously the gateway
Survives daemon killed, host powered off, cable pulled broker down, HA down, network partition
Available on boards with a hardware watchdog every driver

Those are two differently-named config blocks, not one with a fallback. A guarantee that evaporates with the process that promised it should not share a name with one that does not.

Local rules keep running without a broker. A light switch keeps working when the network is down. Five rule types, deliberately: direct, toggle, pulse, interlock, inhibit. No scripting language — this is not a soft PLC, and the vocabulary is fixed so it cannot quietly become one.

interlock and inhibit are constraints, not writers: they apply to every write regardless of origin, so a command from Home Assistant that would violate one is clamped and Home Assistant is told what the hardware actually did. Asking for one direction of a gate while the other is energised releases the first, waits out the dead time, then applies the second — a contactor that has not finished opening is a short circuit waiting for the other to close.

Capability comes from the board, never from a version table. A driver asks the hardware what it can do and reports the answer; nothing above the driver layer knows which firmware release gained which register. A board that predates a feature says so by refusing the request, and that refusal is the answer.

A missing board costs only itself. One unplugged board does not stop the gateway: it is retried with backoff, its entities stay in Home Assistant greyed out rather than disappearing, and when it answers again its configuration is re-applied before it is trusted — because a board that dropped off may have power-cycled into factory defaults, with no watchdog and no safety mask.

Supported hardware

Implemented:

Driver Hardware Fault response
raspihats/* raspihats I²C-HATs — digital in/out, relays board — firmware watchdog, CiA 401 0x6206/0x6207
sim nothing; a board-shaped fake for trying the whole path without hardware configurable, for testing both branches

Verified against DI16ac, DQ5rly, DQ10rly and DI6acDQ6rly on one bus, spanning firmware v1.1.3 to v3.1.1 — every one identified from what it answered rather than from its version string.

Specified but not written — SPEC.md describes these and the driver interface is designed for them, but no code exists yet:

Driver Hardware
canopen401 any CiA-401 generic I/O device over CANopen
gpio SoC GPIO via libgpiod v2
mcp23017, pcf8574 I²C expanders
ads1115, pca9685 I²C ADC / PWM

Install

Nothing is published yet, so all four paths build from a checkout. Each has been installed and run on a Raspberry Pi.

From source

pip install -e '.[all]'
conduiot validate --config examples/panel-1.yaml

Debian package — the reference install (SPEC.md §11.0). Carries its own wheels and builds a virtualenv offline, so no network is needed at install time and nothing is written to the system Python. One package covers Debian 12/13 and Ubuntu 22.04/24.04.

packaging/build-deb.sh --arch arm64
sudo apt install ./dist/conduiot_*_arm64.deb

It installs the service and does not start it. This daemon drives relays, and which ones close is a decision for whoever is near the panel — postinst prints the four steps instead.

Container — the substrate for compose and for the add-on.

packaging/build-image.sh --platform linux/arm64 --tag conduiot:local

See examples/docker-compose.yml. Use docker stop -t 90 rather than --timeout, which only exists from Docker 28.

Home Assistant add-on — addon/ is an add-on repository wrapping that image. Its options carry only broker credentials and log level; the installation itself stays in the same config.yaml every other target uses.

Try it without hardware

The sim driver is a board with nothing behind it, so the whole path - config, entities, discovery, commands - runs on a laptop with a broker:

buses:
  - {id: sim0, type: sim}
boards:
  - id: relays
    driver: sim
    bus: sim0
    sim:
      sections: {dq: 6, di: 6}
      enforcement: {fault_response: board}   # or gateway, or nothing at all
    config:
      fault_response: {timeout: 10s, default: false}
entities:
  - {id: light, type: switch, point: relays.dq.0, name: Light}
conduiot run --config sim.yaml
mosquitto_pub -t conduiot/<gateway>/light/set -m ON

Entities appear in Home Assistant, and the switch works.

Configuration

One YAML file per gateway, identical across every deployment target — only its path differs. See examples/panel-1.yaml for a worked installation and SPEC.md §3 for the whole schema.

gateway:
  id: panel-1

mqtt:
  host: 192.168.1.10
  password: !env MQTT_PASSWORD

buses:
  - {id: i2c1, type: i2c, device: /dev/i2c-1}

boards:
  - id: relays
    driver: raspihats/DQ10rly
    bus: i2c1
    address: 0x50
    config:
      fault_response:
        timeout: 10s
        default: false
        overrides: {9: hold}   # channel 9 holds its last state

entities:
  - {id: garage_light, type: switch, point: relays.dq.0, name: Garage light}

Validation is strict: unknown keys are errors, not warnings. A misspelled fault_respose that validates silently is a safe state you believe you configured and do not have.

Commands

conduiot doctor      # diagnose the environment; changes nothing, needs no privilege
conduiot scan        # find boards and write a starter config
conduiot validate    # parse and check the configuration; opens no bus
conduiot apply       # commission the boards; --check prints the diff and writes nothing
conduiot run         # the gateway itself

doctor is print-only by design: half of what it checks lives in /boot/firmware/config.txt, and a diagnostic that half-applies a boot change bricks a headless Pi one reboot later. It prints the command; you run it.

scan reports presence by default and transmits nothing. --identify sends each driver family's request frame, and 0x50–0x57 is also where 24Cxx EEPROMs live — so it is opt-in.

Operational behaviour

Output restore. gateway.restore: last puts outputs back after a restart, from a state file that is never load-bearing: missing, corrupt or unwritable all degrade to power-on values with a warning, never to a failure. Restored values go through the ordinary write path, so a stale state file that would close both contactors of a reversing starter meets the same interlock a live command does.

Gateway diagnostics. A retained JSON document carries uptime, MQTT reconnects, config hash, boards online and per-bus error counters, surfaced as Home Assistant diagnostic entities. Retries and errors are counted separately — a marginal I²C cable does not fail, it retries, and folding the two together destroys the only early warning there is.

Development

python -m pytest -q      # 496 tests

Requires Python 3.11+. CI runs the suite on 3.11, 3.12 and 3.13 — the same range the .deb carries wheels for, so the two cannot drift.

License

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages