Skip to content

Architecture Concept Document

acdcnow edited this page Sep 21, 2026 · 1 revision

Architecture Concept Document (ACD)

Project: Pontos – Home Assistant integration for Hansgrohe Pontos and SYR water meters Version: 2.9.3 Status: released Verified against: Home Assistant Core 2026.9.3


1. Purpose and scope

The integration connects water meters of the Hansgrohe Pontos and SYR families to Home Assistant and makes their measurements and settings available as entities and services. It is a local push/pull integration: Home Assistant polls the HTTP API of the device in the local network and sends commands back to it. There is no cloud service, no account and no internet dependency.

In scope

  • Reading all values that the devices expose through their HTTP API
  • Exposing relevant values as Home Assistant entities of the appropriate domain
  • Sending commands (valve, profiles, leak tests, regeneration, buzzer)
  • Protecting the Home Assistant recorder and the long term statistics from implausible device values
  • A guided, backwards compatible upgrade from the previous hass_pontos integration

Out of scope

  • Cloud connectivity (SYR Connect / iot-syr) and firmware updates
  • Automatic discovery of devices in the network
  • Reading or writing the SD card / the device file system
  • Control of the device beyond the documented /set endpoints

2. Stakeholders and use cases

Stakeholder Interest
Home owner (primary user) Water consumption, leak detection, water shut-off – reliable values, no maintenance
Automations and dashboards Stable entity ids, long term statistics, services that can be targeted per device
HACS user Simple installation, visible version, working validation
Maintainer / contributor Declarative per-device configuration, testable without hardware, enforced versioning

Main use cases:

  1. UC-1 Monitor: the user wants consumption, flow, pressure, temperature and device state on the dashboard.
  2. UC-2 Detect: an alarm, warning, notification or microleakage event must be visible and automatable.
  3. UC-3 Shut off: the water supply must be closable automatically (for example when leaving the house).
  4. UC-4 Configure: profile limits, leak test schedule and regeneration behaviour must be adjustable.
  5. UC-5 Upgrade: an existing installation must survive the rename of the integration.

3. System context

flowchart LR
    subgraph LAN[Local network]
        HA[Home Assistant Core]
        DEV[Pontos or SYR water meter]
        HA -->|HTTP GET /get/...| DEV
        DEV -->|HTTP GET /set/...| HA
    end
    REC[(Recorder and statistics)]
    HA --> REC
Loading
  • Home Assistant Core loads the integration, drives the polling loop, stores the states and calls the services.
  • Device answers over plain HTTP on port 5333 with a flat JSON document of endpoints (getVOL, getBAR, ...).
  • No cloud dependency: if the device is unreachable, entities become unavailable and the last valid values stay untouched.

Interfaces

Interface Direction Description
GET /<prefix>/get/all out All values in one request (preferred)
GET /<prefix>/get/cnd out Conductivity / hardness (Pontos Base)
GET /<prefix>/set/ADM/(2)f out Enter admin mode before reading /get/cnd
GET /<prefix>/set/..., /clr/... out Commands (valve, profiles, leak test, ...)
Entity state + services in Home Assistant API for the user

4. Architecture overview

flowchart TD
    subgraph Integration[custom_components/pontos]
        CF[config_flow.py - config, options, legacy adoption]
        INIT[__init__.py - setup, unload, logging]
        COORD[coordinator.py - polling]
        UTILS[utils.py - HTTP fetch and command]
        DEV[device.py - device registry info]
        SVC[services.py - service registry and dispatch]
        MIG[migrate.py - config entry migrations]
        subgraph Platforms
            SEN[sensor.py]
            BS[binary_sensor.py]
            NUM[number.py]
            SEL[select.py]
            SWI[switch.py]
            TIM[time.py]
            VAL[valve.py]
            BTN[button.py]
        end
        subgraph Config[device_config/]
            CP[conf_pontos.py]
            CT[conf_trio.py]
            CS[conf_safetech.py]
            CV[conf_safetech_v4.py]
            CN[conf_neosoft.py]
            CM[common.py - shared builders]
        end
        CONST[const.py - domain, options, helpers]
    end
    HA[Home Assistant Core] --> INIT
    INIT --> COORD
    INIT --> DEV
    INIT --> SVC
    COORD --> UTILS
    SVC --> UTILS
    Platforms --> COORD
    Platforms --> Config
    CF --> UTILS
    CONST --> Platforms
