Skip to content

Commit 26ed62b

Browse files
committed
Fix Shelly switch input endpoint discovery
1 parent b99c20f commit 26ed62b

2 files changed

Lines changed: 143 additions & 41 deletions

File tree

src/devices/shelly.ts

Lines changed: 70 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,23 @@ const checkOption = (device: Zh.Device | DummyDevice, options: KeyValue, key: st
2727
return defaultValue;
2828
};
2929

30+
const shellySwitchInputEndpoints = (device: Zh.Device | DummyDevice, endpoints: Record<string, number>): Record<string, number> => {
31+
if (utils.isDummyDevice(device)) return endpoints;
32+
33+
return Object.fromEntries(Object.entries(endpoints).filter(([, endpoint]) => device.getEndpoint(endpoint) !== undefined));
34+
};
35+
36+
const shellySwitchInputExposes = (device: Zh.Device | DummyDevice, endpoints: Record<string, number>): Expose[] =>
37+
Object.keys(shellySwitchInputEndpoints(device, endpoints)).map((endpoint) =>
38+
e.enum("switch_type", ea.ALL, ["toggle", "momentary"]).withDescription("Switch input type").withCategory("config").withEndpoint(endpoint),
39+
);
40+
41+
const shellyDeviceEndpoints = (endpoints: Record<string, number>): ModernExtend => ({
42+
meta: {multiEndpoint: true},
43+
endpoint: (device) => shellySwitchInputEndpoints(device, endpoints),
44+
isModernExtend: true,
45+
});
46+
3047
const shellyPresenceEndpointNames = (device: Zh.Device | DummyDevice): string[] => {
3148
const count =
3249
!utils.isDummyDevice(device) && typeof device.meta.presence_zone_count === "number"
@@ -520,6 +537,13 @@ const shellyModernExtend = {
520537
return JSON.parse(accumulated.substring(0, expectedLen));
521538
};
522539

540+
const getRPCEndpoint = (entity: Zh.Endpoint | Zh.Group): Zh.Endpoint | Zh.Group => {
541+
utils.assertEndpoint(entity);
542+
const endpoint = entity.getDevice().getEndpoint(SHELLY_ENDPOINT_ID);
543+
if (!endpoint) throw new Error(`Shelly RPC endpoint ${SHELLY_ENDPOINT_ID} not found`);
544+
return endpoint;
545+
};
546+
523547
const rpcReceive = async (endpoint: Zh.Endpoint | Zh.Group, key: string) => {
524548
logger.debug(`||| shellyRPC rpcReceive(${key})`, NS);
525549
if (key === "rpc_rxctl") {
@@ -533,6 +557,19 @@ const shellyModernExtend = {
533557
}
534558
};
535559

560+
const getShellySwitchConfig = async (endpoint: Zh.Endpoint | Zh.Group, id: number): Promise<KeyValue | undefined> => {
561+
const response = await rpcRequest(endpoint, "Switch.GetConfig", {id});
562+
const result = response?.result ?? response?.params ?? response;
563+
if (!result) return undefined;
564+
assertObject<KeyValue>(result);
565+
return result;
566+
};
567+
568+
const getShellySwitchMode = async (endpoint: Zh.Endpoint | Zh.Group, id: number): Promise<string | undefined> => {
569+
const config = await getShellySwitchConfig(endpoint, id);
570+
return typeof config?.in_mode === "string" ? config.in_mode : undefined;
571+
};
572+
536573
// Features for exposes
537574
const featurePercentage = (name: string, label: string) => {
538575
return e.numeric(name, ea.STATE_SET).withValueMin(0).withValueMax(100).withValueStep(1).withLabel(label).withUnit("%");
@@ -790,33 +827,30 @@ const shellyModernExtend = {
790827
const inModeValues = ["follow", "flip", "detached", "cycle", "activation"];
791828
exposes.push((device: Zh.Device | DummyDevice, _options: KeyValue) => {
792829
if (utils.isDummyDevice(device) || !device.getEndpoint(SHELLY_ENDPOINT_ID)) return [];
793-
return [
794-
e.enum("switch_mode", ea.ALL, inModeValues).withDescription("Switch input mode").withCategory("config").withEndpoint("sw1"),
795-
e.enum("switch_mode", ea.ALL, inModeValues).withDescription("Switch input mode").withCategory("config").withEndpoint("sw2"),
796-
];
830+
return Object.keys(shellySwitchInputEndpoints(device, {sw1: 2, sw2: 3})).map((endpoint) =>
831+
e.enum("switch_mode", ea.ALL, inModeValues).withDescription("Switch input mode").withCategory("config").withEndpoint(endpoint),
832+
);
797833
});
798834
toZigbee.push({
799835
key: ["switch_mode"],
800836
convertSet: async (entity, key, value, meta) => {
801837
const switchId = meta.endpoint_name === "sw1" ? 0 : 1;
802-
const ep = determineEndpoint(entity, meta, "shellyRPCCluster");
838+
const ep = getRPCEndpoint(entity);
803839
await rpcSend(ep, "Switch.SetConfig", {id: switchId, config: {in_mode: value}});
804840
return {state: {switch_mode: value}};
805841
},
806842
convertGet: async (entity, key, meta) => {
807843
const switchId = meta.endpoint_name === "sw1" ? 0 : 1;
808-
const ep = determineEndpoint(entity, meta, "shellyRPCCluster");
809-
await rpcSend(ep, "Switch.GetConfig", {id: switchId});
810-
await rpcReceive(ep, "rpc_rxctl");
844+
const ep = getRPCEndpoint(entity);
845+
await getShellySwitchMode(ep, switchId);
811846
},
812847
});
813848
configure.push(async (device) => {
814849
const ep = device.getEndpoint(SHELLY_ENDPOINT_ID);
815850
if (!ep) return;
816851
try {
817852
for (const id of [0, 1]) {
818-
await rpcSend(ep, "Switch.GetConfig", {id});
819-
await rpcReceive(ep, "rpc_rxctl");
853+
await getShellySwitchConfig(ep, id);
820854
}
821855
} catch (e) {
822856
logger.warning(`Failed to read switch_mode during configure, use get to retry: ${e}`, NS);
@@ -827,27 +861,27 @@ const shellyModernExtend = {
827861
const inModeValues = ["follow", "flip", "detached", "cycle", "activation"];
828862
exposes.push((device: Zh.Device | DummyDevice, _options: KeyValue) => {
829863
if (utils.isDummyDevice(device) || !device.getEndpoint(SHELLY_ENDPOINT_ID)) return [];
830-
return [e.enum("switch_mode", ea.ALL, inModeValues).withDescription("Switch input mode").withCategory("config").withEndpoint("sw1")];
864+
return Object.keys(shellySwitchInputEndpoints(device, {sw1: 2})).map((endpoint) =>
865+
e.enum("switch_mode", ea.ALL, inModeValues).withDescription("Switch input mode").withCategory("config").withEndpoint(endpoint),
866+
);
831867
});
832868
toZigbee.push({
833869
key: ["switch_mode"],
834870
convertSet: async (entity, key, value, meta) => {
835-
const ep = determineEndpoint(entity, meta, "shellyRPCCluster");
871+
const ep = getRPCEndpoint(entity);
836872
await rpcSend(ep, "Switch.SetConfig", {id: 0, config: {in_mode: value}});
837873
return {state: {switch_mode: value}};
838874
},
839875
convertGet: async (entity, key, meta) => {
840-
const ep = determineEndpoint(entity, meta, "shellyRPCCluster");
841-
await rpcSend(ep, "Switch.GetConfig", {id: 0});
842-
await rpcReceive(ep, "rpc_rxctl");
876+
const ep = getRPCEndpoint(entity);
877+
await getShellySwitchMode(ep, 0);
843878
},
844879
});
845880
configure.push(async (device) => {
846881
const ep = device.getEndpoint(SHELLY_ENDPOINT_ID);
847882
if (!ep) return;
848883
try {
849-
await rpcSend(ep, "Switch.GetConfig", {id: 0});
850-
await rpcReceive(ep, "rpc_rxctl");
884+
await getShellySwitchConfig(ep, 0);
851885
} catch (e) {
852886
logger.warning(`Failed to read switch_mode during configure, use get to retry: ${e}`, NS);
853887
}
@@ -1387,7 +1421,7 @@ const fzLocal = {
13871421
type: ["attributeReport", "readResponse"],
13881422
convert: (model, msg, publish, options, meta) => {
13891423
if (!Object.hasOwn(msg.data, "switchType")) return {};
1390-
const epName = utils.getFromLookup(msg.endpoint.ID, {2: "sw1", 3: "sw1", 4: "sw2"});
1424+
const epName = utils.getFromLookup(msg.endpoint.ID, {2: "sw1", 3: "sw2", 4: "sw2"});
13911425
if (!epName) return {};
13921426
return {[`switch_type_${epName}`]: utils.getFromLookup(msg.data.switchType as number, {0: "toggle", 1: "momentary"})};
13931427
},
@@ -1447,12 +1481,12 @@ export const definitions: DefinitionWithExtend[] = [
14471481
ota: true,
14481482
fromZigbee: [fzLocal.one_switch_input_events, fzLocal.one_switch_input_scene_events, fzLocal.switch_input_type],
14491483
toZigbee: [tzLocal.switch_input_type],
1450-
exposes: [
1484+
exposes: (device) => [
14511485
e.action(["input_1_on", "input_1_off", "input_1_toggle", "input_1_single", "input_1_double", "input_1_triple", "input_1_hold"]),
1452-
e.enum("switch_type", ea.ALL, ["toggle", "momentary"]).withDescription("Switch input type").withCategory("config").withEndpoint("sw1"),
1486+
...shellySwitchInputExposes(device, {sw1: 2}),
14531487
],
14541488
extend: [
1455-
m.deviceEndpoints({endpoints: {sw1: 2}}),
1489+
shellyDeviceEndpoints({sw1: 2}),
14561490
m.onOff({powerOnBehavior: false}),
14571491
...shellyModernExtend.shellyCustomClusters(),
14581492
shellyModernExtend.shellyRPCSetup(["1PMInputMode"]),
@@ -1475,12 +1509,12 @@ export const definitions: DefinitionWithExtend[] = [
14751509
ota: true,
14761510
fromZigbee: [fzLocal.one_switch_input_events, fzLocal.one_switch_input_scene_events, fzLocal.switch_input_type],
14771511
toZigbee: [tzLocal.switch_input_type],
1478-
exposes: [
1512+
exposes: (device) => [
14791513
e.action(["input_1_on", "input_1_off", "input_1_toggle", "input_1_single", "input_1_double", "input_1_triple", "input_1_hold"]),
1480-
e.enum("switch_type", ea.ALL, ["toggle", "momentary"]).withDescription("Switch input type").withCategory("config").withEndpoint("sw1"),
1514+
...shellySwitchInputExposes(device, {sw1: 2}),
14811515
],
14821516
extend: [
1483-
m.deviceEndpoints({endpoints: {sw1: 2}}),
1517+
shellyDeviceEndpoints({sw1: 2}),
14841518
m.onOff({powerOnBehavior: false}),
14851519
...shellyModernExtend.shellyCustomClusters(),
14861520
shellyModernExtend.shellyRPCSetup(["1PMInputMode"]),
@@ -1503,12 +1537,12 @@ export const definitions: DefinitionWithExtend[] = [
15031537
ota: true,
15041538
fromZigbee: [fzLocal.one_switch_input_events, fzLocal.one_switch_input_scene_events, fzLocal.switch_input_type],
15051539
toZigbee: [tzLocal.switch_input_type],
1506-
exposes: [
1540+
exposes: (device) => [
15071541
e.action(["input_1_on", "input_1_off", "input_1_toggle", "input_1_single", "input_1_double", "input_1_triple", "input_1_hold"]),
1508-
e.enum("switch_type", ea.ALL, ["toggle", "momentary"]).withDescription("Switch input type").withCategory("config").withEndpoint("sw1"),
1542+
...shellySwitchInputExposes(device, {sw1: 2}),
15091543
],
15101544
extend: [
1511-
m.deviceEndpoints({endpoints: {sw1: 2}}),
1545+
shellyDeviceEndpoints({sw1: 2}),
15121546
m.onOff({powerOnBehavior: false}),
15131547
m.electricityMeter({producedEnergy: true, acFrequency: true}),
15141548
shellyModernExtend.shellyPowerFactorInt16Fix(),
@@ -1533,12 +1567,12 @@ export const definitions: DefinitionWithExtend[] = [
15331567
ota: true,
15341568
fromZigbee: [fzLocal.one_switch_input_events, fzLocal.one_switch_input_scene_events, fzLocal.switch_input_type],
15351569
toZigbee: [tzLocal.switch_input_type],
1536-
exposes: [
1570+
exposes: (device) => [
15371571
e.action(["input_1_on", "input_1_off", "input_1_toggle", "input_1_single", "input_1_double", "input_1_triple", "input_1_hold"]),
1538-
e.enum("switch_type", ea.ALL, ["toggle", "momentary"]).withDescription("Switch input type").withCategory("config").withEndpoint("sw1"),
1572+
...shellySwitchInputExposes(device, {sw1: 2}),
15391573
],
15401574
extend: [
1541-
m.deviceEndpoints({endpoints: {sw1: 2}}),
1575+
shellyDeviceEndpoints({sw1: 2}),
15421576
m.onOff({powerOnBehavior: false}),
15431577
m.electricityMeter({producedEnergy: true, acFrequency: true}),
15441578
shellyModernExtend.shellyPowerFactorInt16Fix(),
@@ -1618,7 +1652,7 @@ export const definitions: DefinitionWithExtend[] = [
16181652
ota: true,
16191653
fromZigbee: [fzLocal.two_switch_inputs_events, fzLocal.two_switch_inputs_scene_events, fzLocal.switch_input_type],
16201654
toZigbee: [tzLocal.switch_input_type],
1621-
exposes: [
1655+
exposes: (device) => [
16221656
e.action([
16231657
"input_1_on",
16241658
"input_1_off",
@@ -1635,11 +1669,10 @@ export const definitions: DefinitionWithExtend[] = [
16351669
"input_2_triple",
16361670
"input_2_hold",
16371671
]),
1638-
e.enum("switch_type", ea.ALL, ["toggle", "momentary"]).withDescription("Switch input type").withCategory("config").withEndpoint("sw1"),
1639-
e.enum("switch_type", ea.ALL, ["toggle", "momentary"]).withDescription("Switch input type").withCategory("config").withEndpoint("sw2"),
1672+
...shellySwitchInputExposes(device, {sw1: 2, sw2: 3}),
16401673
],
16411674
extend: [
1642-
m.deviceEndpoints({endpoints: {sw1: 2, sw2: 3}}),
1675+
shellyDeviceEndpoints({sw1: 2, sw2: 3}),
16431676
shellyModernExtend.shellyWindowCovering(),
16441677
...shellyModernExtend.shellyCustomClusters(),
16451678
shellyModernExtend.shellyRPCSetup(["2PMInputMode", "CoverTiltAuto"]),
@@ -1690,7 +1723,7 @@ export const definitions: DefinitionWithExtend[] = [
16901723
ota: true,
16911724
fromZigbee: [fzLocal.two_switch_inputs_events, fzLocal.two_switch_inputs_scene_events, fzLocal.switch_input_type],
16921725
toZigbee: [tzLocal.switch_input_type],
1693-
exposes: [
1726+
exposes: (device) => [
16941727
e.action([
16951728
"input_1_on",
16961729
"input_1_off",
@@ -1707,11 +1740,10 @@ export const definitions: DefinitionWithExtend[] = [
17071740
"input_2_triple",
17081741
"input_2_hold",
17091742
]),
1710-
e.enum("switch_type", ea.ALL, ["toggle", "momentary"]).withDescription("Switch input type").withCategory("config").withEndpoint("sw1"),
1711-
e.enum("switch_type", ea.ALL, ["toggle", "momentary"]).withDescription("Switch input type").withCategory("config").withEndpoint("sw2"),
1743+
...shellySwitchInputExposes(device, {sw1: 3, sw2: 4}),
17121744
],
17131745
extend: [
1714-
m.deviceEndpoints({endpoints: {l1: 1, l2: 2, sw1: 3, sw2: 4}}),
1746+
shellyDeviceEndpoints({l1: 1, l2: 2, sw1: 3, sw2: 4}),
17151747
m.onOff({powerOnBehavior: false, endpointNames: ["l1", "l2"]}),
17161748
m.electricityMeter({producedEnergy: true, acFrequency: true, endpointNames: ["l1", "l2"]}),
17171749
shellyModernExtend.shellyPowerFactorInt16Fix(),

0 commit comments

Comments
 (0)