Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 likeunless 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, 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 `([#<pr>](https://github.com/tomquist/astrameter/pull/<pr>))` 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.

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## 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), [#535](https://github.com/tomquist/astrameter/pull/535)).


## 2.2.2

Expand Down
6 changes: 6 additions & 0 deletions docs/esphome-powermeters.md
Original file line number Diff line number Diff line change
Expand Up @@ -887,6 +887,7 @@ external_components:
http_request:
useragent: esphome/astrameter
timeout: 5s
buffer_size_rx: 4096

sensor:
- platform: template
Expand All @@ -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: |-
Expand All @@ -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
Expand Down
19 changes: 19 additions & 0 deletions web/ts/generate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,25 @@ 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");

// 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({
Expand Down
24 changes: 20 additions & 4 deletions web/ts/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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");
Expand Down Expand Up @@ -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` +
Expand Down Expand Up @@ -520,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
Expand Down