Skip to content

Commit 755469a

Browse files
committed
Read Shelly Wi-Fi config through RPC
1 parent 26ed62b commit 755469a

2 files changed

Lines changed: 289 additions & 72 deletions

File tree

src/devices/shelly.ts

Lines changed: 166 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)