Skip to content

Add Shelly Power Strip (Gen4) support, independent on/off colors, and reliability fixes - #2

Open
radioactive-bbs wants to merge 8 commits into
ishiharas:mainfrom
radioactive-bbs:upstream-pr
Open

Add Shelly Power Strip (Gen4) support, independent on/off colors, and reliability fixes#2
radioactive-bbs wants to merge 8 commits into
ishiharas:mainfrom
radioactive-bbs:upstream-pr

Conversation

@radioactive-bbs

Copy link
Copy Markdown

Thanks for this integration - it's been very useful. This PR adds Shelly Power Strip (Gen4) support on top of it, plus independent on/off color entities and a handful of reliability/security fixes found during a focused audit. Happy to adjust anything to fit your preferred style/scope - split into smaller PRs on request.

New features

  • Shelly Power Strip (Gen4) support. The device picker now recognizes power strips, not just single plugs, and the RPC client auto-detects whether a device speaks PLUGS_UI (Plug S) or POWERSTRIP_UI (Power Strip).
  • Independent on-color / off-color entities. Each LED now gets two light entities - e.g. LED Ring On Color and LED Ring Off Color - matching the device's native switch-mode LED, which already shows a different color depending on the relay's own state. Set red-on/green-off once and the device handles it entirely in firmware, no automation needed, and it keeps working even while Home Assistant is offline.
  • Fully independent toggles. Turning one color entity off only dims its own color slot to 0 brightness; it no longer forces the shared LED mode off and no longer affects the sibling entity. Previously (in the single-entity design) there was no way to configure asymmetric on/off colors from HA at all - every turn_on wrote the same RGB to both slots.
  • Diagnostics support (diagnostics.py, new) - redacts the stored username/password on download.
  • Visible reset failures - the "Reset LED Ring to Default" button now raises a real error in the UI if the reset RPC fails, instead of silently doing nothing.

Fixes (from a focused audit)

  • Device resolution. config_flow.py no longer picks entry_devices[0] blindly. Newer multi-outlet devices register a physical "root" device plus one child device per output, and picking by list position could land on the wrong one (reproduced against real Power Strip Gen4 hardware, where it selected a device with no model info). The root device is now resolved deterministically by identifier shape, with sibling devices checked for a model string as a fallback.
  • Race condition. The on-color/off-color entities for one LED share a single coordinator; a refresh triggered by one entity's write could previously wipe the other entity's still-in-flight optimistic UI state. Each entity now tracks its own pending-write count and only clears its optimistic state once its own writes have settled.
  • Digest auth hardening. realm/nonce/opaque values from the device's WWW-Authenticate challenge are now sanitized before being interpolated into the outgoing Authorization header (defense-in-depth against header-value injection from a compromised device or LAN MITM).
  • Simplified LED-component probing. Real-hardware testing showed a device only ever answers one of PLUGS_UI/POWERSTRIP_UI successfully (the other 404s outright), so get_config() uses first-success-wins instead of probing every component and comparing results.

Also included

  • README rewrite: features, supported hardware table, entities reference, the on/off-color model explained, the Power Strip's shared-color-slot behavior, troubleshooting section, and two real screenshots replacing the static banner.
  • Manifest version bumped to 1.5.0.

Testing

Manually tested against real hardware over the course of this work:

  • Shelly Plug S (existing single-outlet path, unchanged entity naming/unique_ids for backward compatibility)
  • Shelly Power Strip 4 Gen4, firmware 2.0.0 (new POWERSTRIP_UI path, multi-device config-entry resolution, on/off color independence, digest-auth-disabled device)

All commits and more detail are also visible on the fork this PR comes from: https://github.com/radioactive-bbs/shelly_plug_led

- api.py: auto-detect the LED RPC component (PLUGS_UI vs
  POWERSTRIP_UI) so the same client works on single-outlet plugs and
  the 4-outlet Power Strip.
- light.py: create one LED light entity per outlet, discovered from
  the switch:N keys in the device's LED color config, instead of a
  single hardcoded switch:0 entity. Single-outlet devices keep their
  original entity name/unique_id for backward compatibility.
- config_flow.py: broaden the device picker filter to also include
  power strip models, not just plugs.
- button.py: generalize the reset button label since it now resets
  LEDs across multiple outlets on a Power Strip.
- Update translations (en/de) and README for multi-outlet wording,
  and note the firmware limitation that LED mode is shared across all
  outlets while color/brightness stays per-outlet.
