OWNd is an asynchronous Python library and daemon for the Legrand / BTicino OpenWebNet home automation protocol.
It powers the Home Assistant MyHOME integration and serves as a standalone Python client for discovering, monitoring, and controlling OpenWebNet bus devices over TCP/IP gateways and serial USB interfaces.
Tip
🚀 V2 Phase 2 Architecture Now Live: Phase 2 architecture is active across OWNd and MyHOME! Featuring strongly typed CEN / CEN+ scenario command builders and device triggers (P2), Thermoregulation Central Unit (3550 / 4695) master mode and zone coordination (P4), Multi-Gateway routing and plant isolation (P6), DALI Tunable White support, and 100.0% test coverage verified against the OpenWebNet Golden Corpus.
Note
🔒 Full mypy --strict compliance (2.0.0-b7): every module now passes strict static typing with zero suppressions, and the package ships a PEP 561 py.typed marker so type checkers pick up OWNd's types in consuming projects (this is what unblocks the Home Assistant Platinum strict-typing quality-scale rule for MyHOME). Also in this release: a public OWNSession.is_open property, one canonical line ending across the repository, and a CI gate that replays MyHOME's golden corpus and test suite against every OWNd change before it ships.
- Hardened Dual-Session Architecture: Decouples real-time bus event monitoring (
OWNEventSession) from command and query execution (OWNCommandSession), preventing command bursts from interrupting event monitoring. - Strongly Typed CEN / CEN+ Command Builders (P2): Dedicated fluent builders (
OWNCenCommand,OWNCenPlusCommand) with strict OpenWebNet golden corpus frame parity for short press, start pressure, still held, and release actions across pushbuttons and rotary encoders. - Thermoregulation Central Unit Coordination (P4): Dedicated builder support for 3550 (
#0) and 4695 (#0#1) central units (OWNHeatingCommand.set_central_mode,set_central_temperature,set_central_antifreeze,set_central_thermal_protection,set_central_off), enabling master heating/cooling state distribution. - DALI Tunable White & Color Temperature: Built-in support for DALI DT8 ballasts (F429 / F461) with Dimension 14 color temperature encoding and bidirectional Kelvin/mireds conversion.
- OpenWebNet Golden Corpus Validation: Cross-checked and validated against the community OpenWebNet Golden Corpus (75+ real-world captured frame scenarios) ensuring exact frame encodings, dimensions, and edge cases.
- Serial & USB Dongle Support: Built-in single-channel serial transport (
AsyncSerialTransport) for the Legrand 3578 USB/ZigBee interface with in-band event and command-reply demultiplexing. - Connection Resilience:
- Fail-closed SHA-1 and HMAC-SHA2 gateway authentication with constant-time signature verification.
- OS-level TCP keepalive (
SO_KEEPALIVE) with aggressive probing (30s idle / 10s interval / 3 count) to detect silent network drops (power loss, cable unplugged) in ~60s. - Periodic application-level keepalives and passive watchdogs.
- Non-blocking bounded timeouts on handshakes and commands to prevent event loop stalls.
- Multi-frame response collection for large bus status sweeps (up to 256 frames).
- Declarative Hardware Profiles: Tailored queue pacing, session concurrency, and subsystem limits for known Legrand/BTicino hardware (F454, F455, MH200, MH200N, MH201, MH202, MyHomeServer1, and conservative generic fallbacks).
- Modern Python & Strict 100% Test Coverage: Designed for Python 3.11+, tested continuously against Python 3.11, 3.12, 3.13, and 3.14 with strict 100.0% line coverage unconditionally enforced across all core modules.
Install the latest stable release from PyPI:
pip install OWNdTo test preview releases or beta builds:
pip install --pre OWNd- Serial / USB support (required for Legrand 3578 USB dongles):
pip install "OWNd[serial]" - Development & test suite:
pip install "OWNd[test]"
OWNd parses OpenWebNet frames and dispatches typed commands and events across the full MyHOME spectrum:
| WHO | Subsystem | Description & Capabilities | Event / Command Classes |
|---|---|---|---|
| 1 | Lighting | On/off switching, dimming level (0–100%), DALI Tunable White (Dimension 14, 2000K–6535K / mireds), status queries | OWNLightingCommand, OWNLightingEvent |
| 2 | Automation | Shutters, blinds, motorized curtains, tilt angles, short & full replies | OWNAutomationCommand, OWNAutomationEvent |
| 3 | Load Control | Load shedding status, circuit priority management | OWNCommand, OWNEvent |
| 4 | Thermoregulation / Climate | Multi-zone temperature readouts, target adjustments, HVAC modes (Heat/Cool/Auto/Off), local offsets, fan coil speeds, valve states, Central Unit 3550/4695 master coordination | OWNHeatingCommand, OWNHeatingEvent |
| 5 | Burglar Alarm | Zone status, system arming / disarming states | OWNAlarmCommand, OWNAlarmEvent |
| 13 | Gateway Diagnostics & Clock | Gateway date/time synchronization, timezone offsets, firmware metadata | OWNGatewayCommand, OWNGatewayEvent |
| 15 | CEN Scenarios | Scenario control, pushbutton push/release/extended press events, strongly typed command builders | OWNCenCommand, OWNCENEvent, OWNScenarioEvent |
| 16 / 22 | Sound Diffusion | Multi-source selection, zone activation, volume adjustment, F441 matrix | OWNSoundCommand, OWNSoundEvent, OWNAVCommand |
| 17 | Scenario Programmer | MH200N / MH202 scenario activation and state monitoring | OWNSceneEvent |
| 18 | Energy Management | Active power (W), hourly/daily/monthly consumption (kWh), Stop & Go breaker diagnostics | OWNEnergyCommand, OWNEnergyEvent |
| 25 | CEN+ & Dry Contacts | 32-button keypads, rotary knob encoders (CW/CCW), dry contacts, PIR sensors, strongly typed command builders | OWNCenPlusCommand, OWNCENPlusEvent, OWNDryContactCommand, OWNDryContactEvent |
Gateways have varying processing limitations, socket budgets, and pacing requirements. OWNd uses declarative profiles to protect your hardware:
| Gateway Model | Concurrency | Queue Delay | Event keepalive | Features |
|---|---|---|---|---|
| MyHomeServer1 | 4 sessions (2 default) | 20 ms | OS TCP only | HMAC-SHA2, Native transitions, Extended frames, Sound system (WHO 16) |
| F454 | 4 sessions | 50 ms | 90 s | HMAC-SHA2, Native transitions, Extended frames, Sound system (WHO 16) |
| F455 | 4 sessions | 50 ms | OS TCP only | HMAC-SHA2, Native transitions, Extended frames, Sound system (WHO 16) |
| F461 | 4 sessions | 50 ms | 90 s | HMAC-SHA2, Native transitions, Extended frames, Sound system (WHO 16) |
| MH202 | 2 sessions | 100 ms | OS TCP only | HMAC-SHA2, Extended frames, Sound system (WHO 16) |
| MH201 | 1 session | 100 ms | OS TCP only | Legacy password auth, Extended frames, Sound system (WHO 16), Clock diagnostics |
| MH200 | 1 session | 150 ms | 90 s | Safe pacing, Legacy password auth, Sound system (WHO 16) |
| MH200N | 1 session | 150 ms | 90 s | Safe pacing, Legacy password auth, Sound system (WHO 16) |
| Generic Gateway | 1 session | 50 ms | OS TCP only | Conservative fallback |
Profiles can be resolved automatically using get_gateway_profile(model_name):
from OWNd.profiles import get_gateway_profile
profile = get_gateway_profile("F454")
print(f"Max concurrent sessions: {profile.max_command_sessions}")
print(f"Command queue delay: {profile.command_queue_delay}s")import asyncio
from OWNd.connection import OWNGateway
from OWNd.transport.tcp import AsyncTcpTransport
from OWNd.message import OWNMessage
async def main():
# Configure gateway credentials
gateway = OWNGateway({
"address": "192.168.1.50",
"port": 20000,
"password": "12345",
})
transport = AsyncTcpTransport(gateway)
# Register an event listener for bus notifications
def on_event(msg: OWNMessage | str):
if isinstance(msg, OWNMessage) and msg.is_event:
print(f"Bus Event: {msg.human_readable_log}")
transport.register_listener(on_event)
# Connect both event and command channels
if await transport.connect():
print("Connected to OpenWebNet gateway!")
# Send a command: Turn ON light at address 12 (*1*1*12##)
response = await transport.send("*1*1*12##")
print(f"Command response: {response}")
# Keep listening for events
await asyncio.sleep(10)
await transport.disconnect()
if __name__ == "__main__":
asyncio.run(main())For fine-grained control, OWNEventSession and OWNCommandSession can be operated independently:
import asyncio
from OWNd.connection import OWNGateway, OWNEventSession, OWNCommandSession
async def main():
gateway = OWNGateway({"address": "192.168.1.50", "port": 20000, "password": "12345"})
# Event listening session
event_session = OWNEventSession(gateway=gateway)
await event_session.connect()
# Command session
command_session = OWNCommandSession(gateway=gateway)
await command_session.connect()
# Query status of zone 1 climate: *#4*1*0##
status = await command_session.send("*#4*1*0##", is_status_request=True)
print(f"Status response: {status}")
await event_session.close()
await command_session.close()
asyncio.run(main())import asyncio
from OWNd.transport.serial import AsyncSerialTransport
async def main():
transport = AsyncSerialTransport(port="/dev/ttyUSB0")
transport.register_listener(lambda msg: print(f"Serial Inbound: {msg}"))
await transport.connect()
# Send OpenWebNet frame over serial
await transport.send("*1*1*12##")
await asyncio.sleep(5)
await transport.disconnect()
asyncio.run(main())from OWNd.message import OWNCenCommand, OWNCenPlusCommand, OWNHeatingCommand
# CEN (WHO=15): Button 2 short press on scenario controller 12
frame_cen_press = OWNCenCommand.press(where="12", button=2) # *15*02*12##
frame_cen_release = OWNCenCommand.release_short_press(where="12", button=2) # *15*02#1*12##
# CEN+ (WHO=25): Button 5 short press on virtual object 21 -> *25*21#5*21##
frame_cenplus_press = OWNCenPlusCommand.press(where="21", button=5)
# CEN+ (WHO=25): Button 5 still held event -> *25*23#5*21##
frame_cenplus_held = OWNCenPlusCommand.still_held(where="21", button=5)
# CEN+ (WHO=25): Button 5 release after a long press -> *25*24#5*21##
frame_cenplus_rel = OWNCenPlusCommand.release(where="21", button=5)
# Central Unit (WHO=4): Set 3550 (#0) master mode to Heating at 21.5°C -> *4*1#0215*#0##
frame_heat = OWNHeatingCommand.set_central_mode(where="#0", mode="heating", temperature=21.5)
# Central Unit (WHO=4): Set 4695 (#0#1) master mode to Cooling at 24.0°C -> *4*2#0240*#0#1##
frame_cool = OWNHeatingCommand.set_central_mode(where="#0#1", mode="cooling", temperature=24.0)
# Central Unit (WHO=4): Turn Central Unit OFF -> *4*303#0215*#0##
frame_off = OWNHeatingCommand.set_central_off(where="#0", mode="heating", temperature=21.5)OWNd includes a built-in CLI for discovering gateways and inspecting live bus events:
Scan the local network for OpenWebNet gateways and listen for events:
python -m OWNdConnect to a known gateway IP address:
python -m OWNd --address 192.168.1.50 --port 20000 --password 12345 --verbose 2Available options:
-a,--address: IP address of the gateway-p,--port: Gateway TCP port (default:20000)-P,--password: Numeric OPEN password or HMAC secret (default:12345)-m,--mac: MAC address (used as unique identifier when skipping SSDP)-v,--verbose: Verbosity level (0= WARNING,1= INFO,2= DEBUG)
Clone the repository and install development dependencies:
git clone https://github.com/OpenWebNet-HA/OWNd.git
cd OWNd
pip install -e ".[test,serial]" ruff mypy types-python-dateutil types-pytzExecute the test suite across all subsystems:
python -m pytest -qVerify type safety and coding standards:
ruff check OWNd tests setup.py
mypy OWNdThis project is licensed under the Apache License 2.0, the same license as Home Assistant Core. See the LICENSE file for details.
OWNd maintains an automated test suite with strict 100.0% line coverage (3,087 / 3,087 statements covered with 0 missing lines across all 9 core modules) verified continuously in CI across Python 3.11, 3.12, 3.13, and 3.14:
| Component / Module | Coverage | Notes |
|---|---|---|
OWNd/__init__.py |
100% | Package initialization and version metadata |
OWNd/connection.py |
100% | Hardened dual-session TCP engine, SHA-1/HMAC auth, keepalives & bounded read loops |
OWNd/discovery.py |
100% | SSDP multicast and UPnP XML gateway discovery and descriptor parsing |
OWNd/message.py |
100% | OpenWebNet frame parsers, encoders, and WHO dimension decoders |
OWNd/profiles.py |
100% | Declarative hardware gateway models (F454, MH200, MH200N, MH201, MH202, MyHomeServer1) |
OWNd/transport/__init__.py |
100% | Transport subpackage exports |
OWNd/transport/base.py |
100% | Abstract transport layer and event listener notification contracts |
OWNd/transport/serial.py |
100% | Async Serial/USB transport for Legrand 3578 interface with in-band demux |
OWNd/transport/tcp.py |
100% | Dual-session TCP transport linking event and command channels |
Live Test Execution: View detailed line-by-line coverage and test history on Codecov (OpenWebNet-HA/OWNd) or download the interactive coverage report from the CI GitHub Actions run.