Skip to content

Conversation

@dan-danache
Copy link
Contributor

@dan-danache dan-danache commented Oct 9, 2025

This PR introduces support for configuring Wi-Fi settings directly over Zigbee for all Shelly Gen4 devices. It leverages the shellyWiFiSetupCluster, enabling remote network setup through Zigbee2MQTT.

Supported Wi-Fi Parameters:

  • Enable/disable Wi-Fi
  • SSID and password
  • Static IP configuration (IP, netmask, gateway, DNS)

Full Zigbee cluster specification: https://shelly-api-docs.shelly.cloud/gen2/Integrations/Zigbee/WiFiSetupCluster/

This is a mix between #9468 and #9927.

I believe diagnostic data should be exposed as standalone state entries (exposes) to enable seamless integration with Home Assistant automations (for example, triggering actions when Wi-Fi is enabled or disabled on a Shelly device). In contrast, Wi-Fi setup parameters are currently exposed as a single composite entry.

image image

View from Home Assistant:

image

How it works

Refresh Action (/get or UI "refresh" icon)
When the user triggers a refresh for any Wi-Fi attribute, the converter performs the following steps:

  1. Resets attributes by writing 0 to the Action (0x02) attribute, syncing all values with the current device configuration.
  2. Reads all Wi-Fi attributes via two Zigbee attribute read operations, updating the internal state and UI accordingly.

Apply Action (/set wifi_config or UI "Apply" button)
When the user applies a new configuration, the converter executes:

  1. Writes all editable Wi-Fi attributes to the Zigbee cluster via two Zigbee attribute write operations
  2. Applies the configuration by writing 1 to the Action (0x02) attribute, committing the current Zigbee attribute values to the device.

Note: The Wi-Fi password must be provided each time the "Apply" action is used, as it is not stored in the device state.


I tested this code on a Mini 1 Gen4 device with firmware 1.7.1, using both DHCP and static configurations, as an external converter in my production environment. For this pull request, I made minimal adjustments to ensure compatibility, primarily updating import paths and TypeScript types where necessary.

Have fun!

@dan-danache dan-danache changed the title Add support for Shelly Wi-Fi Setup cluster Feature: Wi-Fi Configuration via Zigbee for Shelly Gen4 Devices Oct 9, 2025
@Koenkk Koenkk merged commit 6312107 into Koenkk:master Oct 11, 2025
3 checks passed
@Koenkk
Copy link
Owner

Koenkk commented Oct 11, 2025

Thanks!

@thmang82
Copy link

thmang82 commented Oct 11, 2025

Just played around with the this patch. Looking forward to it, as it promisises the only way of performing firmware updates on the Gen4 shellys without physical access to the device (because there is no MQTT based OTA available).

The initial connect to my WiFi via the Zigbee2Mqtt UI worked. Shelly got IP and Web-UI was reachable.
Afterwards, I disabled Wifi via Zigbee2Mqtt UI, that also worked.
But after that, re-enabling WiFi failed. I tried multiple times, the shelly is just not connecting again to WiFi.
Tried first by just setting WiFi enbled (thought it might remember the password). Afterwards also added the password again. Both ways did not work. Restarting Zigbee2Mqtt did not help either.

@dan-danache ever seen something like this while developing & testing the code?

@dan-danache
Copy link
Contributor Author

I just retested this on a Mini 1 Gen4 running firmware 1.7.1, and I was able to enable and disable Wi-Fi multiple times without issues.

Both ways did not work.

After clicking "Apply", try using the refresh icon next to the "Wi-Fi status" field in the UI. What status does it show?

  • If it says disconnected, it likely means the UI didn’t register any changes and skipped executing the "Apply" command. In that case: click refresh icon, then re-enter the SSID and password, and click "Apply".

  • If it says connecting, the command was executed, but the credentials (SSID/password) might be incorrect.

Let me know what status you see, that’ll help narrow it down.

@thmang82
Copy link

thmang82 commented Oct 12, 2025

I tested it with a Shelly 2PM Gen4. Updated the SW to 1.7.1 the one time it connected to wifi.

I can see the API calls in the debug log of the browser after hitting "apply". Something like this (removed ssid/pw):

