Skip to main content

Async WOLF CWL-2-325 controller

wolf-325 is a Python 3.11+ async Modbus controller for a WOLF CWL-2-325 ventilation appliance connected through a Waveshare RS485-to-Ethernet gateway. It is the modular package port of the reference implementation in .guides/wolf_cwl2_async.

The package provides:

  • all 154 documented UWA2-B/UWA2-E input and holding values;
  • fast, slow, and static polling with an in-memory cache;
  • synchronous/async callbacks and an async update iterator;
  • validated named setters, persistent desired state, and read-back verification;
  • startup/reconnect restoration and periodic reconciliation;
  • composable partial JSON profiles with inheritance, replacement, and unset;
  • guarded filter/appliance reset commands;
  • atomic JSON config/state persistence;
  • a CLI for configuration, reads, writes, profiles, and daemon operation; and
  • a full-screen TUI for live monitoring and guarded interactive control.

Install

This repository uses uv:

uv sync --group dev
uv run wolf-cwl2 --help

The base runtime installs PyModbus only. Textual is isolated in the tui optional extra; the development group installs it for the terminal interface and its tests. For a package installation outside this repository, select wolf-325[tui] when the TUI is required.

The console command and module entry point are equivalent:

uv run wolf-cwl2 registers --writable-only
uv run python -m wolf_325 registers --writable-only

Home Assistant integration

The repository contains a native wolf_cwl2 custom integration for Home Assistant. It creates one config entry and device per verified appliance serial, supports multiple appliances from the first release, polls automatically for Recorder history, exposes the complete reviewed datapoint/control surface, and stores profiles independently in Home Assistant.

Release status: the integration is fully exercised in the local Home Assistant 2026.2.3 test host, but it is not yet an end-user HACS/manual release. The lightweight wolf-325 client has not been published, and approved public package-index ownership and publication authority are not yet available. The project is licensed under MIT with Julian Kimmig as author, copyright holder, and GitHub code owner. Public repository, documentation, and issue links point to JulianKimmig/wolf-325. The component manifest intentionally retains an empty client requirement until the exact package is published and pinned, so copying only custom_components/wolf_cwl2 into Home Assistant does not yet constitute a supported release.

For repository development:

uv sync --group dev
UV_CACHE_DIR=.cache/uv uv run pytest \
  -c tests/components/wolf_cwl2/pytest.toml \
  -p no:sugar tests/components/wolf_cwl2

Once the release blockers are resolved, installation will use the standard HACS custom-repository flow or a manual copy of the single custom_components/wolf_cwl2 directory, followed by a Home Assistant restart.

Add appliances and choose authority

In Home Assistant, open Settings → Devices & services → Add integration and select WOLF CWL-2. Enter the gateway host/port, Modbus unit ID, transport (modbus_tcp or rtu_over_tcp), and address offset (0 normally, -1 only for a gateway that maps one-based addresses). Setup performs a read-only identity probe. The verified 12-digit serial is the stable identity, so moving a gateway does not replace Recorder history. Add each additional appliance as a separate entry; duplicate serials are rejected.

Entry options provide three authority modes:

  • monitor_only: polling and read entities only; every control/action rejects before Store or Modbus mutation.
  • temporary: safe controls and profile application change the live appliance but create no durable desired ownership.
  • persistent: desired values are stored before I/O and reconciled after drift or reconnect. Failed writes remain queued.

Leaving persistent mode keeps desired values dormant. Returning does not silently reassert them: use Resume desired ownership to authorize a forced apply, or Clear desired ownership to release them without changing the current appliance values.

Fast, slow, static, and reconciliation intervals are configurable integer seconds with a five-second floor. Defaults are 5, 60, 300, and 30 seconds. Holding-register and optional-extension polling can be disabled independently.

Entities, Recorder, and controls

The integration classifies all 154 catalogue keys: 83 sensors, 39 numbers, 20 selects, 10 switches, and two guarded action-only reset values. Thirty-six ordinary entities are enabled by default; advanced and diagnostic entities stay available in the entity registry but begin disabled. The normal curated read surface contains 23 sensors.

Sensor state is confirmed cached device state. Polling, disconnect, and stale tier handling drive entity availability; volatile raw words, errors, desired values, and profile lineage are not entity attributes. Instantaneous proven telemetry uses Recorder measurement metadata where legal. Counters do not claim total/total-increasing statistics until their reset/monotonic behavior is physically proven.

Safe numbers, selects, and switches remain visible in every authority mode so dashboards and automations keep stable entity IDs. Monitor-only calls reject; temporary calls write without ownership; persistent calls store desired state before verified device I/O. Appliance clock and Modbus interface/address/baud settings remain read-only. There is no raw-register service.

Home Assistant profiles