Loading

Layers

Layer Responsibility Depends on
Deklarative device configuration (device_config/) What a model offers: sensors, services, buttons, numbers, binary sensors, platforms, URLs Home Assistant constants only
Transport (utils.py) Fetching and merging JSON, sending commands, timeouts aiohttp session of Home Assistant
State management (coordinator.py) Poll cycle, interval, staleness, availability transport, options
Platforms Mapping declarative configuration onto entities coordinator, device info, options
Interaction (config_flow.py, services.py) User input, validation, commands transport, options
Lifecycle (__init__.py, migrate.py, device.py) Setup, unload, migrations, device registry everything above

The important architectural rule: models differ only in configuration, not in code. A new device family is added by creating a configuration module, not by writing a new integration.


5. Architecture decisions

AD-1 One config entry per device, no YAML

Every physical device is represented by exactly one config entry. All settings live in the entry (data = model and name, options = connection, polling, data quality, entity and logging switches). This keeps multi-device setups isolated, makes the options flow the single place to change behaviour and allows a reload per device.

AD-2 Polling through a DataUpdateCoordinator

All endpoints of a device are read in one poll cycle and merged into a single data dictionary. The coordinator owns the interval, the availability state (last_update_success) and the timestamp of the last successful poll (TimestampDataUpdateCoordinator). Entities never talk to the device directly, so the number of HTTP requests stays constant regardless of the number of entities.

Consequence: a failed poll makes all entities of that device unavailable, but the last data stays in memory. No partial entity flicker, no duplicated requests.

AD-3 Implausible values never reach the recorder

A cumulative water meter can never count backwards, and devices under load or during start-up occasionally return negative or non-numeric values. Because Home Assistant derives the long term statistics from the raw state values, such a value would be interpreted as a counter reset and would permanently distort the statistics.

Decision: values below the allowed minimum (0 for cumulative meters) are rejected. The sensor keeps its last valid value, exposes the rejected value through attributes and becomes unavailable if the tolerance window (default 60 minutes) is passed. The rule is enabled per sensor, automatically for total and total_increasing state classes.

Alternatives considered: clamping to 0 (writes a wrong but plausible value – rejected), passing the value through and fixing the statistics afterwards (impossible for the recorder – rejected).

AD-4 Declarative device model

Device differences (endpoint names, units, scaling, code dictionaries, available services, platforms) are expressed as data in device_config/conf_<model>.py. Shared structures are built by device_config/common.py. The platform code is model agnostic and works on the dictionaries.

Consequence: adding a device is a review-friendly data change. Common behaviour (validation, targeting, data quality) is implemented exactly once.

AD-5 Domain rename with adoption instead of a hard break

2.9.3 renamed the integration from hass_pontos to pontos. Home Assistant cannot load two integrations from one repository and a config entry cannot change its domain, so a seamless migration is impossible at runtime. Decision: the config flow detects a leftover legacy entry, takes over its values, removes the old entry (which also removes its entities) and creates a new entry. As the device, the entity names and therefore the entity ids stay identical, the recorded history and the long term statistics remain attached to the same entities.

AD-6 Localisation and naming