index-Cg_dwqpD.js:634 Calling API (0 | 0): {"topic":"0x7c2c67fffe7bxxxx/set","payload":{"wifi_config":{"enabled":true,"ssid":"MYSSID","password":"MyPASSWORD"}}}

It does not seem to be an issue with the UI.

I also hit the "refresh" after wifi status, IP, ... after each apply (recognized at the one successful execution that it's needed for getting the current state).

Wifi state says disconnected most times.
Wifi status stays in "enabled: false" in most cases, with only "enabled" entry visible in MQTT messages.

I also gave static IP a try. In that case, it added the entries to the status, but the gateway address was missing the last number of the ip after fetching the current state by "referesh" button. Something like "192.168.6." was in the state.

Maybe an issue with the 2PM firmware ???

PS: That's the state in MQTT where it's stuck:

{
  "dhcp_enabled": true,
  "ip_address": "",
  "last_seen": "2025-10-12T20:25:25.756Z",
  "linkquality": 220,
  "position": 50,
  "state": "OPEN",
  "tilt": 50,
  "wifi_config": {
    "enabled": false
  },
  "wifi_status": "disconnected"
}

PS2: removing power from the shelly (reboot) does not help

@dan-danache
Copy link
Contributor Author

I retested using my other Gen4 device (1PM Mini) and everything worked as expected (enable/disable, DHCP/static) no issues at all. It's a bit frustrating that I can't reproduce your issue 😅

To help dig deeper, I’ve created an external converter with very verbose logging. If you’d like to try it out:

  1. Add the external converter to your Zigbee2MQTT instance using the new (Windfront) UI:
    1. Navigate to Settings -> Dev console -> External Converters
    2. Set Select converter to edit = N/A - Create new converter
    3. Set Name = Shelly.js
    4. Set Code = Paste this code
    5. Click Save -> you should see a “converter/save : OK” notification if successful
  2. Open a new browser tab and go to Logs -> keep this tab open
  3. On your Shelly 2PM Gen4 device, click the blue Interview [i] button -> this should trigger the use of the external converter (you’ll see Supported: external under the device name)
  4. Confirm Zigbee connectivity: toggle the device state in the UI and listen for the relay clicks
  5. Test the Wi-Fi config options (refresh/enable/disable, DHCP/static) while monitoring the Logs tab
  6. Share some of the log output here; feel free to obfuscate sensitive info (e.g., replace SSID/password characters with x, but keep the string lengths intact -> frame size might be relevant)

Thanks again for testing and putting in the effort; really appreciate you helping track this down!

@dan-danache dan-danache deleted the add-shelly-wifi-config branch October 13, 2025 18:59
@thmang82
Copy link

thmang82 commented Oct 14, 2025

Not an expert for the external converters unfortunately, this is the error i get:

converter/save: Shelly.js contains invalid code: Cannot find package 'zigbee-herdsman' imported from /data/zigbee2mqtt/data/external_converters/.tmp-ed42d4f2-Shelly-afa43742-1fc6-476b-a3a5-34e08d3b08ce.js (ay5h2-10)

I am running Zigbee2MQTT version [2.6.3] commit: 6f6bc4bb

@thmang82
Copy link

thmang82 commented Oct 14, 2025

That seemed to be an issue with 2.6.3. version. Version 2.6.2 running in docker was able to load the Shelly.js

I think i found the issue! It's related to the length of the password!

Take aways

  • Passwords with 11 letters or less are successfully send to the 2PM Gen4.
  • Passwords with 12 letters or more lead to a write error (sometimes it also looked like the data was written ok, but not applied by the shelly).
  • Writing SSID with 12 letters works on Zigbee level, but is reported back with 9 letters
  • The results of read commands are not updated in the UI (making it much more difficult recognizing any issues)

Question:

  • Are the password and SSID length issues a problem of the shelly firmware?
  • Or is this a bug somewhere in the device converter code?
  • Given that the only time i was able to connect was on the firmware it shipped with, Shelly might broke long passwords with 1.7.1 firmware (that i installed when it worked). Maybe @teodor-hristov can help here? (he seems to work for Shelly)

Here is my analysis (passwords and SSIDs obfuscated, but length remained as used):

Test with only enabled: true, no ssid, no password

[14.10.2025, 20:43:18] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"","ssid":""}
[14.10.2025, 20:43:18] z2m: 1 - write #1 done: {"enabled":true,"ssid":"","password":""}
[14.10.2025, 20:43:18] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 20:43:18] z2m: 3 - write #3 done: {"actionCode":1}

