Skip to content

Commit 5f6d63f

Browse files
committed
Make Shelly 2PM cover tilt opt-in
1 parent 3e4d8c4 commit 5f6d63f

2 files changed

Lines changed: 66 additions & 1 deletion

File tree

src/devices/shelly.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ const NS = "zhc:shelly";
1818
const HA_ELECTRICAL_MEASUREMENT_CLUSTER_ID = 0x0b04;
1919
const HA_ELECTRICAL_MEASUREMENT_POWER_FACTOR_ATTR_ID = 0x0510;
2020

21+
const checkOption = (device: Zh.Device | DummyDevice, options: KeyValue, key: string, defaultValue = false): boolean => {
22+
if (options?.[key] === "true") return true;
23+
if (options?.[key] === "false") return false;
24+
if (!utils.isDummyDevice(device) && device.meta[key] !== undefined) return !!device.meta[key];
25+
26+
return defaultValue;
27+
};
28+
2129
interface ShellyRPC {
2230
attributes: {
2331
data: string;
@@ -371,6 +379,25 @@ const shellyModernExtend = {
371379
}),
372380
];
373381
},
382+
shellyWindowCovering(): ModernExtend {
383+
const result = m.windowCovering({controls: ["lift", "tilt"]});
384+
const tiltOption = e
385+
.enum("cover_tilt_enabled", ea.SET, ["auto", "true", "false"])
386+
.withDescription("Expose tilt/slat controls for covers with Shelly slat control enabled");
387+
const exposesFn: DefinitionExposesFunction = (device, options) => {
388+
const cover = e.cover().withPosition();
389+
if (checkOption(device, options, "cover_tilt_enabled")) {
390+
cover.withTilt();
391+
}
392+
393+
return [cover];
394+
};
395+
396+
result.exposes = [exposesFn];
397+
result.options = [...(result.options ?? []), tiltOption];
398+
399+
return result;
400+
},
374401
shellyRPCSetup(features: string[] = []): ModernExtend {
375402
// Set helper variables
376403
const shellyRPCBugFixed = false; // For firmware 20250819-150402/ga0def2d
@@ -1501,7 +1528,7 @@ export const definitions: DefinitionWithExtend[] = [
15011528
],
15021529
extend: [
15031530
m.deviceEndpoints({endpoints: {sw1: 2, sw2: 3}}),
1504-
m.windowCovering({controls: ["lift", "tilt"]}),
1531+
shellyModernExtend.shellyWindowCovering(),
15051532
...shellyModernExtend.shellyCustomClusters(),
15061533
shellyModernExtend.shellyRPCSetup(["2PMInputMode"]),
15071534
shellyModernExtend.shellyWiFiSetup(),

test/shelly.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import assert from "node:assert";
2+
import {describe, expect, it} from "vitest";
3+
import {findByDevice} from "../src/index";
4+
import type {DefinitionExposesFunction, Expose} from "../src/lib/types";
5+
import {mockDevice} from "./utils";
6+
7+
const getFeatureNames = (expose: Expose): string[] => {
8+
return "features" in expose ? expose.features.map((feature) => feature.name) : [];
9+
};
10+
11+
describe("Shelly 2PM Gen4 cover mode", () => {
12+
it("exposes lift-only covers by default and opt-in tilt controls", async () => {
13+
const device = mockDevice({
14+
modelID: "2PM",
15+
manufacturerName: "Shelly",
16+
endpoints: [
17+
{ID: 1, profileID: 260, deviceID: 514, inputClusterIDs: [0, 3, 4, 5, 258], outputClusterIDs: []},
18+
{ID: 239, profileID: 49153, deviceID: 8193, inputClusterIDs: [64513, 64514], outputClusterIDs: []},
19+
{ID: 242, profileID: 41440, deviceID: 97, inputClusterIDs: [], outputClusterIDs: [33]},
20+
],
21+
});
22+
const definition = await findByDevice(device);
23+
expect(definition.model).toBe("S4SW-002P16EU-COVER");
24+
expect(typeof definition.exposes).toBe("function");
25+
const exposes = definition.exposes as DefinitionExposesFunction;
26+
27+
const defaultCover = exposes(device, {}).find((expose) => expose.type === "cover");
28+
const tiltCover = exposes(device, {cover_tilt_enabled: "true"}).find((expose) => expose.type === "cover");
29+
assert(defaultCover);
30+
assert(tiltCover);
31+
32+
expect(getFeatureNames(defaultCover)).toContain("position");
33+
expect(getFeatureNames(defaultCover)).not.toContain("tilt");
34+
expect(getFeatureNames(tiltCover)).toContain("position");
35+
expect(getFeatureNames(tiltCover)).toContain("tilt");
36+
expect(definition.options?.some((option) => option.name === "cover_tilt_enabled")).toBe(true);
37+
});
38+
});

0 commit comments

Comments
 (0)