Energy Metrics via the Prometheus HTTP API#
How to read household consumption and PV generation from outside the cluster — for an agent, script or notebook that has no kubectl access and talks only to https://prometheus.internal.neese-web.de.
Everything below was measured against the running system, not inferred from configuration.
1. Access: it does not work as configured today#
Prometheus sits behind Traefik with the Authelia forward-auth middleware (authelia-forwardauth-authelia@kubernetescrd), and apps/authelia/values.enc.yaml places *.internal.neese-web.de under policy: two_factor.
Measured behaviour against /api/v1/query?query=up:
| Request | Response |
|---|---|
| no credentials | 302 → https://auth.k8s.neese-web.de/?rd=… |
Authorization: Basic <wrong> | 401 |
Proxy-Authorization: Basic <wrong> | 302 (header not evaluated here) |
Two things follow. Authelia does parse the Authorization header — a wrong password yields 401 rather than the redirect. But HTTP Basic Authentication conveys a single factor, and a two_factor policy cannot be satisfied by one factor. A headless client therefore cannot reach the API today: it follows the redirect and receives the login page as HTML instead of JSON.
One of the two changes below is required.
2. Option A — Grafana service account token (recommended)#
Grafana is not behind Authelia. Measured: /api/health returns 200 without credentials, while /api/datasources, /api/search and /api/org return 401 — Grafana enforces its own authentication.
Grafana can proxy Prometheus queries, so an agent needs no Authelia change and gets a revocable, least-privilege token instead of a user password.
Setup, once, in the Grafana UI at https://grafana.internal.neese-web.de:
- Administration → Users and access → Service accounts → Add service account, role Viewer.
- Add service account token, copy the
glsa_…value.
Find the Prometheus datasource UID:
Then query through the datasource proxy — the path after the proxy prefix is the ordinary Prometheus API:
Trade-off: the URL is Grafana's, not prometheus.internal.neese-web.de, and the token grants read access to everything that datasource can read. It changes no security policy and can be revoked in one click.
3. Option B — a one_factor rule in Authelia#
If the agent must talk to prometheus.internal.neese-web.de directly, lower only the API paths to one factor. Add this to the access_control.rules list in apps/authelia/values.enc.yaml, above the *.internal.neese-web.de catch-all — Authelia evaluates rules in order and the first match wins, so a rule placed after the wildcard never applies:
Create a dedicated account in lldap for the agent rather than reusing a human login. The browser UI keeps two_factor, because it is not matched by the resources regex.
The agent then authenticates with ordinary basic auth:
Not yet verified end to end. The 401/302 measurements above establish that Authelia parses Authorization, but the success path could not be tested without valid credentials. Confirm it returns 200 with JSON after applying the rule; if it still redirects, the rule ordering is the first thing to check.
Trade-off: read access to every metric in the cluster is reduced to a single password. Option A is preferable unless the direct hostname is a requirement.
4. Querying#
Use --data-urlencode; several of the queries below contain +, - and {} which break if pasted raw into a URL. POST works on the same paths and is safer for long expressions.
5. Metric catalogue#
Inverter — Deye SUN-M160G4-EU-Q0#
Read over Solarman V5 from the logger stick, published to MQTT, exposed by mqtt2prometheus in namespace solar. Values shown are examples from a midday scrape.
| Metric | Unit | Notes |
|---|---|---|
deye_ac_active_power_watts | W | current generation, e.g. 329.6 |
deye_ac_voltage_volts | V | grid voltage at the inverter, e.g. 239 |
deye_ac_current_amperes | A | |
deye_ac_frequency_hertz | Hz | e.g. 50 |
deye_production_today_kwh | kWh | the inverter's own daily counter, resets at midnight |
deye_production_total_kwh | kWh | lifetime counter, genuinely cumulative |
deye_dc_power_watts | W | per MPPT string, label string="pv1".."pv4" |
deye_dc_voltage_volts | V | per string |
deye_dc_current_amperes | A | per string |
deye_dc_production_today_kwh | kWh | per string |
deye_dc_total_power_watts | W | all strings together |
deye_operating_power_watts | W | the inverter's own consumption |
deye_radiator_temperature_celsius | °C | e.g. 52.8 |
deye_uptime_minutes | min | |
deye_logger_online | 0/1 | 1 when the logger answered the last poll |
Only two of the four MPPT inputs are connected; pv3 and pv4 read 0.
Grid meter — FRITZ!Smart Energy 250#
Read over TR-064/AHA from the FRITZ!Box by fritzbox-exporter in namespace monitoring. Select the meter with {ain=~"16000.?0036823.*"}: the device appears both under its own AIN and under the sub-unit …-1, and only the sub-unit carries the power meter.
| Metric | Unit | Notes |
|---|---|---|
fritz_ha_multimeter_power_W | W | signed: positive = import, negative = export |
fritz_ha_multimeter_energy_Wh | Wh | cumulative, import only — verified never to decrease |
fritz_ha_device_present | 0/1 |
Derived series (recording rules)#
Defined in apps/monitoring/k8s.prometheusrule.energy.yaml.
| Series | Meaning |
|---|---|
home:power_grid_import:watts | the meter reading, signed; keeps its name for history continuity |
home:power_generation:watts | PV generation, or vector(0) so it does not vanish at night |
home:power_total:watts | true household load |
home:autarky:ratio | share of consumption covered by own generation, capped at 1 |
6. How the values relate#
The meter is signed, so a single equation holds in both directions:
From that:
And over a day:
Those three daily quantities — self-consumed PV, grid import, export — are mutually exclusive. Gross PV yield is not disjoint from export: export is a part of it, so adding yield, import and export double-counts.
7. Ready-made queries#
8. Pitfalls#
Each of these produced a wrong answer during development.
The grid value is signed. Treating it as import only overstates household load and makes self-sufficiency exceed 100%. Filter with clamp_min(…, 0) when you want import, clamp_min(-…, 0) when you want export.
Aggregate the infrastructure labels away. Every exporter pod restart creates a new series with a different pod/instance. A query over a 24h window then returns several series per device — one per pod that existed. Use max(...), or max by (string) (...) where a label must survive.
The deye_* series disappear at night. The logger powers down with the inverter and mqtt2prometheus evicts the series after its 5-minute cache. Use or vector(0) when a missing generation value would break an expression, and prefer the inverter's own deye_production_today_kwh over reconstructing a day from increase() on the lifetime counter.
Recording rules have no past. A rule records from the moment it is created and never rewrites earlier samples. A query over history must spell the expression out rather than rely on a recently added or recently corrected rule.
A 1d step lands on midnight. Grafana and query_range align steps to multiples of the step, so a daily series produces no point for the current, incomplete day.
fritz_ha_multimeter_energy_Wh counts import only. It never decreases, so exported energy is absent from it. Export has to be integrated from the power series, as in the queries above.