[14.10.2025, 20:43:29] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:43:29] z2m: >>> refresh()
[14.10.2025, 20:43:29] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:43:29] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":false},"wifi_status":"disconnected","ip_address":"","dhcp_enabled":true}
[14.10.2025, 20:43:29] z2m: 2 - read #1 done
[14.10.2025, 20:43:29] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 20:43:29] z2m: 3 - read #2 done

-> Works

Test with only enabled:true and ssid, no password

[14.10.2025, 20:43:54] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"","ssid":"tmaaa_ext"}
[14.10.2025, 20:43:55] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":""}
[14.10.2025, 20:43:55] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 20:43:55] z2m: 3 - write #3 done: {"actionCode":1}
[14.10.2025, 20:44:10] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:44:10] z2m: >>> refresh()
[14.10.2025, 20:44:10] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:44:11] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true,"ssid":"tmaaa_ext"},"wifi_status":"connecting","ip_address":"0.0.0.0","dhcp_enabled":true}
[14.10.2025, 20:44:11] z2m: 2 - read #1 done
[14.10.2025, 20:44:14] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 20:44:14] z2m: 3 - read #2 done

-> Works too

Test with enabled, SSID and password:

[14.10.2025, 20:45:46] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa2221Mggggg","ssid":"tmaaa_ext"}
[14.10.2025, 20:45:46] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaa2221Mggggg"}
[14.10.2025, 20:45:51] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed (Delivery failed for '15797'.)'
[14.10.2025, 20:45:51] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:45:54] z2m: >>> refresh()
[14.10.2025, 20:45:54] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:45:59] z2m: Publish 'get' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.read(["status","ip","enabled","dhcp","ssid"], {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed (Delivery failed for '15797'.)'

-> failed

Started shortening the password one letter by one letter

-> failed again and again, it gets interesting at:

[14.10.2025, 20:54:37] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa2221","ssid":"tmaaa_ext"}
[14.10.2025, 20:54:37] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaa2221"}
[14.10.2025, 20:54:42] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed (Delivery failed for '15797'.)'
[14.10.2025, 20:54:46] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa2221","ssid":"tmaaa_ext"}
[14.10.2025, 20:54:47] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaa2221"}
[14.10.2025, 20:54:51] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed (Delivery failed for '15797'.)'
[14.10.2025, 20:54:58] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa2221","ssid":"tmaaa_ext"}
[14.10.2025, 20:54:58] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaa2221"}
[14.10.2025, 20:55:03] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed (Delivery failed for '15797'.)'

-> Fails reliably with direkt error, no timeout any more

One letter less (11 letters), and the password is taken fine

[14.10.2025, 20:55:11] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa222","ssid":"tmaaa_ext"}
[14.10.2025, 20:55:11] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaa222"}
[14.10.2025, 20:55:11] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 20:55:11] z2m: 3 - write #3 done: {"actionCode":1}
[14.10.2025, 20:55:16] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:55:16] z2m: >>> refresh()
[14.10.2025, 20:55:16] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:55:16] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true,"ssid":"tmaaa_ext"},"wifi_status":"disconnected","ip_address":"","dhcp_enabled":true}
[14.10.2025, 20:55:16] z2m: 2 - read #1 done
[14.10.2025, 20:55:16] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 20:55:16] z2m: 3 - read #2 done

In-between disabling wifi works by the way

