Skip to content

Redesign: full Dimplex WPM integration (read + estimation + control + dashboards) - #19

Merged
nb3rt merged 13 commits into
mainfrom
redesign
Jun 21, 2026
Merged

Redesign: full Dimplex WPM integration (read + estimation + control + dashboards)#19
nb3rt merged 13 commits into
mainfrom
redesign

Conversation

@nb3rt

@nb3rt nb3rt commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Complete redesign of the Dimplex WPM (Modbus TCP) integration. Replaces the
v0.1.0 skeleton with a register-table-driven architecture.

What's included

  • Read foundation — canonical register table; ~100 entities; firmware-aware
    (H/J/L/M) status/lock/fault/sensor-error addresses; medium device tree;
    capabilities + device profiles (LAK9 + generic); two-step config flow with a
    device-probe auto-hint.
  • Estimation engine — Hz→W power LUT (status-aware), EN14511 2D COP, thermal
    comp/heater/defrost/loop, house/installation split, hydraulic flow; native kWh
    (RestoreSensor) for the Energy Dashboard. Measured-vs-estimated selection with
    a source attribute. Live calibration number entities.
  • Control (behind a default-off gate) — writable setpoints, SG-Ready /
    operating-mode selects, HC1/DHW climate thermostats; range-validated.
  • UX & quality — importable dashboards (built-in cards + optional apexcharts);
    in-package brand images; en/pl/de localization; diagnostics; quality_scale.yaml;
    CI (hassfest + HACS + ruff + pytest, all green).

Notes

  • Setpoint register scaling is assumed whole-°C and should be verified on real
    hardware before relying on writes (gate is off by default; writes validated).
  • Estimation requires the LAK9 profile + inverter-frequency register.

Design: DESIGN.md · register map: spec/REGISTERS.md · entity ids:
spec/ENTITY_IDS.md.

🤖 Generated with Claude Code

nb3rt and others added 13 commits June 21, 2026 18:19
Design artifacts for the full HACS integration redesign (dimplex_wpm):
- DESIGN.md: architecture, entity model + medium device tree, measurement
  source matrix (measured vs estimated per quantity), capabilities,
  native estimation engine, write-safety gating, device profiles, roadmap.
- spec/REGISTERS.md: canonical, adversarially-verified register map
  (single source of truth) — version-aware status/lock/fault addresses
  (L/M target), holding-only (FC03) rule, digit-group energy formula,
  real power vs estimation, SG Ready, settings, enums.
- spec/dimplex_modbus_spec_raw.md: verbatim dump of the official Dimplex
  NWPM Modbus TCP wiki (32 pages) used as the authoritative source.
- existing integration/: reference YAML (working estimator "sauce") +
  dashboards to be ported.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rebuild the component around a single canonical register table instead of
hand-written entities. No writes yet (read-only foundation).

Core:
- registers.py: canonical RegisterSpec/EnergyGroup table (HA-free, unit-tested)
  with version-dependent addresses (status/lock/fault/sensor-error per H/J/L/M),
  holding-only (FC03) reads + coils, scale/sign decode, digit-group energy
  formula, read-plan clustering (≤100 regs/chunk), module + capability gating.
- profiles.py: DeviceProfile (LAK9 calibration LUT/COP for M1 + generic_wpm),
  per-model default modules/capabilities.
- coordinator.py: rewritten to read the active register set, decode, resolve
  enum text (version-aware), combine energy groups, derive fault/lock flags.
- entity.py: shared mixin (device_info on the medium device tree, unique_id,
  value-presence availability).
- sensor.py / binary_sensor.py: entities generated from coordinator.specs +
  energy groups + controller_info; coils + derived problem flags.
- const.py: medium device-tree module names, operating-mode map, enum resolver,
  profile/module/capability config keys + resolvers; default firmware L/M.
- config_flow.py: two-step flow (connection+profile → modules+metering with
  device-probe auto-hint); validation now reads a holding register (FC03).
- modbus_client.py: add read_coils.
- manifest.json: correct docs/issue URLs, codeowner @nb3rt, integration_type hub.
- strings/en translations for the new flow.

Tests: tests/test_registers.py (pure-python, 6 cases) — decode, energy digit
groups, version addresses, capability/module filtering, read-plan clustering.

Also adds spec/DASHBOARD_DESIGN.md (background design output, for M3).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the estimation "sauce" natively (used when a meter is absent; see the
measurement-source matrix). All formulas are pure/unit-tested.

- estimation.py: Hz→W LUT (status-aware: DHW/defrost factors), EN14511 2D-COP
  interpolation (fixes the YAML round() bracketing bug), compressor/heater/total
  power, thermal compressor/heater/defrost-loss/loop, house/installation split
  via alpha(dΔT/dt), hydraulic flow estimate.
- coordinator.py: computes derived power/COP/heat/flow each cycle, ΔT + its time
  derivative, and measured-vs-estimated selection (electrical_power_best /
  heat_output_best, cop_measured). Live tunables dict seeded from the profile.
