Skip to content

Commit cad694d

Browse files
committed
Read Shelly input type through RPC
1 parent f57a7c0 commit cad694d

2 files changed

Lines changed: 90 additions & 0 deletions

File tree

src/devices/shelly.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1595,16 +1595,64 @@ const fzLocal = {
15951595
} satisfies Fz.Converter<"ssIasZone", undefined, ["commandStatusChangeNotification", "attributeReport", "readResponse"]>,
15961596
};
15971597

1598+
const shellyRpcResult = (response: KeyValue | undefined): KeyValue | undefined => {
1599+
const result = response?.result ?? response?.params ?? response;
1600+
if (!result) return undefined;
1601+
assertObject<KeyValue>(result);
1602+
return result;
1603+
};
1604+
1605+
const getShellyInputId = (meta: Pick<Tz.Meta, "endpoint_name">): number => {
1606+
if (meta.endpoint_name === "sw2") return 1;
1607+
return 0;
1608+
};
1609+
1610+
const getShellyRpcEndpoint = (entity: Zh.Endpoint | Zh.Group): Zh.Endpoint | undefined => {
1611+
if (!utils.isEndpoint(entity)) return undefined;
1612+
return entity.getDevice().getEndpoint(SHELLY_ENDPOINT_ID);
1613+
};
1614+
1615+
const shellyInputTypeLookup = {
1616+
switch: "toggle",
1617+
button: "momentary",
1618+
} as const;
1619+
1620+
const shellyInputTypeSetLookup = {
1621+
toggle: "switch",
1622+
momentary: "button",
1623+
} as const;
1624+
15981625
const tzLocal = {
15991626
switch_input_type: {
16001627
key: ["switch_type"],
16011628
convertSet: async (entity, key, value, meta) => {
16021629
const lookup = {toggle: 0, momentary: 1} as const;
1630+
const rpcEndpoint = getShellyRpcEndpoint(entity);
1631+
if (rpcEndpoint) {
1632+
const inputId = getShellyInputId(meta);
1633+
await shellyRpcSend(rpcEndpoint, "Input.SetConfig", {
1634+
id: inputId,
1635+
config: {type: utils.getFromLookup(value as string, shellyInputTypeSetLookup)},
1636+
});
1637+
return {state: {switch_type: value}};
1638+
}
1639+
16031640
const ep = determineEndpoint(entity, meta, "genOnOffSwitchCfg");
16041641
await ep.write("genOnOffSwitchCfg", {switchType: utils.getFromLookup(value as string, lookup)});
16051642
return {state: {switch_type: value}};
16061643
},
16071644
convertGet: async (entity, key, meta) => {
1645+
const rpcEndpoint = getShellyRpcEndpoint(entity);
1646+
if (rpcEndpoint) {
1647+
const inputId = getShellyInputId(meta);
1648+
const config = shellyRpcResult(await shellyRpcRequest(rpcEndpoint, "Input.GetConfig", {id: inputId}));
1649+
if (typeof config?.type === "string") {
1650+
const switchType = utils.getFromLookup(config.type, shellyInputTypeLookup);
1651+
meta.publish({[meta.endpoint_name ? `switch_type_${meta.endpoint_name}` : "switch_type"]: switchType});
1652+
}
1653+
return;
1654+
}
1655+
16081656
const ep = determineEndpoint(entity, meta, "genOnOffSwitchCfg");
16091657
await ep.read("genOnOffSwitchCfg", ["switchType"]);
16101658
},

test/shelly.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,48 @@ describe("Shelly 2PM Gen4 cover mode", () => {
9797
expect(state).toStrictEqual({switch_type_sw1: "toggle"});
9898
});
9999

100+
it("reads switch input type through the Shelly RPC endpoint", async () => {
101+
const response = JSON.stringify({id: 1, result: {id: 1, type: "button", enable: true, invert: false}});
102+
const read = vi.fn().mockResolvedValueOnce({rxCtl: response.length}).mockResolvedValueOnce({data: response});
103+
const device = mockShelly2PMCoverWithInputs(read);
104+
const definition = await findByDevice(device);
105+
const converter = definition.toZigbee.find((converter) => converter.key.includes("switch_type")) as Tz.Converter;
106+
const publish = vi.fn();
107+
108+
await converter.convertGet?.(device.getEndpoint(3), "switch_type", {endpoint_name: "sw2", message: {}, publish} as never);
109+
110+
expect(device.getEndpoint(239).write).toHaveBeenCalledWith(
111+
"shellyRPCCluster",
112+
{data: expect.stringContaining("Input.GetConfig")},
113+
expect.any(Object),
114+
);
115+
expect(device.getEndpoint(239).write).toHaveBeenCalledWith("shellyRPCCluster", {data: expect.stringContaining('"id":1')}, expect.any(Object));
116+
expect(read).toHaveBeenCalledWith("shellyRPCCluster", ["rxCtl"], expect.any(Object));
117+
expect(read).toHaveBeenCalledWith("shellyRPCCluster", ["data"], expect.any(Object));
118+
expect(device.getEndpoint(3).read).not.toHaveBeenCalled();
119+
expect(publish).toHaveBeenCalledWith({switch_type_sw2: "momentary"});
120+
});
121+
122+
it("writes switch input type through the Shelly RPC endpoint", async () => {
123+
const device = mockShelly2PMCoverWithInputs();
124+
const definition = await findByDevice(device);
125+
const converter = definition.toZigbee.find((converter) => converter.key.includes("switch_type")) as Tz.Converter;
126+
127+
await converter.convertSet?.(device.getEndpoint(3), "switch_type", "momentary", {endpoint_name: "sw2", message: {}} as never);
128+
129+
expect(device.getEndpoint(239).write).toHaveBeenCalledWith(
130+
"shellyRPCCluster",
131+
{data: expect.stringContaining("Input.SetConfig")},
132+
expect.any(Object),
133+
);
134+
expect(device.getEndpoint(239).write).toHaveBeenCalledWith(
135+
"shellyRPCCluster",
136+
{data: expect.stringContaining('"type":"button"')},
137+
expect.any(Object),
138+
);
139+
expect(device.getEndpoint(3).write).not.toHaveBeenCalled();
140+
});
141+
100142
it("reads two-channel switch mode through the Shelly RPC endpoint", async () => {
101143
const response = JSON.stringify({id: 1, result: {id: 1, in_mode: "detached"}});
102144
const read = vi.fn().mockResolvedValueOnce({rxCtl: response.length}).mockResolvedValueOnce({data: response});

0 commit comments

Comments
 (0)