[14.10.2025, 20:58:26] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":false,"password":"PiiiZaaa222","ssid":"tmaaa_ext"}
[14.10.2025, 20:58:27] z2m: 1 - write #1 done: {"enabled":false,"ssid":"tmaaa_ext","password":"PiiiZaaa222"}
[14.10.2025, 20:58:29] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 20:58:29] z2m: 3 - write #3 done: {"actionCode":1}
[14.10.2025, 20:58:33] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:58:33] z2m: >>> refresh()
[14.10.2025, 20:58:33] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:58:33] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":false,"ssid":"tmaaa_ext"},"wifi_status":"connecting","ip_address":"0.0.0.0","dhcp_enabled":true}
[14.10.2025, 20:58:33] z2m: 2 - read #1 done
[14.10.2025, 20:58:33] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 20:58:33] z2m: 3 - read #2 done

Verified that an 11 letter password works by changing the password

[14.10.2025, 20:58:50] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"YyyyXxxx222","ssid":"tmaaa_ext"}
[14.10.2025, 20:58:50] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"YyyyXxxx222"}
[14.10.2025, 20:58:50] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 20:58:50] z2m: 3 - write #3 done: {"actionCode":1}
[14.10.2025, 20:58:55] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:58:59] z2m: >>> refresh()
[14.10.2025, 20:58:59] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:58:59] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true,"ssid":"tmaaa_ext"},"wifi_status":"connecting","ip_address":"0.0.0.0","dhcp_enabled":true}
[14.10.2025, 20:58:59] z2m: 2 - read #1 done
[14.10.2025, 20:58:59] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 20:58:59] z2m: 3 - read #2 done

Verified that Password with 12 letters reliably brings a communication error again:

[14.10.2025, 21:01:53] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaaPPPP","ssid":"tmaaa_ext"}
[14.10.2025, 21:01:53] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaaPPPP"}
[14.10.2025, 21:02:04] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed ({"target":15797,"apsFrame":{"profileId":49153,"clusterId":64514,"sourceEndpoint":1,"destinationEndpoint":239,"options":4416,"groupId":0,"sequence":130},"zclSequence":241,"commandIdentifier":4} timed out after 10000ms)'
[14.10.2025, 21:02:54] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaaPPPP","ssid":"tmaaa_ext"}
[14.10.2025, 21:02:54] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaaPPPP"}
[14.10.2025, 21:03:04] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed ({"target":15797,"apsFrame":{"profileId":49153,"clusterId":64514,"sourceEndpoint":1,"destinationEndpoint":239,"options":4416,"groupId":0,"sequence":134},"zclSequence":243,"commandIdentifier":4} timed out after 10000ms)'

-> more than 11 letters reliably breaks the write!

Out of curiosity, started also testing how the length of the network name behave

[14.10.2025, 21:07:25] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaaPP","ssid":"PiiiZaaaPPPP"}
[14.10.2025, 21:07:26] z2m: 1 - write #1 done: {"enabled":true,"ssid":"PiiiZaaaPPPP","password":"PiiiZaaaPP"}
[14.10.2025, 21:07:26] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 21:07:26] z2m: 3 - write #3 done: {"actionCode":1}
[14.10.2025, 21:07:41] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 21:07:42] z2m: >>> refresh()
[14.10.2025, 21:07:42] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 21:07:42] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true,"ssid":"PiiiZaaaP"},"wifi_status":"connecting","ip_address":"0.0.0.0","dhcp_enabled":true}
[14.10.2025, 21:07:42] z2m: 2 - read #1 done
[14.10.2025, 21:07:42] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 21:07:42] z2m: 3 - read #2 done
[14.10.2025, 21:07:45] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true,"ssid":"PiiiZaaaP"},"wifi_status":"connecting","ip_address":"0.0.0.0","dhcp_enabled":true}

-> ssid "PiiiZaaaPPPP" got shorted to "PiiiZaaaP" (9 letters)
-> Might also be a problem for some people

@teodor-hristov
Copy link

That seemed to be an issue with 2.6.3. version. Version 2.6.2 running in docker was able to load the Shelly.js

I think i found the issue! It's related to the length of the password!

Take aways

  • Passwords with 11 letters or less are successfully send to the 2PM Gen4.
  • Passwords with 12 letters or more lead to a write error (sometimes it also looked like the data was written ok, but not applied by the shelly).
  • Writing SSID with 12 letters works on Zigbee level, but is reported back with 9 letters
  • The results of read commands are not updated in the UI (making it much more difficult recognizing any issues)

