Feature/3.2#15
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>
botts7
added a commit
that referenced
this pull request
Jul 18, 2026
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>
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.
No description provided.