Release v3.2.0 (stable)#27
Merged
Merged
Conversation
…ot" (#13) peter's /info showed 0 reconnect counters this boot but 8 events under "Events this boot" — those were leftover NVS events recorded by firmware from before boot-id stamping. The split fell back to e.start<=curUp when an event had no boot id, which is always true for a low-uptime prior-boot event, so they bucketed as "this boot". When the summary carries a boot id, an event without one is definitionally a prior boot → classify it as prior. Keep the start-heuristic only for the (ancient) case where the summary has no boot id at all. Counters and the event list now agree; stale events show under "From prior boots" and age out of the ring over time. /info regression-clean (CDP). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The original Pulsar uses a Zentri AMS module running TruConnect serial-over- BLE (service 175f8f23) — not u-blox (MAX) or BGX13P (Plus). It had zero support; this adds it, validated on real hardware (gambys, #12): lock/unlock, start/stop, current control, session energy and autolock all work. The path, all gated on the TruConnect MODE char (20b9794f) so it's inert on MAX/Plus/Copper/Quasar (regression-verified on the user's MAX — normal BAPI unaffected): - detect the module by the MODE char; relax conn params (latency 0, 6s supervision) — the older module dropped the link ~15-17s in otherwise; - skip the proactive SMP pair — this module is unauthenticated and pairing returns ENOTCONN and corrupts the ATT link (empty reads, silent BAPI); - write STREAM_MODE (1) to the MODE char so the module passes BAPI bytes transparently (RX char in, TX char notifications out); - fragment BAPI writes into (mtu-3) chunks: the module caps the ATT MTU at the 23-byte default and never negotiates up, so a ~41-byte frame can't go in one write (NimBLE would attempt a long/prepared write the stream char rejects). MTU>=44 links (MAX/Plus at 247) keep the single-write path. Known gap: schedules don't display on this firmware (3.0.0.23) — its r_schs shape differs; tracked as a #12 follow-up pending the charger's report. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The original (Zentri) Pulsar's r_schs times out; it reads schedules one slot
at a time via r_sch with a bare-integer sid (confirmed on gambys's hardware).
Surface the BLE-detected Zentri flag in /api/status, and in loadSchedules
dispatch on it: Zentri reads r_sch 0..3 and normalizes each
{sid,start:"HHMM",stop:"HHMM",days,mcr,nrg} into the array shape
renderSchedules already uses (start/stop are "HHMM" like MAX, so utcToLocal
handles them; enabled = days!=0; nrg -> target). renderSchedules gains a
view-only mode (no toggle/edit/delete buttons + a note) since this firmware's
schedule WRITE method isn't known yet. MAX/Plus unchanged (zentri=false ->
existing r_schs path; regression-verified on the user's MAX: 2 schedules
still render with full edit controls, zero console errors).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
loadSchedulesZentri hardcoded slots 0..3. Read sequentially until the charger reports no more — an error or non-object reply for an out-of-range sid marks the end — so it adapts to whatever slot count the firmware exposes (gambys's has 4; others may differ). Empty-but-valid slots are skipped while still reading past them; MAXSLOTS=16 is a safety backstop for a charger that returns empty instead of erroring past its last slot. MAX/Plus unaffected (array path). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…Mon-first labels (#12) The original Pulsar's schedule days bitmask is Sunday-first (bit0=Sun; confirmed days:1 == Sundays), but renderSchedules labels bits Monday-first (DAYS_M). Rotate Sun-first -> Mon-first in loadSchedulesZentri so the view-only display shows the correct days. Times confirmed stored UTC (utcToLocal already handles them). Zentri-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> EOF
The original Pulsar's schedule write is w_sch with a packed string
"<sid><startHHMM><stopHHMM><days3>": 1-digit slot, UTC start/stop as HHMM,
and a Sunday-first 3-digit day bitmask (e.g. "317002100001" = slot 3,
17:00-21:00, days=Sunday). Delete = the same slot all-zeroed
("<sid>00000000000"). Flips the original-Pulsar schedule UI from view-only to
fully editable:
- loadSchedules sets window._schZentri from /api/status; gated everywhere.
- saveSchZentri builds the w_sch packed string (converts the editor's local
time -> UTC HHMM and the Mon-first day picker -> the charger's Sun-first
bitmask); doDeleteScheduleZentri zeroes the slot.
- renderSchedules shows edit+delete (no toggle — this fw has no enabled flag);
the editor hides the Power/Energy-limit row and Enabled selector (w_sch
carries none of those). New schedules pick the first free slot (max 4).
MAX/Plus unchanged (window._schZentri=false -> s_sch/clr_sch path);
regression-verified on the user's MAX: toggle+edit+delete intact, editor shows
all fields, zero console errors. Zentri write path awaits gambys's test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… meter Charge-reminder engine (#127): the BLE task fetches the charger's schedules (r_schs on array models, per-sid r_sch on Zentri) on a fast cadence until populated, normalises them (start HHMM UTC + days bitmask, bit0=Sun), and computes the next enabled-schedule start in UTC against NTP time. Exposes next_scheduled_charge (epoch) + plug_reminder (due within reminderLeadMin AND car not connected) on /api/status and the wallbox/gateway MQTT topic, plus two HA discovery entities (timestamp sensor + binary_sensor). New reminderLeadMin config field + /config UI. Dashboard shows a next-charge banner / "not plugged in" warning. Meter capability (#129): r_dca error code 4 = no Power Boost / Power Meter (e.g. original Pulsar). Latches meterPresent=false and the dashboard hides the Power Flow card + House Power + Mains Voltage so a meter-less charger doesn't show empty grid/solar fields. Hardware-verified on Pulsar MAX: next_scheduled_charge matched the charger's daily 14:00 UTC schedule; banner states (info/warn/hidden) and meter show/hide toggled correctly; no console errors; no page regressions. Fixed two bring-up bugs: schedule poll was gated behind 5 min uptime (now fast-retries to ~15 s), and r_schs "enabled" is an integer so the strict `| false` read disabled every schedule (now .as<bool>()). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the official app's connect-cable prompt. The Power Flow Vehicle node swaps its kW label for a '🔌 Plug in' cue when no car is connected, derived from the r_dat status code (same set as carConnected()). Also exposes car_connected on /api/status (NB sta_connected is WiFi, not the car) so HTTP consumers (HA integration, Add-on) get a real cable signal. Verified live: status Ready (st=0) -> Plug-in shown; reverts to kW when connected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…+ command tagging) Add control_owner to WBConfig (persisted, default wallbox_schedule) with a /config dropdown (Wallbox native schedule / Integration / Add-on / None). Expose control_owner + last_command_by + last_command_age_s on /api/status. /api/command records the commander (optional &owner= tag, 'manual' if untagged) for start/stop/resume/current via the new wb_control module (mutex-guarded, no alloc under the spinlock) so controllers can detect a manual/other override. Advisory only — never rejects. Config import/export carries ctrl_owner. See docs/control-owner.md. Foundation only; controller behaviour (obey owner, disable/restore overlapping schedules, manual-override backoff) lands in the integration next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…dler The port-80 server is async (wb_web_async.cpp); the sync wb_web.cpp handler is secondary. The owner-tagging recordCommand() must live in the async handler to take effect. Verified live: GET /api/command?action=current&value=32&owner=test sets last_command_by='test' (age 2s) on /api/status with max-current unchanged. (buildStatusJson is shared, so the /api/status control fields already worked.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
New wb_charge_log module watches r_dat.cp and records each cp>0 burst {usid,start,stop,wh,gwh} to an NVS ring (survives reboot), ungated from MQTT. Exposed via /api/charge_log + status summary fields + 2 MQTT discovery entities (last_burst_energy, charge_log_count; kDiscoveryCount->68). Per-interval green via Dgen/Den fraction.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ties Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…sar (#12) The original/Zentri Pulsar's r_dat omits the `cp` (charge power, kW) field that Plus/MAX firmware reports — it only carries per-phase currents L1/L2/L3 (deci-amps). Every consumer (dashboard kW, MQTT charging_power, the charge-interval log) reads value_json.r.cp, so all of them silently showed nothing on this hardware. New wb_zentri_normalize module synthesises `cp` into the cached r_dat the moment it's polled, so every downstream surface works unchanged: cp = (L1 + L2 + L3) / 10 x V / 1000 [kW] Voltage resolution, two tiers: - measured per-phase voltage from a fitted Power Meter (r_dca v1/v2/v3) — exact, region-agnostic, zero config; - else a user-set nominal mains voltage (new mainsVoltage config, default 230) with region presets on /config: 230 UK/EU/AU/Asia, 240 NA L2, 208 NA 3-phase, 200 Japan, 120 NA L1, 127 LATAM. Power is only counted while st==1 (charging) so standby/decay current during "waiting" can't log phantom bursts. Zentri-gated (isZentri()), so Plus/MAX behaviour is unchanged; the existing cp guard is double protection. Also adds a Zentri-specific dashboard status-name table (ZN) since the small Zentri enum doesn't line up with the MAX 0-18 codes (esp. st4 = charge ramp, not "Paused"), gated on /api/status zentri:true. Verified: math matches real captures (L1=53@6A -> 1.22 kW, L1=103@11A -> 2.37 kW, 3-phase, idle/standby suppressed); builds clean; OTA to a Pulsar MAX confirms cp stays native and the new path is skipped. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rity) Replaces the gateway dashboard's horizontal Grid->Charger->Vehicle Power Flow card with the HA-style triangle the Add-on already uses: Solar (top) + Grid (bottom-left) feeding the Vehicle (bottom-right). Per-source kWh come from the r_lse live-session feed (green_energy / grid_energy / charged_energy); the solar branch lights only when this session actually drew solar, grid is the default source, and the live charge rate shows in the centre. The Vehicle node keeps the segmented battery-fill charge pulse. New staggered PLse() poller fetches r_lse on its own slower cadence (it's not a WS push and P() early-returns under WS), offset off the boot burst to respect the /api/command token bucket. Zentri/no-meter chargers that don't return r_lse simply show the grid branch + live cp, degrading cleanly. Built + OTA-verified: /dashboard/body.gz serves the new card (old pf-veh-kw / Power Flow markers gone), JS balanced, live r_lse populates the nodes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The triangle nodes showed cumulative session energy (r_lse green/grid/ charged kWh), so an idle charger displayed last-session totals with a lit grid line — looked like active flow when nothing was charging (#forum). Switch the nodes to LIVE POWER (kW): Vehicle = charging_power, Grid = live house/grid power from the r_dca meter (p1+p2+p3), Solar = live solar surplus (r_lse active_feature.surplus_power). Flow lines animate only while charging; the grid/solar split is approximated from surplus (charger has no live solar-vs-grid power split). Cumulative session kWh stays in the "Since plugged in" footer. Built + OTA-verified; JS balanced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nergy Solar node still shows live solar surplus AVAILABLE (kW) — honest about being available, not in-use. The "Since plugged in" footer now shows the cumulative SOLAR-USED (green) vs GRID energy split (kWh) from r_lse green_energy/grid_energy — i.e. the official app's "green" number — so available solar (node, kW) and used solar (footer, kWh) are both visible and no longer conflated. Built + OTA-verified; JS balanced. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A negative meter reading (p1+p2+p3 < 0) means solar surplus is being exported to the grid — which is now represented by the Solar node. Showing it as a negative value on the Grid node (whose arrow points Grid->Vehicle) was confusing and redundant. Grid now shows import only (max(0, kW)); export surfaces as Solar surplus. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "Resume Schedule" action unconditionally sent a hard Stop (w_cha par=2 on the MAX) before s_cmode mode:0, on the assumption that Stop is a no-op when not charging. A user hit error 114 pressing Resume on a paused/waiting (not charging) charger — that assumption was wrong; the hard Stop faults the charger in that state. Add WallboxBLE::isCharging() (cached r_dat.st==1 / r_sta.charger_status==1) and gate the defensive Stop on it in both the sync and async /api/command resume handlers. s_cmode mode:0 is only rejected (subcode 6) while actually charging, so the Stop is only needed then; when paused/idle, Resume now sends s_cmode mode:0 alone. Tested on a Pulsar MAX: Resume while idle sends s_cmode only, error_code stays 0 (no 114); charger healthy throughout. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Node value text sat at inconsistent heights (Solar +16, Grid +20, Vehicle +26 from their circle centres), so the bottom-row Grid (y=160) and Vehicle (y=166) values were misaligned. Unify all three at the same offset (Solar 58, Grid 156, Vehicle 156), lift the Grid emoji off the value, and raise the Vehicle car/battery so the value/plug line up with Grid. Also hide the battery bar when no car is connected (was showing an empty battery beside "Plug in"). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Users running both MQTT discovery AND the HACS Integration get duplicate entities (each is a separate entity source). Add a "HA MQTT Discovery" on/off setting (Config -> Advanced, default ON for back-compat). When OFF, the discovery state machine publishes EMPTY retained payloads to every config topic (HA's "delete this entity"), so the gateway's MQTT entities are removed cleanly — pick one entity source. New WBConfig.haDiscoveryEnabled (NVS "ha_disc"), _discoveryClearing path in sendDiscovery()/_tickDiscoveryFromTable() with a kind->component map, /config select, and JSON config backup/restore. The HA Add-on creates no entities, so it's never part of the duplication. READMEs updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "House Power" entity read r_dca.p1 only — so on a 3-phase EM340 it undercounted (missed L2/L3). Fix it to the net total (p1+p2+p3) and rename to "Grid Power". Add per-phase Grid Power L1/L2/L3 (diagnostic) and Meter Total Energy (lifetime kWh, for the HA Energy dashboard) from r_dca. kDiscoveryCount 68 -> 72 (static_assert verified). Requested by a Pulsar Plus + EM340 + P1 Power Boost user. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ns from charge_log Two fixes for the original/Zentri Pulsar (reported on #12): 1. applyMeterCapability() hid the WHOLE energy-flow card when no meter (#129). That now wrongly drops the derived charge-power visual on meter-less chargers. Keep the card; hide only the Solar branch + meter stat cells, so Grid -> Vehicle still shows the charge kW. (New pf-node-solar / pf-guide- solar ids.) 2. The Sessions heatmap was built only from per-session r_log, which the original Pulsar can't serve -> always empty. Add loadFromChargeLog(): probe r_log once on the newest session; if unsupported, fall back to the gateway's own /api/charge_log intervals (wh->en, gwh->gen) so the heatmap/list populate going forward. Avoids the slow 60-sid timeout loop too. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auth-only (no CSRF) so the stateless HA integration / Add-on can set the owner without opening the gateway's own Settings page, matching /api/command. Persists to NVS immediately (no reboot) so the selection survives a power cycle. Registered on both the sync and async web servers; owner validated against wallbox_schedule|integration|addon|none. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wallbox status 4 covers both an active Schedule/Solar override (r_dat.gen != 0) and a plain stopped/idle session (gen == 0). The dashboard now shows "Connected - not charging" for the idle case and reserves "Paused" for a real override, matching the HA integration + Add-on so all surfaces agree. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The charger reported Europe/Tallinn, which wasn't in kTzOptions, so the HA timezone select rejected it (and log-spammed). Added the three Baltic capitals (Tallinn, Riga, Vilnius — all EET) alongside the other populated-region zones. Same class of miss as Amsterdam in #14. The charger stores the IANA name directly, so no POSIX mapping is needed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…point Multi-charger + HA-compatibility fixes (live OTA-tested on a Pulsar MAX): - MQTT topics namespaced per gateway (wallbox/<ha_device_id>/...) so two chargers no longer share wallbox/status — fixes HA entity values cycling between chargers (andypnz). Entities re-point automatically on update. - Default HA Device ID is now MAC-unique (wallbox_pulsar_<mac6>) so two out-of-box gateways get distinct entity IDs. Existing installs keep their saved ID (only a fresh/wiped flash picks up the new default). - gw_fw in /api/status — lets the Add-on / Integration warn when firmware is too old for the fields they read. - wb_copper_normalize scaffold (#20): self-detecting no-op mapping layer for Copper SB / Business firmware (different r_dat/r_dca field names); wired into _pollStatus, ready to fill in once raw dumps arrive. - POST /api/reboot_gateway (#150): auth-only, no-CSRF reboot for the stateless integration / Add-on, on both sync and async servers. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On a marginal BLE link (antenna-less S3 through the charger enclosure) the periodic r_dat/r_sta could drop while an occasional read still got through, leaving the status cache empty for the whole session -> dashboard/HA grid shows "--". _pollStatus/_pollRealtime now retry once on an empty read. Static analysis confirmed the poll and on-demand paths are identical, so this is a runtime (link-timing) failure; root cause pending a serial log, this is cheap insurance. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) - WiFi (#13, peter-mcc): a soft WiFi.reconnect() can't recover a wedged WiFi stack after long uptime (dropped at ~79h, needed a power-cycle). Escalating watchdog in wb_net::tick(): >5 min down -> full WiFi stack restart (off/on + begin); >30 min down -> ESP.restart(). Reset on GOT_IP; healthy links untouched. - MQTT (#20, ManuMaxGit): an unreachable broker (rc=-2) was retried every 5s forever, wasting the loop + overflowing the pub ring. Reconnect gate now backs off exponentially (5s -> 60s), resets to 5s on connect. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of "blank status boxes + integration no data" on MQTT-less gateways (ManuMaxGit, blank MQTT host). The BLE->web cache bridge (webServer.updateCache) and the WS broadcast ran only inside `if (mqtt.isConnected())` in the main loop, so /api/charger served null to the dashboard AND the integration whenever MQTT wasn't connected — even though BLE polling worked fine. - main.cpp: status/realtime/meter/settings drains now run unconditionally; each gates only its own MQTT publish internally. Web cache + WebSocket update regardless of MQTT. - wb_mqtt.cpp: don't attempt MQTT at all when the broker host is blank (stops pointless connect() churn on integration/add-on-only setups). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds WB_BOARD (the PlatformIO env name, e.g. "esp32s3") via version.py and emits it as /api/status.board. The HA integration's new firmware Update entity reads this to fetch the release .bin matching the gateway's build target (future-proofs the multi-target case; the integration falls back to esp32s3 when absent). Single source of truth — no separate board string to maintain. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
last_burst_energy was published with state_class=measurement, which HA rejects for device_class=energy (log spam). It's a per-burst snapshot, not a cumulative total, so it now carries no state_class. Rides beta.14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…#13) The live-session energy read (r_lse — largest periodic BAPI) freed its raw response only after building the sanitized copy, briefly holding three copies of the payload at once. deserializeJson copies into the document's own pool, so the raw String can be freed right after parsing — dropping the peak to two copies and easing heap-fragmentation pressure at long uptime. A rare panic (#13) was traced to this read path on a strong, stable BLE link (-54 dBm), which points at heap pressure rather than a link race. Defensive insurance on the breadcrumb'd path; not a confirmed root cause. Rides beta.14. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two fixes verified on hardware, both rolled into beta.14: - OTA falsely rejected valid uploads as "truncated": the size check compared firmware bytes written against request Content-Length, which for a multipart upload (HA integration + OTA page) includes the boundary + headers (a few hundred bytes not flashed). Longer boundaries tripped the 256-byte tolerance and aborted complete images, forcing USB re-flashes (gambys, ManuMaxGit). Raise the tolerance well clear of multipart framing in both OTA handlers; Update.end()'s image checksum stays the real integrity guard. Confirmed: a 332-byte-framing upload that the old check rejected now flashes cleanly. - Start/Stop now idempotent (#23): a start while already charging, or a stop while already stopped, is skipped. Some chargers (Pulsar Plus USA fw) treat w_cha as a TOGGLE, so a redundant write flipped the state the wrong way (set switch on-while-on -> off). New wallboxBLE.startStopRedundant() gates the sync web, async web, and MQTT handlers. Confirmed on hardware: stop-while-idle skipped, start still dispatches. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Em340 phase mqtt
Follow-up tidy on the merged EM340 per-phase sensors (#22): the group had two comment blocks — the original one (predating the voltage/current additions, so it only mentioned power+energy) and a second column-0 block that broke the array's 4-space indentation. Fold them into one accurate header covering power, mains voltage, house current and lifetime energy, and drop the mis-indented duplicate. Comment-only; no entity or logic change (discovery count stays 76). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ta.15) A failed OTA left BLE paused for the full 5-min pause window (OTA pauses BLE to free resources, but the error path never un-paused it), so a transient flash hiccup looked like a dead gateway with BLE off. The OTA-error path now un-pauses BLE (pause(0)) so it reconnects immediately and a retry works. Also, the generic 500 "Upload failed" now carries the actual flash-layer reason (flash begin failed / write failed / bad magic byte / truncated) via otaRejectReason, which the HA integration already relays in its error — so an OTA failure is diagnosable instead of opaque. Reproduced on the user's gateway (HA update -> 500 "Upload failed" -> BLE stuck off until the pause lapsed). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nsors Release candidate for 3.2.0. Consolidates the OTA-error fixes and adds the per-phase EM340 meter diagnostics (#22) to the notes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
#162: expose the ESP32-S3 internal temperature as chip_temp in /api/status and
as an MQTT diagnostic sensor ("Gateway Temperature"), so heat issues (e.g. a hot
garage — _Mike post #82) are visible instead of guessed. kDiscoveryCount 76->77.
#161: BLE error logs now decode the NimBLE host code (e.g. "err 0x07 ENOTCONN
(link dropped mid-op)") on the secureConnection/encryption/disconnect paths, so
a user's pasted log is self-diagnosing rather than a bare hex to look up.
Built clean on esp32s3.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…all (#166) The open burst lived only in RAM, so a reboot/OTA mid-charge — or an r_dat feed stall that never delivered the cp-drop close-sample — silently dropped the whole charge (intervals are only written on close). Root-caused from the user's log going silent June 28 across a reboot-heavy OTA window. - Persist the open burst to NVS on open + throttled every PERSIST_INTERVAL_S (300s), cleared on close. begin() recovers a persisted open burst by appending what was captured as a completed interval. - wb_charge_log::tick() (called every main loop, self-throttled) auto-closes an open burst with no fresh cp sample for STALE_TIMEOUT_S (600s) — the exact case where the seq-gated feed is frozen and onRealtime never runs. Built clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The HA integration's OTA picks the release asset whose name ends '-<board>.bin' (update.py pick_asset), but the release workflow only attached the generic firmware.bin / bootloader.bin / partitions.bin (for ESP Web Tools) — so with 3+ unnamed .bins pick_asset couldn't disambiguate and HA reported "No firmware asset for board 'esp32s3'". beta.14 worked because it shipped a wallbox-gateway-<tag>-esp32s3.bin; a workflow refactor dropped it. Add a board-named copy of the app firmware (wallbox-gateway-<tag>-esp32s3.bin) to the release alongside the ESP-Web-Tools parts. rc.2 was backfilled manually. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…log (rc.3) Pre-stable code-review fixes: - chip_temp is now published in publishGatewayInfo() (the MQTT gateway payload), not just wb_buildStatusJson() — the "Gateway Temperature" discovery sensor was reading a missing field (stuck at 0). - OTA error path un-pauses BLE only when THIS upload actually took the flash-start pause (new _otaBlePaused gate). Prevents an early admission reject (auth / another-OTA-in-progress / canAcceptOta) — which returns before the pause — from cancelling the pause and, for a concurrent upload, un-pausing BLE mid-flash. - Charge-log boot recovery skips a burst already matching the newest stored interval (usid+start), so a crash between closeBurst()'s two NVS writes can't double-append. Built clean on esp32s3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…race, per-model fixes (rc.4) Charge-log recording was dead since late June — two causes, both fixed and verified on hardware: - Detection now fires on metered en rising, not just cp>0.10 (which missed Eco-Smart solar where cp reads ~0 while energy flows). Burst energy is metered Δen when available, else the cp integral (Zentri, which has no en). - store() frees the old NVS value before writing, so a growing interval ring persists on a near-full partition (was silently failing → ring stuck). - Green Wh now from authoritative r_lse.green_energy, not the r_dat.gen override flag (was costing solar at the grid rate). Stability + per-model correctness: - _parserMutex serialises the NimBLE notify callback vs _sendCommandDirect reset()/move() (the marginal-link response-buffer race → panic). - MQTT resume_schedule Stop gated on isCharging() (a Stop while paused faults the charger, error 114). - den MQTT scale ÷100 (was ÷1000, 10x off vs the integration). - removed bogus MQTT green_energy sensor (was the override flag as kWh). - car_connected uses the Zentri st enum when _isZentri. - Zentri keepalive gated on isPlus() || _isZentri (was reconnect-looping). - web /api/command?action=current clamped 6-32 A. - docs/CHARGER_QUIRKS.md: per-model quirk/diff catalog. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ntrol_mode) /api/status now exposes schedule_paused = (r_lse.control_mode == 1), the real "manual override active" signal (model-agnostic). Falls back to r_dat.gen != 0 only for chargers without r_lse (Zentri/original Pulsar, where gen genuinely is the override flag). On the MAX Pro gen is accumulated green energy, so the old gen!=0 guesses in the dashboard/add-on/integration were wrong (false-positive "paused" banner during solar, never cleared on Resume). Verified the signal on hardware (control_mode 0->1 on manual override, back to 0 on resume). NOTE: not yet flash-verified — the local PlatformIO toolchain is broken (xtensa-esp32s3-elf-g++ missing after a mid-session esptool reinstall); code compiles, flash pending a toolchain repair. Gateway currently runs the prior good build (capture + store + BLE-race fixes all present). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wb_chipTempJson() (shared by /api/status + the MQTT gateway payload) emits the real reading on ESP32-S3/S2/C3/C6 and JSON null on the classic ESP32 (WROOM), whose temperatureRead() returns a fixed/garbage value. MQTT template renders empty on null → HA shows "Gateway Temperature" unavailable instead of a fake 0. No behaviour change on S3. Future-proofs the esp32dev target (#154). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
) The BLE task queued every /api/command passthrough response for MQTT publish regardless of MQTT state, so HACS-integration-only setups (MQTT off / no broker) filled the small pending-pub ring on every poll -> "pending MQTT pub ring full, dropping met=..." forever, with the sustained churn the likely panic trigger. Now gated on wallboxBLE._mqttPubEnabled, which main sets from wallboxMQTT.isConnected() each loop. MQTT off -> ring stays empty; nothing lost (the response still returns via the wake path + caches). Reported by geertvanvaerenbergh (rc.3, S3, Pulsar Plus). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…status payloads
The /api/status and MQTT gateway payloads are hand-built via += concatenation, so
a device-reported string with a stray quote/backslash/control char (partial BLE
read, unusual WiFi SSID) — or chip_temp returning NaN ("nan") — produced invalid
JSON that HA couldn't parse, logging "Invalid state message '' from
wallbox/.../response/gateway" and briefly blanking the gateway sensors. Now
wb_jsonEsc() escapes every device string in both builders and wb_chipTempJson()
emits null on a NaN/inf read. Latent robustness bug (not an rc.4 regression);
reported on the user's gateway.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gateway has been watchdog/panic rebooting roughly once every 8-15h across rc.3, rc.4, beta.14 and current tip alike, and every investigation has died at the same wall: no evidence. The panic handler prints the offending task to UART and the reset throws it away. Arduino's precompiled IDF libs already ship CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH + DATA_FORMAT_ELF and link libespcoredump.a — the firmware has always been able to write a full ELF core. It just had nowhere to put it. This adds the somewhere. The 128K comes out of the app slots (0x1F0000 -> 0x1E0000 each; the current image is 1.44M, leaving ~525K headroom). nvs (0x9000) and nvs2 (0x3F0000) keep their exact offsets so settings and the charge log survive a re-flash. A partition-table change cannot be applied over OTA, so this is inert for existing installs (old table, no coredump, no harm — a build for the 1920K slot still fits their 1984K one). It takes effect on a USB flash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
esp_task_wdt_init()'s second arg is "panic on timeout". extendTo() passes false deliberately — a long OTA flash-erase must not reboot us mid-write. But restore() passed false too, so every extend/restore cycle left the watchdog permanently unable to reset the box: a later wedge would log and hang instead of recovering, and the crash evidence would go with it. restore() now hands back the boot default (CONFIG_ESP_TASK_WDT_PANIC=1). Symmetry: restore() must undo both of extendTo()'s changes, not just the one it is named after. Found while investigating #168; not the cause of it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The coredump partition (c2259e3) means panics now leave a full ELF core in flash, but getting it out meant unplugging the gateway and reading it over serial with esp-coredump. That is impractical for a bug that fires once a day, and flatly impossible for a user reporting a crash on their own box. New wb_coredump module + three routes: GET /api/coredump/summary crashing TASK NAME, PC, exception cause and backtrace PCs, straight from esp_core_dump_get_summary(). Needs no ELF and no cable - this alone answers "which task died", the question #168 has been stuck on all along. GET /api/coredump raw ELF core, streamed chunked off flash (the image is hundreds of KB; building it into a String would blow the heap - see #103). POST /api/coredump/clear erase. POST, not GET, because it destroys evidence: a prefetch or crawler must not be able to wipe a crash we have not read. Two things worth calling out: - The raw dump is gated STRICTER than _checkAuth. _checkAuth waves everything through when auth is disabled, which is fine for /api/status but would hand a full RAM image - MQTT password, auth password, whatever was in memory - to any client on the LAN. /api/config/export already masks its secrets; a memory snapshot cannot be masked, so we refuse with 403 and point at the summary instead. The summary carries no secrets and stays open. - summaryJson() reports "partition":false distinctly from "present":false. A device whose table predates the coredump partition can NEVER capture a crash until a USB re-flash; reporting that identically to "no crash yet" is how you wait a week for evidence that was never coming. Verified on hardware: partition found ("partition":true), raw dump 403s with auth off, summary serves, GET /clear does not erase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ation) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…reeze + releases link (#13) Three things in the /info Firmware section, all from peter-mcc's #13 follow-up. 1. OTA history now shows what an upload INSTALLED, not just what was running when it started. His rc.4 upload displayed as "beta.14" (the FROM), reading as "an OTA about beta.14" rather than "the one that got me to rc.4". The TO version is backfilled on boot: when the new firmware reaches healthy state, recordBoot() walks back to the OTA entry that installed it and stamps WB_VERSION onto it. That's the correct source — an earlier attempt to read the incoming image's app-descriptor version returned the IDF version string ("esp-idf: v4.4.7 ..."), because our git-describe WB_VERSION is a custom macro, not the ESP-IDF app version field. The row now renders "beta.14 -> rc.4" when the two differ; failed/aborted uploads and never-booted images keep no target, so the row reads as before. 2. Fix a SILENT NVS write freeze in the history store (found while verifying #1 on hardware: this box's history was stuck at beta.6 while a dozen later flashes went unrecorded). putString() on a near-full / fragmented NVS returns 0 and leaves the OLD value in place with no error — the same silent-fail class as the charge-log store. Now we remove the key first so the write has a free page, and log a failure instead of losing it quietly. 3. Added a direct link to the GitHub releases page from the Firmware section. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the [3.2.0] section: promotes 3.2.0-rc.4 to stable and documents the post-rc.4 work (coredump diagnostics, OTA-history TO-version + NVS-freeze fix, WDT panic-flag fix, releases link). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
main's only unique commit is the squash merge of PR #15 — an older snapshot of feature/3.2's own history (base 1412196 is a shared ancestor). feature/3.2 is strictly newer for every file, so -s ours keeps feature/3.2's tree intact and records main as merged, clearing the artificial squash-vs-incremental conflicts so the v3.2.0 PR (#27) can merge. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
botts7
added a commit
that referenced
this pull request
Jul 18, 2026
Brings the v3.2.0 release tag into feature/3.2's ancestry so git-describe on the dev branch anchors to v3.2.0 (not the older rc.4), keeping dev-build version strings monotonic — a build off feature/3.2 now reports 'v3.2.0-N-g<hash>' (> 3.2.0) instead of 'v3.2.0-rc.4-N-...' (< 3.2.0), so the HA update entity never mistakes newer dev code for a downgrade target. Content is unchanged (main == the PR #27 merge of this branch); this only records the ancestry. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Promotes 3.2.0-rc.4 → 3.2.0 stable. Soaked clean on two gateways (241h on peter-mcc's box, 20h+ locally), rc.4 confirmed OTA'ing cleanly, and all open reports from the 3.2 cycle resolved.
Highlights since rc.4
/api/coredump/summary(crashing task, PC, backtrace over HTTP, no cable/ELF) and auth-gated raw/api/coredump. Groundwork for #168. Adds a partition, so it activates on a USB re-flash; OTA installs are unaffected.chip_tempemitsnullon NaN (#172).beta.14 → rc.4) and fixes a silent NVS freeze that had stopped history recording on some boxes (Did Reboot Gateway from ESP web page and unit didn't come back up #13).Reports closed this cycle
Full detail in the
[3.2.0]section ofCHANGELOG.md.Known / not blocking
Release steps after merge
v3.2.0on the merge commit → CIreleasejob builds + attachesfirmware.bin/SHA256SUMS.txt.v3.2.0).🤖 Generated with Claude Code