Question:

  • Are the password and SSID length issues a problem of the shelly firmware?
  • Or is this a bug somewhere in the device converter code?
  • Given that the only time i was able to connect was on the firmware it shipped with, Shelly might broke long passwords with 1.7.1 firmware (that i installed when it worked). Maybe @teodor-hristov can help here? (he seems to work for Shelly)

Here is my analysis (passwords and SSIDs obfuscated, but length remained as used):

Test with only enabled: true, no ssid, no password

[14.10.2025, 20:43:18] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"","ssid":""}
[14.10.2025, 20:43:18] z2m: 1 - write #1 done: {"enabled":true,"ssid":"","password":""}
[14.10.2025, 20:43:18] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 20:43:18] z2m: 3 - write #3 done: {"actionCode":1}

[14.10.2025, 20:43:29] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:43:29] z2m: >>> refresh()
[14.10.2025, 20:43:29] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:43:29] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":false},"wifi_status":"disconnected","ip_address":"","dhcp_enabled":true}
[14.10.2025, 20:43:29] z2m: 2 - read #1 done
[14.10.2025, 20:43:29] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 20:43:29] z2m: 3 - read #2 done

-> Works

Test with only enabled:true and ssid, no password

[14.10.2025, 20:43:54] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"","ssid":"tmaaa_ext"}
[14.10.2025, 20:43:55] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":""}
[14.10.2025, 20:43:55] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 20:43:55] z2m: 3 - write #3 done: {"actionCode":1}
[14.10.2025, 20:44:10] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:44:10] z2m: >>> refresh()
[14.10.2025, 20:44:10] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:44:11] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true,"ssid":"tmaaa_ext"},"wifi_status":"connecting","ip_address":"0.0.0.0","dhcp_enabled":true}
[14.10.2025, 20:44:11] z2m: 2 - read #1 done
[14.10.2025, 20:44:14] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 20:44:14] z2m: 3 - read #2 done

-> Works too

Test with enabled, SSID and password:

[14.10.2025, 20:45:46] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa2221Mggggg","ssid":"tmaaa_ext"}
[14.10.2025, 20:45:46] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaa2221Mggggg"}
[14.10.2025, 20:45:51] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed (Delivery failed for '15797'.)'
[14.10.2025, 20:45:51] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:45:54] z2m: >>> refresh()
[14.10.2025, 20:45:54] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:45:59] z2m: Publish 'get' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.read(["status","ip","enabled","dhcp","ssid"], {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed (Delivery failed for '15797'.)'

-> failed

Started shortening the password one letter by one letter

-> failed again and again, it gets interesting at:

[14.10.2025, 20:54:37] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa2221","ssid":"tmaaa_ext"}
[14.10.2025, 20:54:37] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaa2221"}
[14.10.2025, 20:54:42] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed (Delivery failed for '15797'.)'
[14.10.2025, 20:54:46] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa2221","ssid":"tmaaa_ext"}
[14.10.2025, 20:54:47] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaa2221"}
[14.10.2025, 20:54:51] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed (Delivery failed for '15797'.)'
[14.10.2025, 20:54:58] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa2221","ssid":"tmaaa_ext"}
[14.10.2025, 20:54:58] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaa2221"}
[14.10.2025, 20:55:03] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed (Delivery failed for '15797'.)'

-> Fails reliably with direkt error, no timeout any more

One letter less (11 letters), and the password is taken fine

[14.10.2025, 20:55:11] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa222","ssid":"tmaaa_ext"}
[14.10.2025, 20:55:11] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaa222"}
[14.10.2025, 20:55:11] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 20:55:11] z2m: 3 - write #3 done: {"actionCode":1}
[14.10.2025, 20:55:16] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:55:16] z2m: >>> refresh()
[14.10.2025, 20:55:16] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:55:16] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true,"ssid":"tmaaa_ext"},"wifi_status":"disconnected","ip_address":"","dhcp_enabled":true}
[14.10.2025, 20:55:16] z2m: 2 - read #1 done
[14.10.2025, 20:55:16] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 20:55:16] z2m: 3 - read #2 done

In-between disabling wifi works by the way