- sensor.py: Analytics device gains computed sensors (each tagged source=
  measured|estimated) and trapezoidal kWh integration sensors (RestoreSensor):
  electrical energy, heat energy, heat-to-house, heat-to-installation. Gated by
  capability / estimation_possible.
- number.py (new platform): live calibration entities (k_dhw, k_defrost,
  k_defrost_loss, heater/pump power, alpha base/sensitivity/deadband) →
  coordinator.tunables, RestoreNumber-persisted, NOT behind the write gate.
- __init__.py: pass the profile object + flow-sensor entity; add NUMBER platform.
- const.py: drop dead register-strategy / EMS-BMS-lock stubs (superseded by
  capabilities).

Tests: tests/test_estimation.py (6 cases) — LUT, status-aware power, COP
grid+bilinear+clamp, thermal/defrost, alpha deadband/clamp, flow clamp.
Total 12/12 pure-python tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- .github/workflows/ci.yml: hassfest, hacs/action (category integration,
  ignore brands until submitted), ruff check, pytest.
- manifest.json: sort keys to hassfest order (domain, name, then alphabetical);
  pin version 0.2.0.
- hacs.json: keep only valid keys (name, country, homeassistant, render_readme);
  drop non-schema domains/integration_type/filename/zip_release.
- pyproject.toml: ruff (E/F/W/I/UP, isort force-sort-within-sections) + pytest
  config. Applied ruff import-sort autofix across the package.

Locally green: ruff clean, 12/12 pytest, manifest hassfest-sorted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Read-only by default; all writes go to the heat pump only when the
"enable control" option is on, are range-clamped, and use documented encodings.

- registers.py: WriteSpec table + pure encode/decode (to_raw/from_raw), incl.
  enum-coded registers (5036 curve offset K=raw-19; cool-setpoint 15+0.5·raw).
  Writable set: DHW setpoint/min/max, HC1 room/fixed-flow/curve-end/curve-offset,
  pool setpoint (module-gated), operating-mode select. build_read_plan gains
  extra_holding so current values are read back.
- coordinator.py: enable_control flag → active_write_registers, their addresses
  added to the read plan, decoded into values (number=display, select=raw code).
- number.py: DimplexWritableNumber (writes holding register, range-clamped,
  ModbusException→HomeAssistantError). Calibration numbers unchanged/ungated.
- select.py: DimplexWriteSelect (operating mode) alongside the existing SG Ready
  select; both gated.
- __init__.py: pass enable_control from options.

Tests: +4 (write encode/decode/clamp, offset19, cool-setpoint, module gate);
16/16 pure-python tests pass.

NOTE: exact register scaling for some setpoints is unverified without hardware;
gate is OFF by default and writes are range-validated. Climate entities (HC1/DHW)
are the remaining M2 sub-item.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adversarial audit (4 agents) findings addressed:

Critical/blocking:
- modbus_client.close(): pymodbus 3.x close() is SYNC — drop the await (was a
  guaranteed TypeError on every unload/reload), wrap defensively.
- device.py: remove host/IP from the root device name (it seeds entity_ids →
  was baking the host into every controller entity_id, non-portable).
- __init__: default to the 'lak9' profile when none is set (was silently falling
  back to generic → empty Analytics).

Major/minor:
- select.py: SG Ready select uses _attr_name (had translation_key with no
  translation → unnamed/unstable id); guard current_option against unmapped
  codes; writable select availability based on last_update_success.
- number.py: writable-number availability based on last_update_success (operable
  even if read-back register is absent).
- config_flow.py: OptionsFlow no longer assigns self.config_entry (HA 2024.11+
  deprecation); async_get_options_flow() takes no arg.
- sensor.py: suppress estimated heat_energy_kwh when a heat meter exists (Energy
  Dashboard double-count); (meter)/(est.) name suffixes; energy-group display
  precision; add flow_rate_smoothed (EMA) sensor.
- coordinator.py: compute EMA-smoothed flow.
- registers.py: inverter_frequency device_class=frequency; HC1 room setpoint
  step=1 (avoid silent 0.5 truncation; scaling "verify on device"); gate
  pv_surplus behind electric-meter cap; energy groups named "(meter)"; add
  HC2/3 cooling-setpoint WriteSpec (uses the previously-dead cool_setpoint
  encoder).

Deferred (tracked): climate (HC1/DHW) platform; full dashboard entity-id
reconcile (done in M3); on-device verification of setpoint scaling.

ruff clean, 16/16 pytest, manifest hassfest-sorted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Authored the front-end against the REAL (derived) entity-id scheme, per the
pre-M3 audit reconcile.

- dashboards/dimplex_wpm.yaml: combined 5-view dashboard (Overview, Heat &
  Energy, History, Control, Diagnostics/Calibration). Built-in cards only (no
  HACS frontend dependency); module/meter cards self-hide via per-card
  visibility; no climate cards (platform deferred).
