github nicois/foxess-control v1.0.22-beta.4

latest releases: v1.0.22-beta.6, v1.0.22-beta.5
pre-release2 months ago

Added

  • New sensor.foxess_pv3_power and sensor.foxess_pv4_power — per-string power for multi-MPPT inverters (issue #15). KH-series and other inverters with more than two MPPT inputs report pv3Power / pv4Power, which the integration never asked for. Both follow the existing PV1/PV2 pattern exactly: diagnostic category, kW, disabled by default — enable them in Settings → Devices & Services → Entities. On inverters with only two strings the variables are absent from the API response, so the entities simply read unavailable and nothing else is affected. Note the overview card's PV breakdown still shows PV1 + PV2 only.
  • New sensor.foxess_solar_pv_energy — "Solar PV Energy" (C-041). Cumulative photovoltaic-only yield, from the FoxESS PVEnergyTotal variable, which the integration did not previously poll at all. Energy device class, total_increasing, kWh, enabled by default. This is the sensor to use as the Home Assistant Energy dashboard's solar source. On inverter models that do not report PVEnergyTotal the entity simply reads unavailable — the FoxESS API silently omits an unsupported variable from the response (verified against a live KH10), so the rest of the poll is unaffected and no reconfiguration is needed.

Fixed

  • Two sensors were mislabelled as solar when they measure inverter output (C-041), which caused the HA Energy dashboard to count battery discharge twice. sensor.foxess_solar_generation_energy ("Solar Generation Energy" / German "Solarenergie") and sensor.foxess_generation ("Generation") are fed by the FoxESS generation / generationPower variables — the inverter's cumulative AC output energy and power, which include everything the battery discharged. The solar name and solar icon led users to wire the energy one into the Energy dashboard as their solar source alongside the battery-discharge sensor as their battery source; on a live KH10 one night's computed home consumption came out as 26.6 kWh against an actual house load of 7.2 kWh (raw cloud history for that night: generation +18.2 kWh, PVEnergyTotal +0.2 kWh, pvPower flat at 0). They are now named "Inverter Output Energy" and "Inverter Output" in every language, and no longer carry solar icons. This is a friendly-name change only — unique IDs are unchanged and Home Assistant keeps the entity IDs of already-registered entities, so existing automations, dashboards, statistics and history are untouched. Fresh installs get entity IDs derived from the new names (sensor.foxess_inverter_output_energy, sensor.foxess_inverter_output). If you had either sensor set as your Energy dashboard solar source, switch it to the new sensor.foxess_solar_pv_energy.
  • scripts/collect_ha_session.py recorded inverter output in its solar_power column when the locale-aware entity lookup was unavailable — the English fallback pointed at sensor.foxess_generation instead of sensor.foxess_solar_power.
  • docs/api/foxess-cloud-api.md documented generation as "Lifetime solar generation energy" — the origin of the mislabelling. It now describes generation / generationPower as the inverter's AC output side, documents PVEnergyTotal as the PV-only counter, records that todayYield reads 0.0 on the KH10 and must not be relied on, and documents that real/query silently omits unsupported variables (errno: 0) rather than rejecting the request.
  • Every scheduler write failed with FoxESS API error 40257: Parameters do not meet expectations on H3-15.0-Smart, H3-12.0-M and EVO 10-5-H (issues #17, #14, #12; new C-042). Force Discharge, Feed-in and Force Charge all failed immediately — as did session teardown — while polling, real-time data and setup worked normally. Root cause: the force-discharge power (fdPwr) written into every schedule group was derived as capacity_kw * 1050, a factor reverse-engineered from a KH10. The FoxESS API declares the accepted fdPwr ceiling per device (POST /op/v3/device/scheduler/get → properties.fdpwr.range.max), and on a KH10 that ceiling is exactly 10 * 1050 = 10500 W — so the heuristic was right there by coincidence. Other model families declare the plain nameplate rating, so the same arithmetic overshot by 5 % (12600 W on a 12 kW H3, 5250 W on a 5 kW EVO) and the API rejected the whole write. The integration now reads the device's declared ranges once at setup and clamps fdPwr, fdSoc and minSocOnGrid into them at the single point every schedule write passes through, so no service argument or pacing value can produce a rejected payload. max_power_w (the ceiling the pacing algorithms work against) is now the lower of the capacity-derived value and the declared maximum. Installs whose devices do not expose the declared ranges keep the previous behaviour unchanged, and devices whose declared ceiling already equals capacity * 1050 (KH family) write a byte-identical payload — no reconfiguration is needed for anyone. Regression class tests/test_scheduler_device_limits.py.
  • Diagnostics always reported inverter_model: null. The field read a non-existent model attribute instead of device_type, so every diagnostics download from every user showed a null model — which made the three 40257 reports above look like a model-recognition problem when the model string is not used for anything. Diagnostics now report the real model name (e.g. KH10, H3-12.0-M) plus a new scheduler_limits section carrying the device-declared fdPwr ceiling and supported work modes, so a future rejected write is diagnosable from the download alone (C-020, C-026).
  • A work mode the inverter does not list as supported now logs a warning naming the mode and the device's own supported list, instead of only surfacing the API's opaque 40257. The write is still attempted, so a device that under-reports its capabilities keeps working.
  • Add-on could not start on Home Assistant OS (issues #9, #10). The add-on container died before reaching run.sh, so the integration was never copied into /config/custom_components/ — users saw the add-on install successfully but nothing appear under Settings → Devices & Services → Integrations. foxess-control/config.yaml did not declare init:, and the Supervisor defaults that option to true (vol.Optional(ATTR_INIT, default=True) in supervisor/apps/validate.py, passed to Docker as init= in supervisor/docker/app.py), which puts Docker's own init process (tini) at PID 1. The add-on is built on Home Assistant's Alpine base images, which ship s6-overlay v3 (3.2.2.0) and declare ENTRYPOINT ["/init"]; s6-overlay v3 requires its /init to be PID 1 and aborts with s6-overlay-suexec: fatal: can only run as pid 1 otherwise. Fixed by declaring init: false, which the add-on documentation has required for s6-overlay-v3 base images since 2022. This is the second of two stacked failures at the same choke point: the [FATAL tini (7)] exec /init failed: Permission denied variant both users quoted was the custom AppArmor profile that omitted /init (dropped in 1.0.19), and removing tini from the exec chain rules out both. No change to the integration itself — entity IDs, unique IDs and behaviour are untouched; only how the installer container boots. Regression class tests/test_addon_supervisor_boot.py, which builds the real add-on image and boots it the way the Supervisor does.

Changed

  • Test infrastructure: two pytest runs on one machine no longer destroy each other (new C-043). No user-visible change — this is a developer-workflow fix. E2E container names were derived only from the xdist worker id (ha-e2e-gw0), and the ha_e2e fixture removes that name at setup as a hygiene step, so the second of two concurrent runs issued podman rm -f against the first run's live container; the losing run then polled a dead container until its budget expired and reported TimeoutError: HA did not become ready within 120s at setup, producing cascades of failures that looked like product regressions. Reproduced directly by running the same two-test subset from two working directories at once: 1 passed, 1 skipped, 2 errors (121.9s setup) versus 2 passed, 2 skipped for the run that won the race; after the fix both runs give 2 passed, 2 skipped in ~31s. Container names now carry a checkout token, the worker process's pid and the xdist worker id, and cleanup removes a container only when it can prove the container is this run's own or belongs to a run that is provably dead — so a crashed run's leftovers are still reclaimed but a live run's are never touched. Separately, _find_free_port() probed a host port and released it before podman run -p bound it, so a concurrent run's probe could be handed the same port (measured: 6 concurrent processes x 50 ports produced duplicates in 5 of 6 rounds); ports now come from an OS probe paired with a host-wide claim held for the life of the run, under a cross-process lock, so a collision is impossible rather than unlikely. tests/soak/ uses the same helpers, so a soak run and an E2E run can share a host, and .githooks/pre-push no longer blanket-stops every ha-e2e-* container. Regression class tests/test_e2e_container_isolation.py (29 tests). This restores local E2E as a trustworthy signal (C-031) — CI is unaffected, as it runs one job per machine.
  • Test infrastructure: the simulator now models device-declared scheduler ranges (/op/v3/device/scheduler/get), the deviceType / function fields of device detail, and rejects out-of-range groups the way the live API does. Test HTTP servers bind 127.0.0.1 rather than localhost — a dual-stack localhost bind assigns different ports to the IPv4 and IPv6 sockets, so the advertised port was only valid for one family and concurrently-running test servers could receive each other's requests (C-031).

Don't miss a new foxess-control release

NewReleases is sending notifications on new releases.