[14.10.2025, 20:58:26] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":false,"password":"PiiiZaaa222","ssid":"tmaaa_ext"}
[14.10.2025, 20:58:27] z2m: 1 - write #1 done: {"enabled":false,"ssid":"tmaaa_ext","password":"PiiiZaaa222"}
[14.10.2025, 20:58:29] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 20:58:29] z2m: 3 - write #3 done: {"actionCode":1}
[14.10.2025, 20:58:33] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:58:33] z2m: >>> refresh()
[14.10.2025, 20:58:33] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:58:33] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":false,"ssid":"tmaaa_ext"},"wifi_status":"connecting","ip_address":"0.0.0.0","dhcp_enabled":true}
[14.10.2025, 20:58:33] z2m: 2 - read #1 done
[14.10.2025, 20:58:33] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 20:58:33] z2m: 3 - read #2 done

Verified that an 11 letter password works by changing the password

[14.10.2025, 20:58:50] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"YyyyXxxx222","ssid":"tmaaa_ext"}
[14.10.2025, 20:58:50] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"YyyyXxxx222"}
[14.10.2025, 20:58:50] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 20:58:50] z2m: 3 - write #3 done: {"actionCode":1}
[14.10.2025, 20:58:55] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 20:58:59] z2m: >>> refresh()
[14.10.2025, 20:58:59] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 20:58:59] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true,"ssid":"tmaaa_ext"},"wifi_status":"connecting","ip_address":"0.0.0.0","dhcp_enabled":true}
[14.10.2025, 20:58:59] z2m: 2 - read #1 done
[14.10.2025, 20:58:59] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 20:58:59] z2m: 3 - read #2 done

Verified that Password with 12 letters reliably brings a communication error again:

[14.10.2025, 21:01:53] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaaPPPP","ssid":"tmaaa_ext"}
[14.10.2025, 21:01:53] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaaPPPP"}
[14.10.2025, 21:02:04] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed ({"target":15797,"apsFrame":{"profileId":49153,"clusterId":64514,"sourceEndpoint":1,"destinationEndpoint":239,"options":4416,"groupId":0,"sequence":130},"zclSequence":241,"commandIdentifier":4} timed out after 10000ms)'
[14.10.2025, 21:02:54] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaaPPPP","ssid":"tmaaa_ext"}
[14.10.2025, 21:02:54] z2m: 1 - write #1 done: {"enabled":true,"ssid":"tmaaa_ext","password":"PiiiZaaaPPPP"}
[14.10.2025, 21:03:04] z2m: Publish 'set' 'wifi_config' to '0x7c2c67fffe7b5970' failed: 'Error: ZCL command 0x7c2c67fffe7b5970/239 shellyWiFiSetupCluster.write({"staticIp":"","netMask":"","gateway":"","nameServer":""}, {"timeout":10000,"disableResponse":false,"disableRecovery":false,"disableDefaultResponse":true,"direction":0,"reservedBits":0,"manufacturerCode":5264,"writeUndiv":false,"profileId":49153}) failed ({"target":15797,"apsFrame":{"profileId":49153,"clusterId":64514,"sourceEndpoint":1,"destinationEndpoint":239,"options":4416,"groupId":0,"sequence":134},"zclSequence":243,"commandIdentifier":4} timed out after 10000ms)'

-> more than 11 letters reliably breaks the write!

Out of curiosity, started also testing how the length of the network name behave

[14.10.2025, 21:07:25] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaaPP","ssid":"PiiiZaaaPPPP"}
[14.10.2025, 21:07:26] z2m: 1 - write #1 done: {"enabled":true,"ssid":"PiiiZaaaPPPP","password":"PiiiZaaaPP"}
[14.10.2025, 21:07:26] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[14.10.2025, 21:07:26] z2m: 3 - write #3 done: {"actionCode":1}
[14.10.2025, 21:07:41] z2m: >>> toZigbee.convertGet(wifi_config)
[14.10.2025, 21:07:42] z2m: >>> refresh()
[14.10.2025, 21:07:42] z2m: 1 - write done: {"actionCode":0}
[14.10.2025, 21:07:42] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true,"ssid":"PiiiZaaaP"},"wifi_status":"connecting","ip_address":"0.0.0.0","dhcp_enabled":true}
[14.10.2025, 21:07:42] z2m: 2 - read #1 done
[14.10.2025, 21:07:42] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[14.10.2025, 21:07:42] z2m: 3 - read #2 done
[14.10.2025, 21:07:45] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true,"ssid":"PiiiZaaaP"},"wifi_status":"connecting","ip_address":"0.0.0.0","dhcp_enabled":true}

