Add HomeWizard P1 usermod: live grid power visualization ("Grid Flow" effect)#5719
Add HomeWizard P1 usermod: live grid power visualization ("Grid Flow" effect)#5719stephancar wants to merge 13 commits into
Conversation
Polls a HomeWizard P1 meter (local API v1) and visualizes live grid power as a custom effect: purple pulses for offtake, green for injection, white breathing when neutral. The meter is discovered automatically via mDNS (_hwenergy._tcp) or set manually. HTTP polling is fully asynchronous (AsyncClient); TCP callbacks only buffer data and parsing happens on the main loop task, avoiding the blocking-request watchdog resets of the previous attempt. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deleting the client inside onTimeout runs the destructor, whose close fires onDisconnect, which deleted the client a second time. Only flag the failure in onTimeout; the rx timeout closes the connection and onDisconnect performs the single delete. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Render from a low-pass filtered power value (~1.2 s time constant) so 3 s poll steps ramp instead of jump. Fade the color white -> purple/ green proportionally with power (no hard cut at the deadband edge), let pulses gradually emerge from the neutral breathing, and cross-fade the waiting/active brightness via a smoothed presence factor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r ramp Testing revealed stuttering pulses, no visible green during injection, and a lockup after about a minute of running. - Move all networking (mDNS discovery + HTTP polling) to a dedicated low-priority task pinned to core 0, replacing the AsyncClient state machine. This removes the client deletion races that most plausibly caused the lockup, and discovery no longer pauses the LEDs. - Integrate the pulse position over time instead of deriving it from time * speed; speed changes no longer rescale the phase and teleport the pulses (the stutter). - Saturate the identity color (purple/green) at ~25% of the power range with an ease-out curve so a few hundred watts is clearly colored; magnitude keeps scaling flow speed and brightness to full scale. - Add a Blur slider and a quadratic comet-style trail falloff. - Add p1_simulator.py, a fake local API v1 that cycles injection/ neutral/offtake every 10 s for testing without a real meter; verified rendered colors against it via /json/live. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Investigation of a reported 'LEDs off until power cycle' incident ruled out brownout (full-white stress test), meter rate limiting (90 fresh connections in 3 min, zero failures) and crashes under state changes (12 min of simulated injection/neutral/offtake cycling, stable heap). The remaining explanation: fetch failures put the effect into its no-data state, which was so dim it read as 'off', and discovery could fail to recover because the last known meter address was discarded. - Never forget the discovered meter IP; background re-discovery only replaces it on success, so a broken mDNS state no longer strands the usermod and polling resumes as soon as the meter answers. - Raise the no-data breathing level so a data outage is visibly 'breathing dimly' instead of looking switched off. - Report seconds since the last successful fetch on the info page. - Guard the smoothing filters against NaN poisoning. - Halve the flow speed range (2-30 px/s): ambient, not alarming. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
State the ESP32 requirement explicitly, document the Blur slider, and add a power-supply note (white is the maximum-current state; configure ABL with headroom for the ESP32). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The networking runs on a FreeRTOS task, so the usermod is ESP32-only; declaring the platform lets ESP8266 CI builds skip it cleanly. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Root cause of the recurring switch-off-at-white incidents, captured live with the firmware healthy and rendering: master power was toggled off (and the segment color randomized to green) with no HTTP or UDP trace - phantom presses of the default GPIO0 button from electrical noise during the white-transition current step. Self-latching because an off strip draws no current, so no phantom press turns it back on. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds an ESP32-only HomeWizard P1 usermod that discovers or uses a configured host, polls the local API on a dedicated task, smooths power readings, and renders a “Grid Flow” effect. It also adds build metadata, a simulator, and user-facing documentation. ChangesHomeWizard P1 Usermod
Estimated code review effort: 4 (Complex) | ~60 minutes Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@usermods/homewizard_p1/homewizard_p1.cpp`:
- Line 322: The early return in the HomeWizard usermod update flow leaves the
Grid Flow effect state active when the usermod is disabled. Update the logic
around the enabled check in homewizard_p1.cpp (the update/smoothing path in the
HomeWizard usermod class) so disabling the usermod also clears or resets the
registered Grid Flow state before returning, preventing stale import/export
visuals from persisting.
- Line 224: The host/port parsing in homewizard_p1.cpp currently uses atoi() and
assigns directly to a uint16_t, which lets malformed, negative, or oversized
ports wrap to unintended values. Update the host:port parsing logic around the
colon handling to validate the port string explicitly in the same parsing flow,
reject non-numeric or out-of-range values, and only accept ports within the
valid 1–65535 range before storing them in port. Use the existing parsing block
near the colon check to enforce this validation and fail the config instead of
silently normalizing bad input.
- Around line 176-188: The mutex/task creation paths in homewizard_p1 need
failure handling: `ensureMutex()` currently assumes `xSemaphoreCreateMutex()`
succeeds, and `setup()` appears to ignore a failed `xTaskCreatePinnedToCore()`
result. Update `ensureMutex()` and `getEffectiveHost()` so they detect a null
`p1Mutex` and skip any `xSemaphoreTake()`/`xSemaphoreGive()` usage when creation
fails, and add a check around the `xTaskCreatePinnedToCore()` call in `setup()`
to surface the error instead of leaving the usermod idle. Use the existing
`ensureMutex`, `getEffectiveHost`, and `setup` symbols to locate the affected
code.
- Around line 29-30: The shared P1 scalar fields are crossing the
fetch-task/main-thread boundary without a real synchronization mechanism, so
protect `p1PowerW`, `p1LastGoodMs`, `taskStatus`, `taskHttpCode`,
`updateIntervalMs`, `autoDiscover`, and `cfgEnabled` using the existing
`p1Mutex` or convert them to `std::atomic` and read/write them consistently.
Update the relevant access points in the HomeWizard P1 task and its readers so
these values are accessed under the same synchronization scheme as the host
strings, rather than relying on `volatile`.
- Around line 242-255: Parse the HomeWizard response as a stream in the logic
around http.getString(), deserializeJson, and p1PowerW so the filter works
without buffering the whole body, and replace the containsKey check with a
numeric type check on active_power_w. Update the flow in the same block to
deserialize directly from http.getStream() and only assign p1PowerW when
doc["active_power_w"].is<float>() is true; otherwise set ST_JSON_ERR and return.
- Around line 419-420: The wattage normalization in the configuration path can
overflow when `p1DeadbandW` is at `INT_MAX`, so update the logic around
`p1DeadbandW` and `p1FullScaleW` to avoid evaluating `p1DeadbandW + 1` in the
same integer type. Use a wider intermediate or clamp the upper bound first, then
enforce that `p1FullScaleW` remains strictly greater than `p1DeadbandW` inside
the `homewizard_p1` normalization code.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7f1ca196-802d-4942-a4d2-62dfaae98475
📒 Files selected for processing (4)
usermods/homewizard_p1/homewizard_p1.cppusermods/homewizard_p1/library.jsonusermods/homewizard_p1/p1_simulator.pyusermods/homewizard_p1/readme.md
Second failure mode captured live: hard chip freeze (serial silent, no panic) exactly as a ~5 kW load switched off, i.e. a mains transient. Distinct from the phantom-button mode and inherent to the use case, since a grid-power light lives near big switching loads. Document the hardware mitigations and the WLED_WATCHDOG_TIMEOUT build flag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The switch-off-at-transition incidents were caused by a leftover Home Assistant automation reacting to the same P1 meter, not by GPIO0 noise: the signature (on=false, brightness and color set) recurred with the button removed, and the HA WLED integration polling was visible in the debug serial all along as Not-Found /presets.json calls. Replace the phantom-button advice with guidance on identifying external controllers, which is the far more likely cause for this usermod audience. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Use std::atomic for all scalars crossing the fetch-task boundary instead of volatile. - Handle mutex and task creation failures: ensureMutex() reports failure, all xSemaphoreTake() paths are guarded against a null mutex, and a failed xTaskCreatePinnedToCore() is surfaced on the info page instead of leaving the usermod silently idle. - Validate host:port with strtol and reject malformed or out-of-range ports instead of wrapping them. - Parse the HTTP response from the stream (no full-body buffering) and require active_power_w to be numeric, rejecting null. - Reset the smoothed visuals when the usermod is disabled so the still-registered effect does not show stale import/export state. - Clamp deadbandW before deriving the full-scale floor to avoid signed overflow at extreme config values. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
All coderabbit findings addressed in 2c6efd0:
Compiled for 🤖 Generated with Claude Code |
| // Returns false when no host is known yet. | ||
| bool getEffectiveHost(char *buf, size_t len) { | ||
| bool have = false; | ||
| if (xSemaphoreTake(p1Mutex, portMAX_DELAY) == pdTRUE) { |
There was a problem hiding this comment.
portMAX_DELAY can hang forever. Better to use a realistic shorter dalay, and some code to handle mutex aquisition failure.
| strip.addEffect(255, &mode_grid_flow, _data_FX_MODE_GRID_FLOW); | ||
| #ifdef ARDUINO_ARCH_ESP32 | ||
| ensureMutex(); | ||
| if (xSemaphoreTake(p1Mutex, portMAX_DELAY) == pdTRUE) { |
There was a problem hiding this comment.
portMAX_DELAY => see my previous comments
| } | ||
| cfgEnabled = enabled; | ||
| // Low priority, pinned to core 0 (WLED's LED loop runs on core 1) | ||
| xTaskCreatePinnedToCore(fetchTask, "P1Fetch", 10240, this, 1, &fetchTaskHandle, 0); |
There was a problem hiding this comment.
if this is not timing-critical (=needs to respond to an event within 1-2milliseconds), you can better use xTaskCreate (without pin to core).
Also check if 10240 words of stack are really needed, it seems excessive.
@stephancar I would prefer if you take the time to respond in person, instead of letting your AI talk for you. |
| } | ||
| cfgEnabled = enabled; | ||
| // Low priority, pinned to core 0 (WLED's LED loop runs on core 1) | ||
| if (xTaskCreatePinnedToCore(fetchTask, "P1Fetch", 10240, this, 1, &fetchTaskHandle, 0) != pdPASS) { |
There was a problem hiding this comment.
No need to pin this task to core #0. Simply use xTaskCreate() instead.
| if (found < 0 && n > 0) found = 0; // no explicit P1: fall back to the first device | ||
| if (found >= 0) { | ||
| String ip = MDNS.IP(found).toString(); | ||
| if (p1Mutex != nullptr && xSemaphoreTake(p1Mutex, portMAX_DELAY) == pdTRUE) { |
There was a problem hiding this comment.
Don't use portMAX_DELAY. Better to find a reasonable max delay, and handle mutex acquisition failure gracefully.
| if (um->taskStatus != ST_OK && um->autoDiscover && | ||
| millis() - p1LastGoodMs > 20000 && millis() - lastDiscoveryMs > 30000) { | ||
| bool manual = false; | ||
| if (um->p1Mutex != nullptr && xSemaphoreTake(um->p1Mutex, portMAX_DELAY) == pdTRUE) { |
There was a problem hiding this comment.
portMAX_DELAY is dangerous - see my previous comment.
|
|
||
| #ifdef ARDUINO_ARCH_ESP32 | ||
| ensureMutex(); | ||
| if (xSemaphoreTake(p1Mutex, portMAX_DELAY) == pdTRUE) { |
| - **Blur**: softens the pulses for a smoother look. | ||
|
|
||
| ## Troubleshooting | ||
|
|
There was a problem hiding this comment.
I've lost days to problems that turned out to be other things (a leftover home assistant automation that controlled the wled controller as well as an undersized power supply)
| strip turns white: | ||
|
|
||
| - **Device browns out / reboots at the white transition**: set a realistic | ||
| PSU limit in Config → LED Preferences (ABL), leaving ~250 mA headroom for |
There was a problem hiding this comment.
@stephancar is brownout directly caused by your usermod?
There was a problem hiding this comment.
no this turned out to be the undersized PSU and/or the home assistant automation that I forgot about (it turned wled on or of based on whether I had injection or not)
| char manualHost[64] = ""; // copy of 'host' for the task | ||
| char discoveredHost[48] = ""; // written by the task after mDNS discovery | ||
|
|
||
| #ifdef ARDUINO_ARCH_ESP32 |
There was a problem hiding this comment.
This ifdef can be omitted - the complete usermod will not compile on 8266 any way.
| // Returns false when no host is known yet. | ||
| bool getEffectiveHost(char *buf, size_t len) { | ||
| bool have = false; | ||
| if (p1Mutex != nullptr && xSemaphoreTake(p1Mutex, portMAX_DELAY) == pdTRUE) { |
| @@ -0,0 +1,473 @@ | |||
| #include "wled.h" | |||
|
|
|||
There was a problem hiding this comment.
Suggestion: add a compile-time blocker for 8266 here. Then you can remove all remaining #ifdef ARDUINO_ARCH_ESP32 checks.
#ifndef ARDUINO_ARCH_ESP32
#error "this usermod only supports esp32 boards"
#endifThere was a problem hiding this comment.
None of them should be necessary at all. The supported platform list is already set in library.json.
|
@coderabbitai full review |
✅ Action performedFull review finished. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@usermods/homewizard_p1/homewizard_p1.cpp`:
- Around line 456-457: Guard the mutex acquisition in readFromConfig() because
ensureMutex() may fail and leave p1Mutex null before xSemaphoreTake() is called.
Update the readFromConfig() path to check the result of ensureMutex() (or
combine it with the mutex take like the other call sites) and return early if
the mutex could not be created, so xSemaphoreTake(p1Mutex, ...) is never reached
with a null handle.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ab7709e6-53af-4439-bf3b-2dae27abb9d9
📒 Files selected for processing (4)
usermods/homewizard_p1/homewizard_p1.cppusermods/homewizard_p1/library.jsonusermods/homewizard_p1/p1_simulator.pyusermods/homewizard_p1/readme.md
- Handle init failures once in setup(): if mutex or task creation fails the usermod disables itself (reason shown on the info page) and the effect falls back to the segment color; this removes the scattered null-handle checks (softhack007's suggestion). - Replace all portMAX_DELAY mutex waits with a 50 ms bounded wait; on timeout the caller skips the cycle and retries on the next one. - Use xTaskCreate instead of xTaskCreatePinnedToCore (no need to pin). - Right-size the task stack: measured high-water mark on real hardware is ~2.3 KB used (mDNS + HTTP + JSON), so 10240 -> 4096, leaving ~1.8 KB margin (verified on-device: 1852 B headroom). Debug builds report the headroom on the info page. - Remove all ARDUINO_ARCH_ESP32 guards: library.json already restricts the usermod to espressif32 (willmmiles). - Trim the readme troubleshooting to the usermod-specific pitfall (external meter-triggered automations); drop the generic PSU and EMI hardware advice (softhack007's readme questions). - Guard the config-save host sync against the pre-setup case where the mutex does not exist yet (coderabbit). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fair point, sorry! This is my first PR and Im learning about AI tooling for the code (as marked in the sources). From here on the replies are my own. Thanks for taking the time to review my code, the suggestions are very helpfull! |
What it does
New usermod
homewizard_p1(ESP32 only) that polls a HomeWizard P1 meter (a popular WiFi P1 dongle for European smart meters) on the local network and visualizes the home's live grid power through a new "Grid Flow" effect:The meter is discovered automatically via mDNS (
_hwenergy._tcp), or a fixed host can be configured. Deadband, full-scale wattage, and poll interval are configurable in Usermod Settings; the effect has Speed / Pulses / Trail / Blur sliders.How it works
/api/v1/data) runs on a dedicated low-priority FreeRTOS task pinned to core 0 (same pattern as the audioreactive usermod's processing task), so blocking calls can never stall the LED pipeline. The task publishes 32-bit values (atomic on ESP32); host strings are exchanged under a mutex.Testing
p1_simulator.py, a small fake local-API server that cycles injection/neutral/offtake every 10 s, so the usermod can be developed and tested without a real meter. Rendered output was verified against it via/json/live.esp32dev;library.jsonrestricts the usermod toespressif32(it needs a FreeRTOS task).Notes
usermods/homewizard_p1/; noplatformio.inichanges.// AI:comments per AGENTS.md; the code was written with AI assistance, reviewed and hardware-tested by the contributor.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
host:portoverride.active_power_w, including import/export direction, neutral deadband breathing, and a graceful neutral “no data” state when updates stop.Documentation