@@ -111,6 +111,66 @@ interface ShellyLightLevel {
111111 commandResponses : never ;
112112}
113113
114+ let shellyRpcSending = false ;
115+
116+ const shellyRpcSendRaw = async ( endpoint : Zh . Endpoint | Zh . Group , message : string ) => {
117+ // Since RPC messages require multiple writes to complete, we have to make sure
118+ // we're not interleaving them accidentally. This is good enough for now, at least
119+ // until the RPC receive firmware bug is fixed by Shelly.
120+ while ( shellyRpcSending ) {
121+ await sleep ( 200 ) ;
122+ }
123+ try {
124+ shellyRpcSending = true ;
125+ const splitBytes = 40 ;
126+
127+ logger . debug ( ">>> shellyRPC write TxCtl" , NS ) ;
128+ const txCtl = message . length ;
129+ await endpoint . write < "shellyRPCCluster" , ShellyRPC > ( "shellyRPCCluster" , { txCtl : txCtl } , SHELLY_OPTIONS ) ;
130+ logger . debug ( `>>> TxCtl: ${ txCtl } ` , NS ) ;
131+
132+ logger . debug ( ">>> shellyRPC write Data" , NS ) ;
133+ let dataToSend = message ;
134+ while ( dataToSend . length > 0 ) {
135+ const data = dataToSend . substring ( 0 , splitBytes ) ;
136+ dataToSend = dataToSend . substring ( splitBytes ) ;
137+ await endpoint . write < "shellyRPCCluster" , ShellyRPC > ( "shellyRPCCluster" , { data : data } , SHELLY_OPTIONS ) ;
138+ logger . debug ( `>>> Data: ${ data } ` , NS ) ;
139+ }
140+ } finally {
141+ shellyRpcSending = false ;
142+ }
143+ } ;
144+
145+ const shellyRpcSend = async ( endpoint : Zh . Endpoint | Zh . Group , method : string , params : object = undefined ) => {
146+ const command = {
147+ id : 1 ,
148+ method : method ,
149+ params : params ,
150+ } ;
151+ return await shellyRpcSendRaw ( endpoint , JSON . stringify ( command ) ) ;
152+ } ;
153+
154+ const shellyRpcRequest = async ( endpoint : Zh . Endpoint | Zh . Group , method : string , params : object = undefined ) : Promise < KeyValue | undefined > => {
155+ await shellyRpcSend ( endpoint , method , params ) ;
156+ const rxCtlResult = await endpoint . read < "shellyRPCCluster" , ShellyRPC > ( "shellyRPCCluster" , [ "rxCtl" ] , SHELLY_OPTIONS ) ;
157+ const expectedLen = rxCtlResult . rxCtl ;
158+ if ( ! expectedLen ) return undefined ;
159+
160+ let accumulated = "" ;
161+ while ( accumulated . length < expectedLen ) {
162+ const dataResult = await endpoint . read < "shellyRPCCluster" , ShellyRPC > ( "shellyRPCCluster" , [ "data" ] , {
163+ ...SHELLY_OPTIONS ,
164+ timeout : 1000 ,
165+ } ) ;
166+ if ( ! dataResult . data ) break ;
167+ accumulated += dataResult . data ;
168+ }
169+
170+ if ( accumulated . length < expectedLen ) return undefined ;
171+ return JSON . parse ( accumulated . substring ( 0 , expectedLen ) ) ;
172+ } ;
173+
114174// =============================================================================
115175// WS90 Weather Station - Calculated Values (stored in device.meta for persistence)
116176// =============================================================================
@@ -476,66 +536,9 @@ const shellyModernExtend = {
476536 }
477537 } ;
478538
479- // RPC helper functions
480- let rpcSending = false ;
481-
482- const rpcSendRaw = async ( endpoint : Zh . Endpoint | Zh . Group , message : string ) => {
483- // Since RPC messages require multiple writes to complete, we have to make sure
484- // we're not interleaving them accidentally. This is good enough for now, at least
485- // until the RPC receive firmware bug is fixed by Shelly.
486- while ( rpcSending ) {
487- await sleep ( 200 ) ;
488- }
489- try {
490- rpcSending = true ;
491- const splitBytes = 40 ;
492-
493- logger . debug ( ">>> shellyRPC write TxCtl" , NS ) ;
494- const txCtl = message . length ;
495- await endpoint . write < "shellyRPCCluster" , ShellyRPC > ( "shellyRPCCluster" , { txCtl : txCtl } , SHELLY_OPTIONS ) ;
496- logger . debug ( `>>> TxCtl: ${ txCtl } ` , NS ) ;
497-
498- logger . debug ( ">>> shellyRPC write Data" , NS ) ;
499- let dataToSend = message ;
500- while ( dataToSend . length > 0 ) {
501- const data = dataToSend . substring ( 0 , splitBytes ) ;
502- dataToSend = dataToSend . substring ( splitBytes ) ;
503- await endpoint . write < "shellyRPCCluster" , ShellyRPC > ( "shellyRPCCluster" , { data : data } , SHELLY_OPTIONS ) ;
504- logger . debug ( `>>> Data: ${ data } ` , NS ) ;
505- }
506- } finally {
507- rpcSending = false ;
508- }
509- } ;
510-
511- const rpcSend = async ( endpoint : Zh . Endpoint | Zh . Group , method : string , params : object = undefined ) => {
512- const command = {
513- id : 1 ,
514- method : method ,
515- params : params ,
516- } ;
517- return await rpcSendRaw ( endpoint , JSON . stringify ( command ) ) ;
518- } ;
519-
520- const rpcRequest = async ( endpoint : Zh . Endpoint | Zh . Group , method : string , params : object = undefined ) : Promise < KeyValue | undefined > => {
521- await rpcSend ( endpoint , method , params ) ;
522- const rxCtlResult = await endpoint . read < "shellyRPCCluster" , ShellyRPC > ( "shellyRPCCluster" , [ "rxCtl" ] , SHELLY_OPTIONS ) ;
523- const expectedLen = rxCtlResult . rxCtl ;
524- if ( ! expectedLen ) return undefined ;
525-
526- let accumulated = "" ;
527- while ( accumulated . length < expectedLen ) {
528- const dataResult = await endpoint . read < "shellyRPCCluster" , ShellyRPC > ( "shellyRPCCluster" , [ "data" ] , {
529- ...SHELLY_OPTIONS ,
530- timeout : 1000 ,
531- } ) ;
532- if ( ! dataResult . data ) break ;
533- accumulated += dataResult . data ;
534- }
535-
536- if ( accumulated . length < expectedLen ) return undefined ;
537- return JSON . parse ( accumulated . substring ( 0 , expectedLen ) ) ;
538- } ;
539+ const rpcSendRaw = shellyRpcSendRaw ;
540+ const rpcSend = shellyRpcSend ;
541+ const rpcRequest = shellyRpcRequest ;
539542
540543 const getRPCEndpoint = ( entity : Zh . Endpoint | Zh . Group ) : Zh . Endpoint | Zh . Group => {
541544 utils . assertEndpoint ( entity ) ;
@@ -939,12 +942,85 @@ const shellyModernExtend = {
939942 return { exposes, fromZigbee, toZigbee, configure, isModernExtend : true } ;
940943 } ,
941944 shellyWiFiSetup ( ) : ModernExtend {
942- // biome-ignore lint/suspicious/noExplicitAny: generic
943- const refresh = async ( endpoint : any ) => {
944- await endpoint . write ( "shellyWiFiSetupCluster" , { actionCode : 0 } , SHELLY_OPTIONS ) ;
945- await endpoint . read ( "shellyWiFiSetupCluster" , [ "status" , "ip" , "enabled" , "dhcp" , "ssid" ] , SHELLY_OPTIONS ) ;
946- await endpoint . read ( "shellyWiFiSetupCluster" , [ "staticIp" , "netMask" ] , SHELLY_OPTIONS ) ;
947- await endpoint . read ( "shellyWiFiSetupCluster" , [ "gateway" , "nameServer" ] , SHELLY_OPTIONS ) ;
945+ const normalizeWifiString = ( value : unknown ) : string | undefined => ( typeof value === "string" && value !== "" ? value : undefined ) ;
946+ const getKnownFullWifiSsid = ( device : Zh . Device , options ?: KeyValue ) : string | undefined => {
947+ if ( typeof options ?. shelly_wifi_ssid === "string" && options . shelly_wifi_ssid !== "" ) return options . shelly_wifi_ssid ;
948+ if ( typeof device . meta . shelly_wifi_ssid === "string" && device . meta . shelly_wifi_ssid !== "" ) return device . meta . shelly_wifi_ssid ;
949+ return undefined ;
950+ } ;
951+ const cacheFullWifiSsid = ( device : Zh . Device , ssid : unknown ) => {
952+ if ( typeof ssid === "string" && ssid !== "" && device . meta . shelly_wifi_ssid !== ssid ) {
953+ device . meta . shelly_wifi_ssid = ssid ;
954+ device . save ( ) ;
955+ }
956+ } ;
957+ const rpcResult = ( response : KeyValue | undefined ) : KeyValue | undefined => {
958+ const result = response ?. result ?? response ?. params ?? response ;
959+ if ( ! result ) return undefined ;
960+ assertObject < KeyValue > ( result ) ;
961+ return result ;
962+ } ;
963+ const readWifiStateViaRpc = async ( endpoint : Zh . Endpoint ) : Promise < KeyValue | undefined > => {
964+ const config = rpcResult ( await shellyRpcRequest ( endpoint , "Wifi.GetConfig" ) ) ;
965+ const status = rpcResult ( await shellyRpcRequest ( endpoint , "Wifi.GetStatus" ) ) ;
966+ const state : KeyValue = { } ;
967+ const wifiConfig : KeyValue = { } ;
968+
969+ const sta = config ?. sta ;
970+ if ( sta ) {
971+ assertObject < KeyValue > ( sta ) ;
972+ if ( typeof sta . enable === "boolean" ) wifiConfig . enabled = sta . enable ;
973+ if ( typeof sta . ssid === "string" ) wifiConfig . ssid = sta . ssid ;
974+ if ( typeof sta . ipv4mode === "string" ) state . dhcp_enabled = sta . ipv4mode === "dhcp" ;
975+ if ( typeof sta . ip === "string" ) wifiConfig . static_ip = normalizeWifiString ( sta . ip ) ;
976+ if ( typeof sta . netmask === "string" ) wifiConfig . net_mask = normalizeWifiString ( sta . netmask ) ;
977+ if ( typeof sta . gw === "string" ) wifiConfig . gateway = normalizeWifiString ( sta . gw ) ;
978+ if ( typeof sta . nameserver === "string" ) wifiConfig . name_server = normalizeWifiString ( sta . nameserver ) ;
979+ }
980+
981+ if ( status ) {
982+ if ( typeof status . status === "string" ) state . wifi_status = status . status ;
983+ if ( typeof status . sta_ip === "string" ) state . ip_address = normalizeWifiString ( status . sta_ip ) ;
984+ if ( wifiConfig . ssid === undefined && typeof status . ssid === "string" ) wifiConfig . ssid = status . ssid ;
985+ }
986+
987+ if ( Object . keys ( wifiConfig ) . length > 0 ) {
988+ state . wifi_config = wifiConfig ;
989+ }
990+
991+ return Object . keys ( state ) . length > 0 ? state : undefined ;
992+ } ;
993+ const refresh = async ( endpoint : Zh . Endpoint , meta ?: Tz . Meta ) => {
994+ let published = false ;
995+ try {
996+ const rpcState = await readWifiStateViaRpc ( endpoint ) ;
997+ const ssid = rpcState ?. wifi_config ;
998+ if ( ssid && utils . isObject ( ssid ) ) {
999+ cacheFullWifiSsid ( endpoint . getDevice ( ) , ssid . ssid ) ;
1000+ }
1001+ if ( rpcState && meta ) {
1002+ meta . publish ( rpcState ) ;
1003+ published = true ;
1004+ }
1005+ } catch ( e ) {
1006+ logger . debug ( `Failed to read Wi-Fi state through Shelly RPC, falling back to setup cluster: ${ e } ` , NS ) ;
1007+ const ssid = meta ? getKnownFullWifiSsid ( endpoint . getDevice ( ) , meta . options ) : undefined ;
1008+ if ( ssid ) {
1009+ meta . publish ( { wifi_config : { ssid} } ) ;
1010+ published = true ;
1011+ }
1012+ }
1013+
1014+ if ( published ) {
1015+ return ;
1016+ }
1017+
1018+ // biome-ignore lint/suspicious/noExplicitAny: custom Shelly setup cluster is added dynamically
1019+ const setupEndpoint = endpoint as any ;
1020+ await setupEndpoint . write ( "shellyWiFiSetupCluster" , { actionCode : 0 } , SHELLY_OPTIONS ) ;
1021+ await setupEndpoint . read ( "shellyWiFiSetupCluster" , [ "status" , "ip" , "enabled" , "dhcp" , "ssid" ] , SHELLY_OPTIONS ) ;
1022+ await setupEndpoint . read ( "shellyWiFiSetupCluster" , [ "staticIp" , "netMask" ] , SHELLY_OPTIONS ) ;
1023+ await setupEndpoint . read ( "shellyWiFiSetupCluster" , [ "gateway" , "nameServer" ] , SHELLY_OPTIONS ) ;
9481024 } ;
9491025
9501026 const exposes : Expose [ ] = [
@@ -999,7 +1075,14 @@ const shellyModernExtend = {
9991075
10001076 // Wi-Fi config
10011077 if ( msg . data . enabled !== undefined ) wifi_config . enabled = msg . data . enabled === 1 ;
1002- if ( msg . data . ssid !== undefined ) wifi_config . ssid = msg . data . ssid ;
1078+ if ( msg . data . ssid !== undefined ) {
1079+ const reportedSsid = normalizeWifiString ( msg . data . ssid ) ;
1080+ const knownFullSsid = getKnownFullWifiSsid ( meta . device , options ) ;
1081+ wifi_config . ssid = knownFullSsid && reportedSsid && knownFullSsid . startsWith ( reportedSsid ) ? knownFullSsid : reportedSsid ;
1082+ if ( reportedSsid && ( ! knownFullSsid || reportedSsid . length > knownFullSsid . length ) ) {
1083+ cacheFullWifiSsid ( meta . device , reportedSsid ) ;
1084+ }
1085+ }
10031086 if ( msg . data . staticIp !== undefined ) wifi_config . static_ip = msg . data . staticIp ;
10041087 if ( msg . data . netMask !== undefined ) wifi_config . net_mask = msg . data . netMask ;
10051088 if ( msg . data . gateway !== undefined ) wifi_config . gateway = msg . data . gateway ;
@@ -1021,15 +1104,19 @@ const shellyModernExtend = {
10211104 {
10221105 key : [ "wifi_status" , "ip_address" , "dhcp_enabled" ] ,
10231106 convertGet : async ( entity , key , meta ) => {
1024- const ep = determineEndpoint ( entity , meta , "shellyWiFiSetupCluster" ) ;
1025- await refresh ( ep ) ;
1107+ utils . assertEndpoint ( entity ) ;
1108+ const ep = entity . getDevice ( ) . getEndpoint ( SHELLY_ENDPOINT_ID ) ;
1109+ if ( ! ep ) throw new Error ( `Shelly endpoint ${ SHELLY_ENDPOINT_ID } not found` ) ;
1110+ await refresh ( ep , meta ) ;
10261111 } ,
10271112 } ,
10281113 {
10291114 key : [ "wifi_config" ] ,
10301115 convertGet : async ( entity , key , meta ) => {
1031- const ep = determineEndpoint ( entity , meta , "shellyWiFiSetupCluster" ) ;
1032- await refresh ( ep ) ;
1116+ utils . assertEndpoint ( entity ) ;
1117+ const ep = entity . getDevice ( ) . getEndpoint ( SHELLY_ENDPOINT_ID ) ;
1118+ if ( ! ep ) throw new Error ( `Shelly endpoint ${ SHELLY_ENDPOINT_ID } not found` ) ;
1119+ await refresh ( ep , meta ) ;
10331120 } ,
10341121 convertSet : async ( entity , key , value , meta ) => {
10351122 assertObject < KeyValue > ( value ) ;
@@ -1082,11 +1169,18 @@ const shellyModernExtend = {
10821169 const configure : Configure [ ] = [
10831170 async ( device , coordinatorEndpoint , definition ) => {
10841171 const ep = device . getEndpoint ( SHELLY_ENDPOINT_ID ) ;
1172+ if ( ! ep ) return ;
10851173 await refresh ( ep ) ;
10861174 } ,
10871175 ] ;
10881176
1089- return { exposes, fromZigbee, toZigbee, configure, isModernExtend : true } ;
1177+ const options = [
1178+ e
1179+ . text ( "shelly_wifi_ssid" , ea . SET )
1180+ . withDescription ( "Full Wi-Fi SSID to use when the Shelly Wi-Fi setup cluster reports a shortened network name" ) ,
1181+ ] ;
1182+
1183+ return { exposes, fromZigbee, toZigbee, configure, options, isModernExtend : true } ;
10901184 } ,
10911185 ws90CalculatedValues ( ) : ModernExtend {
10921186 const exposes : Expose [ ] = [
0 commit comments