-> ssid "PiiiZaaaPPPP" got shorted to "PiiiZaaaP" (9 letters) -> Might also be a problem for some people

Thank you for the finding! I'll investigate!

@dan-danache
Copy link
Contributor Author

The converter sends 3 write attribute commands, each write command containing more than 1 attribute update. Writing more at once, means that more bytes are sent in one command. The Zigbee specification defines a maximum frame size of 127 bytes at the MAC layer, but actual usable payload size is lower due to headers and encryption overhead.

Your "frame size" seems to be 82 bytes "on wire" = 37 bytes of "data":
image

My maximum "frame size" seems to be 127 (max!) bytes "on wire" = 82 bytes of "data":
image

I don't know why the frame size is smaller on your side:

  • Maybe the data is trimmed to 82/37 bytes by your coordinator hardware/firmware <- problem is with your setup
  • Maybe your coordinator sends the full 128/82 bytes, but the 2PM Gen4 only processes the first 82/37 (cannot know without a Wireshark frame capture) <- problem is with the Shelly device

I guess the best way to move further is to write less attributes at once, so instead of 3 writes per "Apply", we can go to 4. This way we keep the frames smaller and every setup works.

@dan-danache
Copy link
Contributor Author

dan-danache commented Oct 16, 2025

Try this updated version of the External Converter, which distributes attribute read/write operations across multiple Zigbee commands to help avoid frame size limitations.

  1. Navigate to Settings -> Dev console -> External Converters
  2. From the Select converter to edit dropdown, choose "Shelly.js"
  3. Replace the existing code with this version
  4. Click Save -> you should see a “converter/save : OK” notification if successful
  5. On your Shelly 2PM Gen4 device, click the blue Interview [i] button -> this should trigger the use of the updated external converter.

Also, please test the static configuration to ensure everything works as expected.

Thanks again for your effort and thorough testing!

@thmang82
Copy link

Did not see your second post, but modified the code myself by this write command:

    const attr1a = {
        enabled: value.enabled === true,
        ssid: value.ssid || ""
    };
    await ep.write("shellyWiFiSetupCluster", attr1a, SHELLY_OPTIONS);
    logger.warning(`1a - write #1 done: ${JSON.stringify(attr1a)}`);

    const attr1b = {
        password: value.password || "",  // change here, set password independent of ssid
    };
    await ep.write("shellyWiFiSetupCluster", attr1b, SHELLY_OPTIONS);
    logger.warning(`1b - write #1 done: ${JSON.stringify(attr1b)}`);

and this modified read command:

            await endpoint.write("shellyWiFiSetupCluster", {actionCode: 0}, SHELLY_OPTIONS);
            logger.warning(">>> refresh()");
            logger.warning("1 - write done: {\"actionCode\":0}");
            await endpoint.read("shellyWiFiSetupCluster", ["status", "ip", "enabled", "dhcp"], SHELLY_OPTIONS);
            logger.warning("2 - read #1 done");
            await endpoint.read("shellyWiFiSetupCluster", ["status", "ssid"], SHELLY_OPTIONS);
            logger.warning("3 - read #2 done"); // change here, get ssid independent of ip, ...
            await endpoint.read("shellyWiFiSetupCluster", ["staticIp", "netMask", "gateway", "nameServer"], SHELLY_OPTIONS);
            logger.warning("4 - read #3 done");

This is the result:

[16.10.2025, 20:56:40] z2m: >>> toZigbee.convertSet(wifi_config): {"enabled":true,"password":"PiiiZaaa2221Mggggga","ssid":"tmaaa_ext"}
[16.10.2025, 20:56:40] z2m: 1a - write #1 done: {"enabled":true,"ssid":"tmaaa_ext"}
[16.10.2025, 20:56:40] z2m: 1b - write #1 done: {"password":"PiiiZaaa2221Mggggga"}
[16.10.2025, 20:56:40] z2m: 2 - write #2 done: {"staticIp":"","netMask":"","gateway":"","nameServer":""}
[16.10.2025, 20:56:40] z2m: 3 - write #3 done: {"actionCode":1}