- Bump manifest version to 1.2.0.
On a real Shelly Power Strip Gen4, PLUGS_UI.GetConfig also answers (with
only switch:0) alongside POWERSTRIP_UI.GetConfig (with all 4 channels).
The previous 'first success wins' probe locked onto PLUGS_UI, so only a
single LED Ring entity was created instead of one per outlet.

Now all LED_UI_COMPONENTS are probed and the one whose config reports
the most switch:N keys is kept and cached.

Also:
- manifest.json: point documentation/issue_tracker at this fork, credit
  original creator @ishiharas alongside @radioactive-bbs in codeowners.
- README.md: GitHub badge linking to this fork, Credits section for
  the original creator.
Real Power Strip Gen4 firmware confirmed: each outlet's physical LED
independently tracks its own relay state (native switch mode), but the
config only exposes ONE on/off color pair per LED - and the previous
light entity wrote the same RGB to both 'on' and 'off' on every
turn_on, silently clobbering any asymmetric on/off color scheme (e.g.
red-when-on / green-when-off) the moment the entity was touched again.

light.py: split ShellyPlugLedRing by a new color_key ('on'/'off') so
each LED gets two entities, e.g. 'LED Ring' + 'LED Ring Off Color',
each writing only its own color slot. The 'on' entity keeps its
original name/unique_id for backward compatibility; only a new 'Off
Color' entity is added.

README: document the on/off split and that a Power Strip's outlets
currently share one color slot in firmware while still tracking their
own relay state independently - so per-outlet colors work without any
HA automation once set.

Bump manifest version to 1.3.0.
…olor

Was just 'LED Ring' before, which read oddly next to 'LED Ring Off
Color'. Display name only - unique_id/entity_id unchanged, so this
doesn't create new entities or orphan existing ones.
…mode

Previously both entities' is_on read the same device-wide leds.mode, so
they always showed the same state and turning either off ('mode: off')
silently killed the other's color too - the toggle was effectively
meaningless as a per-entity control.

Now each entity's is_on is switch-mode-engaged AND its own slot's
brightness > 0. turn_on engages switch mode and writes its own
rgb/brightness (falling back to full brightness instead of a stale 0
when the slot was previously off). turn_off only dims its own slot to
0 brightness - it no longer touches leds.mode at all, so the sibling
entity is completely unaffected.

Updated README to describe the new independent on/off/off behavior.
… header sanitization, credential redaction

- config_flow.py: stop picking entry_devices[0] blindly. The official
  Shelly integration can register multiple devices per config entry on
  multi-outlet devices (a root device + one child per output); resolve
  the root device explicitly by identifier shape instead of list
  position, and fall back to sibling devices for a model string when
  the root device doesn't carry one itself. Confirmed live this
  session that list-position selection could land on a modelless
  device and behave inconsistently.

- light.py: fix a real race between the on-color/off-color sibling
  entities that share one coordinator. A refresh triggered by one
  entity's write no longer clears the other entity's still-in-flight
  optimistic state - each entity now tracks its own pending-write
  count and only clears its optimistic overrides once its own writes
  have settled.

- button.py: reset button no longer swallows every exception silently.
  Failures are now logged and raised as HomeAssistantError so a failed
  reset is visible in the UI instead of looking like it succeeded.

- api.py: sanitize realm/nonce/opaque values taken from the device's
  WWW-Authenticate challenge before interpolating them into our own
  outgoing Digest Authorization header (defense-in-depth against
  header-value injection from a malicious/compromised device or LAN
  MITM). Also simplified get_config()'s component probing back to
  first-success-wins now that real-world testing confirmed a device
  only ever answers one of PLUGS_UI/POWERSTRIP_UI successfully - the
  'probe all, keep the one with more outlets' comparison never had
  more than one candidate in practice.

- diagnostics.py (new): redact the integration's own stored
  username/password copy from Home Assistant's diagnostics download,
  matching how the official Shelly integration already redacts its
  copy.

Bump manifest version to 1.5.0.
Swaps the old custom_components/shelly_plug_led/brand/banner.png for
two real screenshots showing the On Color / Off Color toggles on the
device's control card, and the Off Color picker set to green.
…am PR

Only self-referential branding changes on top of main - all functional
changes (Power Strip support, on/off color entities, audit fixes,
diagnostics, screenshots) are carried over as-is.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants