From 1890a6c75d6d5159c6af28158381c76cd8ec183d Mon Sep 17 00:00:00 2001 From: tomquist <528585+tomquist@users.noreply.github.com> Date: Tue, 30 Jun 2026 04:58:42 +0000 Subject: [PATCH 1/6] Fix truncated HTTP responses in generated ESPHome powermeter config The Fronius Solar API (and other verbose HTTP meters) return a multi-KB JSON document, but the generated config left ESPHome's small default receive buffer in place, so the body was truncated and json::parse_json silently failed. The grid reading stayed stuck and the battery went uncontrolled until users manually enlarged the buffer (discussion #534). Emit buffer_size_rx on http_request and max_response_buffer_size on the per-request action (4 KB) so the config works as generated. Update the Fronius ESPHome doc example and add a changelog entry. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FEzZ7zoMfEEZt2cyrdiAV4 --- CHANGELOG.md | 2 ++ docs/esphome-powermeters.md | 6 ++++++ web/ts/generate.test.ts | 5 +++++ web/ts/generate.ts | 11 +++++++++-- 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 57e64192..6a3f13e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next +- **Fixed** the generated ESPHome config for HTTP-polled meters (notably the **Fronius Smart Meter** via the inverter's Solar API) truncating large JSON responses, which left the grid reading stuck and the battery uncontrolled until you manually enlarged the receive buffer. The config generator and docs now set a 4 KB receive buffer so it works as-is ([#534](https://github.com/tomquist/astrameter/discussions/534)). + ## 2.2.2 diff --git a/docs/esphome-powermeters.md b/docs/esphome-powermeters.md index e8f8c6ec..974fc3f0 100644 --- a/docs/esphome-powermeters.md +++ b/docs/esphome-powermeters.md @@ -887,6 +887,7 @@ external_components: http_request: useragent: esphome/astrameter timeout: 5s + buffer_size_rx: 4096 sensor: - platform: template @@ -900,6 +901,7 @@ interval: - http_request.get: url: http://192.168.1.130/solar_api/v1/GetMeterRealtimeData.cgi?Scope=Device&DeviceId=0 capture_response: true + max_response_buffer_size: 4096 on_response: then: - lambda: |- @@ -913,6 +915,10 @@ ct002: power_sensor_l1: grid_l1 ``` +The Solar API returns a multi-KB JSON document, so the `buffer_size_rx` / +`max_response_buffer_size` above are required — with ESPHome's small default the +response is truncated and `json::parse_json` silently fails. + If your readings have the wrong sign, add a `multiply: -1` filter on the sensor. For three-phase, add `grid_l2` / `grid_l3` template sensors and publish the diff --git a/web/ts/generate.test.ts b/web/ts/generate.test.ts index bd74d82b..b0ca4324 100644 --- a/web/ts/generate.test.ts +++ b/web/ts/generate.test.ts @@ -243,6 +243,11 @@ has(eyJsonHeaders, " - http_request.get:\n url: http://x/api", "es has(eyJsonHeaders, " capture_response: true", "esp/json_http: capture_response nested under action"); has(eyJsonHeaders, " on_response:", "esp/json_http: on_response nested under action"); has(eyJsonHeaders, " headers:", "esp/json_http: headers nested under action"); +// Large JSON responses (e.g. the Fronius Solar API) overflow ESPHome's small +// default receive buffer and truncate the body, so json::parse_json fails. +// Enlarge the buffer on both the client and the per-request action (issue #534). +has(eyJsonHeaders, "buffer_size_rx: 4096", "esp/http: enlarged http_request rx buffer"); +has(eyJsonHeaders, " max_response_buffer_size: 4096", "esp/http: enlarged per-request response buffer"); has(eyJsonHeaders, " Authorization: Bearer t", "esp/json_http: header entry nested under headers"); const eyModbusTcp = generateEsphome({ diff --git a/web/ts/generate.ts b/web/ts/generate.ts index e7d5c357..d2c611f4 100644 --- a/web/ts/generate.ts +++ b/web/ts/generate.ts @@ -294,7 +294,14 @@ function esphomeSensor(state: State) { } if (esp.kind === "http") { - topBlocks.push(`http_request:\n${IND}useragent: esphome/astrameter\n${IND}timeout: 5s`); + // Some meters (notably the Fronius Solar API) return a multi-KB JSON + // document. ESPHome's default receive buffer is far smaller, so the body is + // truncated and json::parse_json silently fails. Enlarge it on both the + // client and the per-request action so the generated config works as-is. + const RX_BUFFER = 4096; + topBlocks.push( + `http_request:\n${IND}useragent: esphome/astrameter\n${IND}timeout: 5s\n${IND}buffer_size_rx: ${RX_BUFFER}`, + ); const use3 = phases === 3 && esp.url3; const sensors = (use3 ? ids : ["grid_l1"]).map((id, i) => templateSensor(id) + phaseFilterBlock(i)); const url = use3 ? esp.url3!(f) : (esp.url1 ? esp.url1(f) : "http://example.com/api"); @@ -322,7 +329,7 @@ function esphomeSensor(state: State) { const interval = `interval:\n${IND}- interval: 1s\n${IND}${IND}then:\n${IND}${IND}${IND}- http_request.get:\n` + `${IND}${IND}${IND}${IND}${IND}url: ${url}${headerLines}\n` + - `${IND}${IND}${IND}${IND}${IND}capture_response: true\n${IND}${IND}${IND}${IND}${IND}on_response:\n${IND}${IND}${IND}${IND}${IND}${IND}then:\n` + + `${IND}${IND}${IND}${IND}${IND}capture_response: true\n${IND}${IND}${IND}${IND}${IND}max_response_buffer_size: ${RX_BUFFER}\n${IND}${IND}${IND}${IND}${IND}on_response:\n${IND}${IND}${IND}${IND}${IND}${IND}then:\n` + `${IND}${IND}${IND}${IND}${IND}${IND}${IND}- lambda: |-\n` + `${IND}${IND}${IND}${IND}${IND}${IND}${IND}${IND}${IND}json::parse_json(body, [](${jsonRoot}) -> bool {\n` + `${IND}${IND}${IND}${IND}${IND}${IND}${IND}${IND}${IND}${IND}${IND}${lambdaBody}\n` + From a41d740e2591a7b4d104b210cdd6d01872df85cd Mon Sep 17 00:00:00 2001 From: tomquist <528585+tomquist@users.noreply.github.com> Date: Tue, 30 Jun 2026 05:00:31 +0000 Subject: [PATCH 2/6] Trim implementation details from changelog entry Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FEzZ7zoMfEEZt2cyrdiAV4 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a3f13e6..f5f00a0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Next -- **Fixed** the generated ESPHome config for HTTP-polled meters (notably the **Fronius Smart Meter** via the inverter's Solar API) truncating large JSON responses, which left the grid reading stuck and the battery uncontrolled until you manually enlarged the receive buffer. The config generator and docs now set a 4 KB receive buffer so it works as-is ([#534](https://github.com/tomquist/astrameter/discussions/534)). +- **Fixed** the generated ESPHome config for HTTP-polled meters (notably the **Fronius Smart Meter**) not reading the grid, which left the battery uncontrolled until the config was hand-edited. It now works as generated ([#534](https://github.com/tomquist/astrameter/discussions/534)). ## 2.2.2 From 85a0f3b1c95f5e1759c89e2f8119924ce3f63ec4 Mon Sep 17 00:00:00 2001 From: tomquist <528585+tomquist@users.noreply.github.com> Date: Tue, 30 Jun 2026 05:01:56 +0000 Subject: [PATCH 3/6] Link changelog entry to PR #535 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FEzZ7zoMfEEZt2cyrdiAV4 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f5f00a0e..5e872926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Next -- **Fixed** the generated ESPHome config for HTTP-polled meters (notably the **Fronius Smart Meter**) not reading the grid, which left the battery uncontrolled until the config was hand-edited. It now works as generated ([#534](https://github.com/tomquist/astrameter/discussions/534)). +- **Fixed** the generated ESPHome config for HTTP-polled meters (notably the **Fronius Smart Meter**) not reading the grid, which left the battery uncontrolled until the config was hand-edited. It now works as generated ([#534](https://github.com/tomquist/astrameter/discussions/534), [#535](https://github.com/tomquist/astrameter/pull/535)). ## 2.2.2 From a4f9ff8e87abee06d64575aa07420f8b630de347 Mon Sep 17 00:00:00 2001 From: tomquist <528585+tomquist@users.noreply.github.com> Date: Tue, 30 Jun 2026 05:02:49 +0000 Subject: [PATCH 4/6] Make "no implementation details in changelog" rule explicit in AGENTS.md Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FEzZ7zoMfEEZt2cyrdiAV4 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 785b5b83..13fa96b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,7 +69,7 @@ For user-facing work, contribute **exactly one bullet under `## Next`** that sum Do **not** expand `CHANGELOG.md` with every internal or tooling-only follow-up. If the change's bullet already states the high-level theme, leave it unless the **user-visible** story changes. -Write each bullet for the **user**, not the implementer: describe what changed for them and why it matters, and keep it **compact and clear**. Leave out implementation details — internal symbol/function/class/file names, config knob mechanics, data structures, parity-mirror notes, and the like — unless a user genuinely needs them (e.g. a config option or env var they set). Prefer one tight sentence over an exhaustive list of everything touched. +Write each bullet for the **user**, not the implementer: describe what changed for them and why it matters, and keep it **compact and clear**. **No implementation details in the changelog** — leave out internal symbol/function/class/file names, config knob mechanics, data structures, parity-mirror notes, buffer sizes and other low-level tuning, and the like, unless a user genuinely needs them (e.g. a config option or env var they set). State the user-visible problem and outcome, not *how* it was fixed. Prefer one tight sentence over an exhaustive list of everything touched. **Link the bullet to its PR once the number is known** — append a `([#](https://github.com/tomquist/astrameter/pull/))` reference (alongside any issue links already cited) so the changelog points back to the change. The PR number usually isn't known when you first write the bullet, so add the link on the follow-up iteration after the PR exists. **Always do this as soon as you learn the PR number** (e.g. the moment a PR is opened for the branch, or a number is shared with you) — don't wait to be asked: add the reference and push it in your next commit. From d4ef1b4014e56d0fedde90a96f05e42555bdbaa2 Mon Sep 17 00:00:00 2001 From: tomquist <528585+tomquist@users.noreply.github.com> Date: Tue, 30 Jun 2026 05:04:14 +0000 Subject: [PATCH 5/6] Keep no-implementation-details changelog rule general Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FEzZ7zoMfEEZt2cyrdiAV4 --- AGENTS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 13fa96b9..5a15fca1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,7 +69,7 @@ For user-facing work, contribute **exactly one bullet under `## Next`** that sum Do **not** expand `CHANGELOG.md` with every internal or tooling-only follow-up. If the change's bullet already states the high-level theme, leave it unless the **user-visible** story changes. -Write each bullet for the **user**, not the implementer: describe what changed for them and why it matters, and keep it **compact and clear**. **No implementation details in the changelog** — leave out internal symbol/function/class/file names, config knob mechanics, data structures, parity-mirror notes, buffer sizes and other low-level tuning, and the like, unless a user genuinely needs them (e.g. a config option or env var they set). State the user-visible problem and outcome, not *how* it was fixed. Prefer one tight sentence over an exhaustive list of everything touched. +Write each bullet for the **user**, not the implementer: describe what changed for them and why it matters, and keep it **compact and clear**. **No implementation details in the changelog** — leave out internal symbol/function/class/file names, config knob mechanics, data structures, parity-mirror notes, and the like, unless a user genuinely needs them (e.g. a config option or env var they set). State the user-visible problem and outcome, not *how* it was fixed. Prefer one tight sentence over an exhaustive list of everything touched. **Link the bullet to its PR once the number is known** — append a `([#](https://github.com/tomquist/astrameter/pull/))` reference (alongside any issue links already cited) so the changelog points back to the change. The PR number usually isn't known when you first write the bullet, so add the link on the follow-up iteration after the PR exists. **Always do this as soon as you learn the PR number** (e.g. the moment a PR is opened for the branch, or a number is shared with you) — don't wait to be asked: add the reference and push it in your next commit. From a032c2b84251fe8b29e0207a7db66f76b496edd1 Mon Sep 17 00:00:00 2001 From: tomquist <528585+tomquist@users.noreply.github.com> Date: Tue, 30 Jun 2026 05:11:03 +0000 Subject: [PATCH 6/6] Merge meter and registration/cloud http_request into one block ESPHome allows only one top-level http_request:. An HTTP-polled meter emits one (useragent + RX buffer) and marstek_registration/cloud_reporting emit another (20s timeout); with both enabled the generated YAML had two http_request: blocks, so ESPHome would drop either the timeout or the new RX buffer. Merge into the meter's single block, bumping its timeout to 20s when a slow feature also needs it. Add a regression test. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01FEzZ7zoMfEEZt2cyrdiAV4 --- web/ts/generate.test.ts | 14 ++++++++++++++ web/ts/generate.ts | 13 +++++++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/web/ts/generate.test.ts b/web/ts/generate.test.ts index b0ca4324..a5871110 100644 --- a/web/ts/generate.test.ts +++ b/web/ts/generate.test.ts @@ -248,6 +248,20 @@ has(eyJsonHeaders, " headers:", "esp/json_http: headers nested under ac // Enlarge the buffer on both the client and the per-request action (issue #534). has(eyJsonHeaders, "buffer_size_rx: 4096", "esp/http: enlarged http_request rx buffer"); has(eyJsonHeaders, " max_response_buffer_size: 4096", "esp/http: enlarged per-request response buffer"); + +// An HTTP-polled meter plus cloud_reporting both need http_request:, but ESPHome +// allows only one. They must merge into a single block that keeps the RX buffer +// and uses the longer 20s timeout — not two blocks that drop one or the other. +const eyHttpPlusCloud = generateEsphome({ + target: "esphome", + esphome: {}, + meters: [{ type: "fronius", phases: 1, fields: { IP: "10.0.0.9" }, tuning: {} }], + ct: { fields: { CLOUD_REPORTING: "True", CLOUD_REPORTING_HOST: "eu.hamedata.com" } }, +}); +ok((eyHttpPlusCloud.match(/^http_request:/gm) || []).length === 1, "esp/http+cloud: exactly one http_request block"); +has(eyHttpPlusCloud, "buffer_size_rx: 4096", "esp/http+cloud: rx buffer survives merge"); +has(eyHttpPlusCloud, "timeout: 20s", "esp/http+cloud: timeout bumped to 20s"); +lacks(eyHttpPlusCloud, "timeout: 5s", "esp/http+cloud: meter's 5s timeout replaced, not duplicated"); has(eyJsonHeaders, " Authorization: Bearer t", "esp/json_http: header entry nested under headers"); const eyModbusTcp = generateEsphome({ diff --git a/web/ts/generate.ts b/web/ts/generate.ts index d2c611f4..d2055891 100644 --- a/web/ts/generate.ts +++ b/web/ts/generate.ts @@ -527,9 +527,18 @@ export function generateEsphome(state: State): string { const port = (useMeter && mFields.PORT) || mf.PORT || "1883"; out.push(`mqtt:\n${IND}broker: ${broker}\n${IND}port: ${port}`); } - // Both marstek_registration and cloud_reporting need a single http_request:. + // ESPHome allows only one top-level http_request:. An HTTP-polled meter + // already emits one (with useragent + RX buffer); marstek_registration and + // cloud_reporting also need one (with a longer 20s timeout). When both apply, + // merge into the meter's single block — bump its timeout to 20s rather than + // emitting a second block that would drop either the timeout or the buffer. + const meterHttpIdx = topBlocks.findIndex((b) => b.startsWith("http_request:")); if (wantMarstek || wantCloud) { - out.push(`http_request:\n${IND}timeout: 20s`); + if (meterHttpIdx >= 0) { + topBlocks[meterHttpIdx] = topBlocks[meterHttpIdx].replace(/timeout: \d+s/, "timeout: 20s"); + } else { + out.push(`http_request:\n${IND}timeout: 20s`); + } } // top blocks from the sensor (api/mqtt/uart/etc.) — de-dup api/mqtt if already added