[16.10.2025, 20:56:52] z2m: >>> toZigbee.convertGet(wifi_config)
[16.10.2025, 20:56:52] z2m: >>> refresh()
[16.10.2025, 20:56:52] z2m: 1 - write done: {"actionCode":0}
[16.10.2025, 20:56:53] z2m: <<< fromZigbee.convert(): {"wifi_config":{"enabled":true},"wifi_status":"got ip","ip_address":"192.168.6.159","dhcp_enabled":true}
[16.10.2025, 20:56:53] z2m: 2 - read #1 done
[16.10.2025, 20:56:53] z2m: <<< fromZigbee.convert(): {"wifi_config":{"ssid":"tmaaa_ext"},"wifi_status":"got ip"}
[16.10.2025, 20:56:53] z2m: 3 - read #2 done
[16.10.2025, 20:56:53] z2m: <<< fromZigbee.convert(): {"wifi_config":{}}
[16.10.2025, 20:56:53] z2m: 4 - read #3 done

Works fine. Shelly connected to my network and gets an IP.
As the combined write on the old shelly firmware worked, I would guess that they reduced the receive buffer in the latest firmware and that broke it for my setup.

Splitting the write commands resolves it.
The writes and reads for the IP, netmask and so on should also be split. The truncated IP (see my post above) likely was due to the same reason of a too small send and/or receive buffer!

@dan-danache Thanks for the analysis. Will you provide an update for your code? I can offer a review if you like.
Would be also cool if you can find a way of propagating the reads to the UI. Not sure if that's possible though (I did not dig deeper into the UI extension design, but you would need to update the editable fields somehow)

@DataGhost
Copy link
Contributor

The RPC cluster isn't working (for me) in the dev console, all commands time out. I could be doing something wrong of course, but looking at some other code it seems that they're sending the profileId along with every command, as seems to happen in this PR as well, but only for the wifi cluster. I can't seem to specify this in the dev console and I guess it's required for this to work. The RPC cluster should expose things like LED control on the Power Strip without having to enable wifi (Koenkk/zigbee2mqtt#28974) so I'd say this is a nice feature to have working properly.

I've been trying to get this to work myself, but as often, I'm not too familiar with the ever-changing codebase and the internals of the Zigbee protocol, so there might be an easy fix but I haven't found it yet.

@DataGhost
Copy link
Contributor

DataGhost commented Oct 29, 2025

Okay I've managed to get the RPC cluster to work based on your code, but it wasn't generating full/any responses to my commands. It would correctly fill RxCtl with some high number (like 480) but reading Data times out most of the time without any data returning, or sometimes (<10%) it returns a single partial chunk that's either the beginning or the end of a response. I had to wait for another zigbee dongle to arrive so I could sniff what's going on and it seems like the power strip actually isn't sending Read Attributes Response most of the time when querying the Data attribute, although it does Ack the requests. I'm leaning towards a firmware bug.

@dan-danache have you had any luck using that cluster? I'm writing 43 into TxCtl, followed by {"id":1,"method":"POWERSTRIP_UI.GetConfig"} into Data. That should return a quite large JSON object but instead I've only observed the strings mode":"momentary"}}}} and t":{"restart_required":false}} as a response to that.

@dan-danache
Copy link
Contributor Author

It is on my to do list to play with the RPC cluster, but currently things are a bit crazy at work. I'll get back to it when things cool a little bit down. Sorry I cannot help rn.

@DataGhost
Copy link
Contributor

No problem. Shelly just responded and told me I am doing the correct thing, so you don't need to check/try anymore.

We can confirm that this is a known limitation in the current firmware of the Shelly Power Strip. Our development team is already aware of the issue, and improvements are planned in an upcoming firmware release to stabilize the RPC communication, especially for complex JSON responses.

So I'll go ahead on a write-only version of some controls for now.

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.

5 participants