The synthetic Profile select lists the profiles in that entry's private Home Assistant Store. Selecting a profile applies it under the current authority contract. Its state is the last profile Home Assistant fully applied, not a claim that live settings still match. Profile writes are sequential and not device-atomic: partial success is not rolled back, and persistent desired values stay queued for reconciliation.

Use the response-capable wolf_cwl2.preview_profile_capture action to inspect the exact persistent desired-state delta, parent lineage, unset list, and Store revision. Use wolf_cwl2.save_profile with a suffix-free name, optional description, explicit overwrite, and optionally the previewed expected_revision. Capture is persistent-only, performs no Modbus I/O, and never includes temporary writes or live telemetry. Saving neither applies nor selects the new profile.

action: wolf_cwl2.save_profile
data:
  config_entry_id: "<entry-id>"
  name: custom-night
  description: Quiet settings derived from the active base profile
  overwrite: false
  expected_revision: 12

Guarded actions, diagnostics, and recovery

wolf_cwl2.reset_filter requires one loaded entry, temporary/persistent mode, a fresh serial match, and confirmation EXECUTE ACTION. Appliance reset has no entity and additionally requires the disabled-by-default entry opt-in, a Home Assistant administrator context, and confirmation RESET APPLIANCE. Its response means only command_sent; cached availability is invalidated and ordinary polling reconnects. Neither reset changes desired/profile Store state.

Download integration or device diagnostics through Home Assistant for versions, non-sensitive policy, scheduler/connection health, tier freshness, and availability/error counts. The download excludes endpoints, identity, live/raw values, desired/profile content, and exception text. Persistent Repairs are created only for changed identity, invalid Store data, or a newer unsupported Store schema—not for an ordinary disconnect.

Reconfigure an entry to move its gateway while retaining serial identity and Recorder history. Reload drains in-flight work and replaces only that entry's runtime. Removing an entry closes its transport and permanently deletes only its Home Assistant-owned desired state, lineage, and profiles; it does not write or reset the appliance.

Detailed operator records:

Interactive TUI

Start the full controller interface with the same JSON configuration:

uv run wolf-cwl2-tui --config wolf_cwl2_config.json

For an initial inspection that cannot write the device or configuration:

uv run wolf-cwl2-tui \
  --config wolf_cwl2_config.json \
  --read-only

The left tree groups every documented register into monitor and settings submenus. Quick views cover the live overview, all registers, writable values, errors, and persistent desired state. Search matches words across keys, descriptions, Modbus addresses, status, and errors. The right panel shows wire address, codec, limits, raw value, poll tier, timestamps, ownership, and safety flags for the highlighted value.

Use e or Edit / execute for metadata-derived enum, boolean, numeric, date/time, or one-shot editors. Restorable settings can be persistent or temporary. Communication changes and appliance reset require exact confirmation phrases because they can disconnect the active session. Profiles are always resolved and previewed before application. Use s or Save profile to review and save persistent desired-state changes as a new profile; when a profile was loaded, the saved profile extends it. The TUI skips the immediate startup restore, but control-enabled mode still follows the configured periodic and reconnect desired-state reconciliation policy. Use --read-only for a fully passive session, or Apply desired for an immediate explicit reconciliation.

Connected gateway

The repository's physical Waveshare gateway endpoint is intentionally kept in the local untracked config. Its verified configuration is:

Ethernet: TCP server, Modbus TCP-to-RTU, port 502
RS485:    19200 baud, 8 data bits, even parity, 1 stop bit
Device:   Modbus unit/slave ID 20
Offset:   0

The web interface remains on port 80. Keep its password outside source, snapshots, reports, and issue text.

On the CWL-2-325, communication menu 14 should match:

14.1 Bus connection: Modbus
14.2 Slave address:  20
14.3 Baud rate:      19.2 kbit/s
14.4 Parity:         Even

These values follow Brink's official Modbus UWA2-B/UWA2-E 614882-D documentation.

Configure and read safely

Create a local configuration and example profiles:

uv run wolf-cwl2 \
  --config wolf_cwl2_config.json \
  init-config --host <gateway-host>

Generated local config/state files are ignored by Git. A fresh configuration has an empty desired object and therefore owns no device settings.

Start with read-only commands:

uv run wolf-cwl2 --config wolf_cwl2_config.json snapshot --available-only
uv run wolf-cwl2 --config wolf_cwl2_config.json get supply_temperature_c
uv run wolf-cwl2 \
  --config wolf_cwl2_config.json \
  run --read-only --print-updates

Expected checks include plausible software/hardware versions, a 12-digit serial, unit address 20, realistic temperatures/airflows/fan speeds, and no one-word address shift. The physical device returns a connected zero-word response for the absent optional extension_hardware_version; the library isolates it as unavailable while preserving neighboring extension values.

Python API

import asyncio

from wolf_325 import WolfCWL2


