Repository navigation
Architecture Concept Document
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
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.
- 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_pontosintegration
- 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
/setendpoints
| 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:
- UC-1 Monitor: the user wants consumption, flow, pressure, temperature and device state on the dashboard.
- UC-2 Detect: an alarm, warning, notification or microleakage event must be visible and automatable.
- UC-3 Shut off: the water supply must be closable automatically (for example when leaving the house).
- UC-4 Configure: profile limits, leak test schedule and regeneration behaviour must be adjustable.
- UC-5 Upgrade: an existing installation must survive the rename of the integration.
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
- 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
unavailableand the last valid values stay untouched.
| 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 |
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
| 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.
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.
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.
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).
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.
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.
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.
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).
| 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 |
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
If the first poll fails, the entry ends in ConfigEntryNotReady and Home Assistant retries with
backoff – no failure loop, no half-created entities.
- The coordinator calls
fetch_data()for every URL of the model. - A failing endpoint is logged and does not discard the other endpoints.
- An empty result raises
UpdateFailed(entities becomeunavailable). - Each entity maps its endpoint to a value, applies format/scale/code dictionaries and runs the plausibility check (see SDD).
-
availableisTrueonly if the last poll succeeded and the entity has a usable value.
- The service resolves the target entry (or all entries) and validates the fields.
- The endpoint template is filled from the validated fields.
- A per-device command lock prevents interleaving with another command.
- After the command the coordinator refreshes, so the UI reflects the new state quickly.
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.
| 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 |
- 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.
| 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 |
| 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
|
- Version 2.9.3
- Domain
pontos - Verified with HA 2026.9.3