- spec/ENTITY_IDS.md: authoritative entity_id list + the device-name→prefix
  derivation rule (has_entity_name + _attr_name, no translation_key).
- Naming for clean/predictable slugs: Analytics device renamed from
  "Energy & analytics" → "Analytics" (prefix analytics_); ΔT/α computed-sensor
  names switched to ASCII ("Temperature difference", "House heat fraction").
- README rewritten for the redesigned integration (profiles, capabilities,
  measurement-source matrix, dashboards, write gate, limitations).
- spec/DASHBOARD_DESIGN.md: status note (v1 authored, slugs reconciled).

ruff clean, 16/16 pytest, dashboard + ci YAML validated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Brand assets shipped inside the integration (HA 2026.02+ supports in-package
  brand images, no separate home-assistant/brands PR needed):
  icon.png (256) + icon@2x.png (512) — square Dimplex mark, transparent;
  logo.png + logo@2x.png — full wordmark, transparent. Generated from the
  official Dimplex logo (recolored to brand red, alpha from source).
- translations/pl.json + de.json: full config + options flow (mirrors
  strings.json key structure; hassfest-compatible).

Entity-name localization (translation_key) intentionally deferred — entities use
_attr_name so entity_ids (and the shipped dashboards) stay stable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per developers.home-assistant.io/blog/2026/02/24/brands-proxy-api and the
integration file-structure docs, in-package brand images must live in
custom_components/<domain>/brand/ (local images override the brands CDN via the
new /api/brands proxy; no manifest change needed). Moved icon/icon@2x/logo/
logo@2x there.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- climate.py: HC1 and DHW thermostats behind the control gate. Single HEAT mode;
  current temperature from a read value (room/return for HC1, DHW temp for DHW),
  target temperature reuses the writable setpoint WriteSpec (encoding, range and
  read-back shared with the number platform). Platform.CLIMATE added.
- dashboards: Control view gains thermostat cards (visibility-gated); Heat &
  Energy view gains optional apexcharts-card section (dual-axis power/COP +
  stacked thermal) with the built-in history-graph kept as fallback.
- docs: ENTITY_IDS + README updated (climate now shipped).

ruff clean, 16/16 pytest, dashboard YAML valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entities now use translation_key instead of _attr_name, so names localize while
entity_ids stay English-derived and language-independent.

- entity.py: _apply_common sets _attr_translation_key = key. SG Ready select
  uses translation_key too.
- strings.json + translations/en.json: full entity{} section (129 names);
  English strings equal the former _attr_name, so entity_ids are unchanged...
- ...except the 4 integration energy sensors whose names were dynamic
  ("(est.)"/"(meter)"): fixed to stable names → ids drop the _est suffix
  (analytics_electrical_energy / heat_energy / heat_energy_to_house /
  heat_energy_to_installation). Dashboard + ENTITY_IDS updated accordingly.
- translations/pl.json + de.json: 129 localized entity names each (full
  coverage; key structure matches strings.json — hassfest-compatible).
- ENTITY_IDS.md: header note updated (translation_key → English-derived ids).

ruff clean, 16/16 pytest, all translation files key-match strings.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Toward Silver/Gold:
- PARALLEL_UPDATES on every platform (0 for read, 1 to serialize writes).
- diagnostics.py: config-entry diagnostics (config + coordinator data, host
  redacted) — Gold "diagnostics" rule.
- quality_scale.yaml: honest done/todo/exempt across Bronze→Platinum.
- README: Removal section (Bronze docs-removal) + quality-scale note.

Bronze/Silver largely met; documented gaps: HA-runtime test coverage,
entry.runtime_data, and Gold polish (icon/exception translations, reconfigure).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the adversarial pre-release review (no blockers; these are the real ones):
- modbus_client.write_register: map ConnectionError/OSError → ModbusException so
  control writes to an offline device surface a friendly HomeAssistantError
  instead of an unhandled traceback.
- Fix entity_id collision: raw register sensor 5170 renamed "Electrical power"
  → "Electrical power (meter)" (id analytics_electrical_power_meter) so it no
  longer clashes with the canonical "Electrical power" (electrical_power_best).
- controller_info: replace the always-zero "consecutive_failures" attribute with
  "last_update_success".
- Naming/UX: drop the redundant device word from ventilation/passive-cooling/
  solar entity names (was producing doubled display names + ids like
  ventilation_ventilation_*); regenerated en/strings, patched pl/de (incl.
  stripping the German "Lüftung " prefix). ENTITY_IDS.md updated.
- select.py: read software_version from the coordinator (consistency).

ruff clean, 16/16 pytest, en/pl/de key-parity with strings.json, dashboard valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@nb3rt nb3rt changed the title Redesign: design foundation + canonical register map (M0 in progress) Redesign: full Dimplex WPM integration (read + estimation + control + dashboards) Jun 21, 2026
@nb3rt
nb3rt marked this pull request as ready for review June 21, 2026 21:01
@nb3rt
nb3rt merged commit a76937c into main Jun 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant