Skip to content

feat: add EMON energy sensors via /heatSources/emon endpoints - #14

Open
engels0n wants to merge 2 commits into
BassXT:mainfrom
engels0n:feat/emon-energy-sensors
Open

feat: add EMON energy sensors via /heatSources/emon endpoints#14
engels0n wants to merge 2 commits into
BassXT:mainfrom
engels0n:feat/emon-energy-sensors

Conversation

@engels0n

@engels0n engels0n commented Aug 5, 2026

Copy link
Copy Markdown

Closes #8

Summary

Adds energy consumption and heat production sensors (kWh) to the integration,
exposed through the undocumented heatSources/emon endpoints. All new energy
sensors are compatible with the Home Assistant Energy Dashboard.

Completely made with Claude Opus 5.

Background

Energy values are visible in the MyBuderus app but were never surfaced by this
integration. The obvious candidate path /energyMonitoring consistently returns
403 Forbidden, which is what made this hard to find.

Things that did not work, documented here so nobody repeats them:

  • RFC 8693 token exchange against the SingleKey IdP — fails with
    unsupported_grant_type. No additional token is needed; the regular access
    token works fine against pointt-api.
  • HTTPS proxying (mitmproxy) — blocked by certificate pinning in the Android app.

The endpoints were finally recovered by decompiling the Android APK with jadx,
which exposed both the real paths and the enums used to build them.

API structure

Base host: pointt-api (same host and auth as the existing calls). All EMON
reads go through the existing /resource/ endpoint, so no new transport layer
was needed.

Lifetime counters — one path per domain, no separate domain segment:

GET /gateways/{gatewayId}/resource/heatSources/emon/totalConsumption
GET /gateways/{gatewayId}/resource/heatSources/emon/chConsumption
GET /gateways/{gatewayId}/resource/heatSources/emon/dhwConsumption
GET /gateways/{gatewayId}/resource/heatSources/emon/coolingConsumption

Historical time series:

GET /gateways/{gatewayId}/resource/recordings/heatSources/emon/{domain}/{sub}?interval={interval}

interval is YYYY-MM-DD, YYYY-MM or YYYY.

The API answers 403, not 404, for a domain the appliance does not have, so
403 cannot be treated as an authentication failure here. See the token section
below.

Enums (extracted from the decompiled client)

EmonType
ch, dhw, cooling, pool, total, each also as a *Consumption variant.
The *Consumption values are what the client puts into the path.

SystemComponentDomainType
CH, COOLING, DHW, POOL, TOTAL

SubDomainSourceType
ELECTRICITY, COMPRESSOR, EHEATER, BURNER, SOLAR, OUTPUT_PRODUCED
— these appear as keys inside the response payload, and as the {sub} segment
for recordings.

This integration implements the four consumption paths above. pool is defined
in the app but not covered here, as no test system was available.

Response format

EmonValue returns a values array of single-key objects:

{
  "values": [
    { "compressor": 27.35 },
    { "eheater": 3.85 }
  ]
}

Keys are camelCase in the payload (outputProduced) and are converted to
snake_case before being mapped onto entities.

yRecording is used for the time-series endpoint and is aggregated by the new
sum_recording() helper.

Changes

api.py

  • New get_emon() and get_emon_recording()
  • New sum_recording() helper for aggregating yRecording payloads
  • {domain}_electric is derived as compressor + eheater, and scop as
    total_output_produced / total_electric. Neither value is returned by the
    API directly.
  • Raises if no domain returned any data at all, rather than silently producing
    an empty sensor set
  • Removed the dead token-exchange code path (it was never required)

coordinator.py

  • EMON data refreshes on its own 10-minute interval, separate from the main
    60-second poll. Lifetime counters change slowly, so there is no reason to poll
    them at the normal rate. This keeps the additional API load minimal.