async def main() -> None:
    controller = WolfCWL2("wolf_cwl2_config.json")

    async def on_change(update: dict) -> None:
        print(update["key"], update["value"])

    controller.subscribe(on_change)
    await controller.start(read_only=True)
    try:
        print(controller.get_value("supply_temperature_c"))
        print(controller.snapshot(available_only=True))
    finally:
        await controller.stop()


asyncio.run(main())

Core read/cache methods:

await controller.start()
await controller.stop()
await controller.poll_once()
await controller.refresh("filter_status")
controller.get_value("supply_temperature_c")
controller.get_state("supply_temperature_c")
controller.snapshot(available_only=True)

Validated writes are available after values have been reviewed against the physical installation:

await controller.set_ventilation_level("normal")
await controller.set_airflow(180)
await controller.set_bypass_mode("automatic")
await controller.set_standby(False)
await controller.set_setting("filter_warning_days", 120)
await controller.apply_profile("night")
await controller.save_profile("custom-night", description="Night with 120-day filter interval")

Setters default to persist=True. The normalized desired value is written to the config before Modbus I/O, so an offline write remains queued for startup or reconnect restoration. Use persist=False for a temporary change.

Profiles and CLI writes

Example profiles are generated under the configured profiles_dir:

uv run wolf-cwl2 --config wolf_cwl2_config.json profiles
uv run wolf-cwl2 --config wolf_cwl2_config.json preview-profile night
uv run wolf-cwl2 --config wolf_cwl2_config.json profile night
uv run wolf-cwl2 \
  --config wolf_cwl2_config.json \
  save-profile custom-night --description "My adjusted night profile"

save-profile is local-only: it does not connect to or write the appliance. It compares persistent desired state with the last loaded profile, writes only changed/new settings plus released parent settings, and sets extends to that parent. Without a loaded profile it writes all desired settings as a standalone profile. Temporary writes are intentionally excluded. Existing files are protected unless --overwrite is supplied, and saving does not activate the new profile or change desired state.

Other write commands include:

uv run wolf-cwl2 --config wolf_cwl2_config.json level high
uv run wolf-cwl2 --config wolf_cwl2_config.json airflow 180
uv run wolf-cwl2 --config wolf_cwl2_config.json bypass automatic
uv run wolf-cwl2 --config wolf_cwl2_config.json standby off
uv run wolf-cwl2 --config wolf_cwl2_config.json set filter_warning_days 120

Add --temporary to supported commands to avoid persistent ownership. Appliance reset requires reset-appliance --yes; communication registers are deliberately not exposed as normal CLI writes.

Tests and physical audit

Run the complete simulated suite and branch-aware coverage:

uv run pytest
uv run pytest --cov=wolf_325 --cov-branch --cov-report=term-missing

The physical audit is opt-in and read-only. It reads every catalogue definition individually—including the two normal poll="never" reset-status values—and then verifies normal block polling:

WOLF_325_DEVICE_CONFIG=/path/to/local-device-config.json \
WOLF_325_DEVICE_REPORT=/tmp/wolf-325-device-audit.json \
uv run pytest -m hardware tests/hardware/test_read_all.py

The verified 2026-07-18 run accounted for all 154 definitions: 153 available, one unsupported optional, zero decode errors, zero failed keys, and zero required failures. The raw report is intentionally untracked because it contains the device serial and live operational data.

Architecture records

Durable package boundaries and behavior contracts are documented in:

License

This project is licensed under the MIT License. Copyright (c) 2026 Julian Kimmig.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wolf_325-0.1.0.tar.gz (56.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wolf_325-0.1.0-py3-none-any.whl (72.9 kB view details)

Uploaded Python 3

File details

Details for the file wolf_325-0.1.0.tar.gz.

File metadata

  • Download URL: wolf_325-0.1.0.tar.gz
  • Upload date:
  • Size: 56.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.7.21

File hashes

Hashes for wolf_325-0.1.0.tar.gz
Algorithm Hash digest
SHA256 f7be2d24a86f4ef530c7dafc13e5e36901d5e84bdf0e45270f0c693ff8311b7f
MD5 28e67a30e3cd129ee1375ee70944f06a
BLAKE2b-256 a15fc0aa9e576cc82235489b6357a4a21810a832eda319499f94cf48b3debe0f

See more details on using hashes here.

File details

Details for the file wolf_325-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: wolf_325-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 72.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.7.21

File hashes

Hashes for wolf_325-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1e60bc6dfe08d11818d7af44c5b8e684f65923b2302df73b64f5f48f3ae86f43
MD5 4a8d04a5a7867f2faeeede6c0d3772bb
BLAKE2b-256 fa8c1924767956795432daf2e0da0c906e0ef707979180122100331d21812e8d

See more details on using hashes here.

Release history Release notifications | RSS feed

0.1.2

2 files

0.1.1

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page