User visible strings are translation keys resolved by Home Assistant (translations/*.json). English is always complete and is the fallback for other languages. Entities use has_entity_name so the device name is composed by Home Assistant and renaming a device does not touch entity ids.

AD-7 Services are addressable per device

Every service accepts a target (device_id or entry_id). Without a target the command is applied to all configured devices and a warning is logged. Field validation happens in a voluptuous schema per service, and the endpoint placeholders are resolved from the same fields, so the documented fields and the implementation cannot drift apart (which was the case before 2.9.3).


6. Quality goals

Goal Scenario Measure
Data integrity A device reports -1 liters while starting up The value is rejected, the previous valid value is kept, statistics show no reset
Availability transparency The device is off the network for a while All entities of the device become unavailable, values recover automatically
Setup robustness The device answers slowly or partially Configurable timeout and retries, partial payloads are still used
Maintainability A new model must be supported Only a new conf_<model>.py plus an entry in MAKES
Upgradeability A user upgrades from hass_pontos One documented step, settings and statistics survive
Traceability A user reports a problem Debug logging switch, supported values and rejected values visible as attributes

7. Runtime view

7.1 Start-up

sequenceDiagram
    participant HA as Home Assistant
    participant I as __init__.py
    participant C as Coordinator
    participant D as Device
    HA->>I: async_setup_entry(entry)
    I->>C: create coordinator with entry options
    I->>C: async_config_entry_first_refresh()
    C->>D: GET get/all (plus get/cnd for Pontos Base)
    D-->>C: JSON of all endpoints
    I->>I: register device (serial, MAC, versions)
    I->>I: register services
    I->>I: forward platforms (awaited)
    I-->>HA: True
Loading

If the first poll fails, the entry ends in ConfigEntryNotReady and Home Assistant retries with backoff – no failure loop, no half-created entities.

7.2 Poll cycle

  1. The coordinator calls fetch_data() for every URL of the model.
  2. A failing endpoint is logged and does not discard the other endpoints.
  3. An empty result raises UpdateFailed (entities become unavailable).
  4. Each entity maps its endpoint to a value, applies format/scale/code dictionaries and runs the plausibility check (see SDD).
  5. available is True only if the last poll succeeded and the entity has a usable value.

7.3 Command

  1. The service resolves the target entry (or all entries) and validates the fields.
  2. The endpoint template is filled from the validated fields.
  3. A per-device command lock prevents interleaving with another command.
  4. After the command the coordinator refreshes, so the UI reflects the new state quickly.

7.4 Options change

Saving the options triggers the config entry update listener, which reloads the entry. A reload is the only correct reaction because the model, the entity set, the polling interval and the coordinator itself can all change.


8. Cross-cutting concepts

Concept Implementation
Configuration Config entry data (model, name) and options (everything changeable), defaults centralised in const.py
Error handling UpdateFailed for polls, ServiceValidationError for user errors, ConfigEntryNotReady for setup, warnings without data loss otherwise
Logging Module loggers, optional debug switch stored in the options
Localisation translations/<lang>.json, English as fallback, sections for the options form
Migrations migrate.py for config entry versions, config flow adoption for the domain rename
Security No credentials, plain HTTP in the local network only, no outbound connections
Testing Simulated device endpoints (testing/serve.py), endpoint coverage matrix (testing/endpoint_matrix.py), hassfest and HACS validation in CI

9. Constraints

  • Home Assistant must be able to reach the device; the API has no authentication.
  • The HTTP API is undocumented by the vendor and differs between models and firmware versions.
  • Home Assistant Core 2026.9 requires Python 3.14, so the integration can only be executed against a matching Home Assistant test instance.
  • The device has no push channel – everything is polling.
  • A config entry belongs to exactly one device; parallel access to the same device from two entries would produce duplicate entities.
  • The integration is a HACS custom repository; it is not part of the HACS default store.

10. Risks and open points

Risk Impact Mitigation
Firmware variations report unknown endpoints or units Entities stay unavailable or show implausible values Diagnostic entities, raw_value attribute, per-device configuration
Device returns implausible values for a longer period Sensor becomes unavailable after the tolerance window Tolerance is configurable, including "keep forever"
Statistics already distorted by an earlier version Wrong historical values Documented in the release notes, statistics can be corrected manually in the developer tools
Model reported differently than the device actually is Wrong endpoints, no values The model can be changed in the options at any time
Domain rename One manual step per installation Guided adoption flow, documented in the README
API documentation gaps (for example the semantics of clock endpoints) Do not invent entities Only endpoints with verified semantics are exposed

11. Traceability

Decision Implemented in
AD-1 config_flow.py, const.py, __init__.py
AD-2 coordinator.py, utils.py
AD-3 sensor.py (_process, _reject), const.py (options)
AD-4 device_config/*, device.py
AD-5 config_flow.py (async_step_adopt_legacy), migrate.py
AD-6 translations/*, _attr_has_entity_name in every platform
AD-7 services.py, services.yaml, device_config/common.py