sensor.py

  • New energy sensors with device_class: energy and
    state_class: total_increasing, so they can be selected in the Energy Dashboard
  • available returns False for domains the system does not have (e.g. cooling
    on a heating-only unit), instead of surfacing permanent unknown entities

const.py

  • EMON paths and the update interval defined centrally

Translations

  • strings.json, de.json and en.json updated with names for all new sensors

get_emon_recording() and sum_recording() are not yet called by any entity.
They are included because the recording endpoint is the only way to obtain
historical data, which is a prerequisite for backfilling long-term statistics —
happy to drop them from this PR if you would rather keep the diff minimal.

New entities

Key Meaning
total_electric Total electrical consumption
total_compressor Compressor consumption, total
total_eheater Backup heater consumption, total
ch_compressor Compressor consumption, space heating
dhw_compressor Compressor consumption, hot water
dhw_eheater Backup heater consumption, hot water
cooling_compressor Compressor consumption, cooling
total_output_produced Heat produced, total
dhw_output_produced Heat produced, hot water
scop Efficiency — heat produced / electricity consumed

scop is the only non-energy entity: it is a dimensionless ratio, so it carries
no device class and uses measurement instead of total_increasing. It is
therefore not selectable in the Energy Dashboard — the nine kWh sensors are.

Testing

Verified against a live heat pump. Example readings:

Metric Value
Compressor 27.35 kWh
Backup heater 3.85 kWh
Total electrical 31.2 kWh
Heat produced 55.55 kWh

The numbers are internally consistent
(27.35 + 3.85 = 31.2 kWh; 55.55 / 31.2 ≈ 1.78 COP), which confirms the values
are being read and mapped correctly.

Token handling and re-authentication

While testing the new sensors, a pre-existing bug surfaced that also affects
installations without EMON:

Both coordinators refreshed the access token independently. When the token
expired (~1 hour), they raced — SingleKey rotates the refresh token on use, so
the second request received invalid_grant and that coordinator failed
silently. Sensors went unavailable with no reauth prompt.

__init__.py

  • Token refresh is serialised behind an asyncio.Lock, with a double-check so
    the waiting caller does not refresh again unnecessarily

api.py

  • BuderusAuthError now carries the HTTP status. 401 (expired token) is
    propagated so the coordinator can trigger reauth; 403 is only skipped when
    probing optional EMON domains
  • A 401 triggers one forced token refresh and a single retry before failing

config_flow.py

  • async_step_reauth / async_step_reauth_confirmConfigEntryAuthFailed
    now surfaces a repair card instead of failing silently
  • A fresh PKCE verifier and state are generated per attempt
  • The returned gateway ID is validated against the existing entry, so signing
    in with a different SingleKey account aborts with wrong_account rather than
    silently rebinding the entry

Translations

  • Reauth strings added to strings.json, de.json, en.json

Compatibility

  • No breaking changes. No existing entity IDs, names or units were touched.
  • Purely additive — existing installations gain new sensors and nothing else.
  • Systems without a cooling domain simply show those sensors as unavailable
    rather than broken.

Screenshots (German UI)

image image image image image image image

@engels0n
engels0n force-pushed the feat/emon-energy-sensors branch from 9233458 to d88ae1c Compare August 5, 2026 09:50
@engels0n
engels0n force-pushed the feat/emon-energy-sensors branch from d88ae1c to 1221db4 Compare August 5, 2026 09:55
- api: EMON endpoints (/heatSources/emon/...), recording aggregation
- api: distinguish 401 (expired token) from 403 (domain unavailable)
- init: separate EMON coordinator, lock-guarded token refresh
- sensor: 10 energy/COP sensors
- config_flow: reauth via PKCE with fresh verifier and state
- translations: reauth strings (en, de)
@engels0n
engels0n force-pushed the feat/emon-energy-sensors branch from 505c969 to 23394db Compare August 5, 2026 12:53
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.

Feature Request - Energy consumpution

1 participant