Metadata-Version: 2.4
Name: OWNd
Version: 2.0.0b7
Summary: Python interface for the OpenWebNet protocol
Home-page: https://github.com/OpenWebNet-HA/OWNd
Author: anotherjulien
Author-email: yetanotherjulien@gmail.com
License: LGPL-3.0-only
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Operating System :: OS Independent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp
Requires-Dist: defusedxml
Requires-Dist: pytz
Requires-Dist: python-dateutil
Provides-Extra: serial
Requires-Dist: pyserial-asyncio>=0.6; extra == "serial"
Provides-Extra: test
Requires-Dist: pytest<9; extra == "test"
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
Requires-Dist: pytest-cov>=4.0; extra == "test"
Requires-Dist: pyyaml>=6.0; extra == "test"
Requires-Dist: jsonschema>=4.0; extra == "test"
Dynamic: author
Dynamic: author-email
Dynamic: classifier
Dynamic: description
Dynamic: description-content-type
Dynamic: home-page
Dynamic: license
Dynamic: license-file
Dynamic: provides-extra
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# OWNd

[![PyPI version](https://img.shields.io/pypi/v/OWNd.svg?color=blue)](https://pypi.org/project/OWNd/)
[![Python versions](https://img.shields.io/pypi/pyversions/OWNd.svg)](https://pypi.org/project/OWNd/)
[![CI](https://github.com/OpenWebNet-HA/OWNd/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenWebNet-HA/OWNd/actions/workflows/ci.yml)
[![License: LGPL-3.0](https://img.shields.io/badge/License-LGPL--3.0-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
[![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
[![mypy: strict](https://img.shields.io/badge/mypy-strict-blue.svg)](https://mypy.readthedocs.io/en/stable/getting_started.html#strict-mode-and-configuration)
[![Typed: PEP 561](https://img.shields.io/badge/typed-PEP%20561-informational.svg)](https://peps.python.org/pep-0561/)
[![Coverage](coverage.svg)](https://app.codecov.io/github/OpenWebNet-HA/OWNd)
[![Codecov](https://codecov.io/gh/OpenWebNet-HA/OWNd/branch/master/graph/badge.svg)](https://app.codecov.io/github/OpenWebNet-HA/OWNd)

**OWNd** is an asynchronous Python library and daemon for the Legrand / BTicino **OpenWebNet** home automation protocol.

It powers the [Home Assistant MyHOME integration](https://github.com/OpenWebNet-HA/MyHOME) 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](https://peps.python.org/pep-0561/) `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](https://github.com/OpenWebNet-HA/MyHOME)'s golden corpus and test suite against every OWNd change before it ships.

---

## Key Features

- **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, 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.

---

## Installation

Install the latest stable release from PyPI:

```bash
pip install OWNd
```

To test preview releases or beta builds:

```bash
pip install --pre OWNd
```

### Optional Extras

- **Serial / USB support** (required for Legrand 3578 USB dongles):
  ```bash
  pip install "OWNd[serial]"
  ```
- **Development & test suite**:
  ```bash
  pip install "OWNd[test]"
  ```

---

## Supported Subsystems (WHO Catalog)

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` |

---

## Hardware Gateway Profiles

Gateways have varying processing limitations, socket budgets, and pacing requirements. OWNd uses declarative profiles to protect your hardware:

| Gateway Model | Concurrency | Queue Delay | Keepalive | Features |
|:---|:---:|:---:|:---:|:---|
| **MyHomeServer1** | 4 sessions (2 default) | 20 ms | Profile | HMAC-SHA2, Native transitions, Extended frames |
| **F454 / F455** | 4 sessions | 50 ms | 90 s | HMAC-SHA2, Native transitions, Extended frames |
| **MH202** | 2 sessions | 100 ms | Profile | HMAC-SHA2, Extended frames |
| **MH201** | 1 session | 100 ms | Profile | Extended frames, Clock diagnostics |
| **MH200N** | 1 session | 150 ms | 90 s | Safe pacing, Legacy password auth |
| **Generic Gateway** | 1 session | 50 ms | Profile | Conservative fallback |

Profiles can be resolved automatically using `get_gateway_profile(model_name)`:

```python
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")
```

---

## Quick Start

### 1. High-Level TCP Transport (Dual-Session)

```python
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())
```

### 2. Direct Session Management

For fine-grained control, `OWNEventSession` and `OWNCommandSession` can be operated independently:

```python
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())
```

### 3. Serial / USB Dongle (Legrand 3578)

```python
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())
```

### 4. Strongly Typed CEN / CEN+ & Central Unit Commands (Phase 2)

```python
from OWNd.message import OWNCenCommand, OWNCenPlusCommand, OWNHeatingCommand

# CEN (WHO=15): Button 2 short press on scenario controller 12 -> *15*02#2*12##
frame_cen = OWNCenCommand.short_press(where="12", button=2)

# CEN+ (WHO=25): Button 5 start pressure on keypad 01 -> *25*21#5*01##
frame_cenplus_press = OWNCenPlusCommand.start_pressure(where="01", button=5)

# CEN+ (WHO=25): Button 5 still held event -> *25*23#5*01##
frame_cenplus_held = OWNCenPlusCommand.still_held(where="01", button=5)

# CEN+ (WHO=25): Button 5 short release -> *25*24#5*01##
frame_cenplus_rel = OWNCenPlusCommand.release_from_short(where="01", 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)
```

---

## Command Line Interface (CLI)

OWNd includes a built-in CLI for discovering gateways and inspecting live bus events:

### Auto-Discovery (SSDP)

Scan the local network for OpenWebNet gateways and listen for events:

```bash
python -m OWNd
```

### Direct Connection

Connect to a known gateway IP address:

```bash
python -m OWNd --address 192.168.1.50 --port 20000 --password 12345 --verbose 2
```

Available 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)

---

## Development

Clone the repository and install development dependencies:

```bash
git clone https://github.com/OpenWebNet-HA/OWNd.git
cd OWNd
pip install -e ".[test,serial]" ruff mypy types-python-dateutil types-pytz
```

### Running Tests

Execute the test suite across all subsystems:

```bash
python -m pytest -q
```

### Static Analysis & Linting

Verify type safety and coding standards:

```bash
ruff check OWNd tests setup.py
mypy OWNd
```

---

## License

This project is licensed under the **GNU Lesser General Public License v3.0 (LGPL-3.0-only)**. See the [LICENSE](LICENSE) file for details.


## 📊 Code Coverage & Quality Assurance

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:

<!-- START_COVERAGE_TABLE -->

| Component / Module | Coverage | Notes |
|---|:---:|---|
| [`OWNd/__init__.py`](OWNd/__init__.py) | **100%** | Package initialization and version metadata |
| [`OWNd/connection.py`](OWNd/connection.py) | **100%** | Hardened dual-session TCP engine, SHA-1/HMAC auth, keepalives & bounded read loops |
| [`OWNd/discovery.py`](OWNd/discovery.py) | **100%** | SSDP multicast and UPnP XML gateway discovery and descriptor parsing |
| [`OWNd/message.py`](OWNd/message.py) | **100%** | OpenWebNet frame parsers, encoders, and WHO dimension decoders |
| [`OWNd/profiles.py`](OWNd/profiles.py) | **100%** | Declarative hardware gateway models (F454, MH200N, MH201, MH202, MyHomeServer1) |
| [`OWNd/transport/__init__.py`](OWNd/transport/__init__.py) | **100%** | Transport subpackage exports |
| [`OWNd/transport/base.py`](OWNd/transport/base.py) | **100%** | Abstract transport layer and event listener notification contracts |
| [`OWNd/transport/serial.py`](OWNd/transport/serial.py) | **100%** | Async Serial/USB transport for Legrand 3578 interface with in-band demux |
| [`OWNd/transport/tcp.py`](OWNd/transport/tcp.py) | **100%** | Dual-session TCP transport linking event and command channels |

<!-- END_COVERAGE_TABLE -->

> **Live Test Execution**: View detailed line-by-line coverage and test history on [**Codecov (OpenWebNet-HA/OWNd)**](https://app.codecov.io/github/OpenWebNet-HA/OWNd) or download the interactive coverage report from the [**CI GitHub Actions run**](https://github.com/OpenWebNet-HA/OWNd/actions/workflows/ci.yml).
