|
| 1 | +import assert from "node:assert"; |
1 | 2 | import {describe, expect, it, vi} from "vitest"; |
2 | 3 | import {findByDevice} from "../src/index"; |
3 | | -import type {DefinitionExposesFunction} from "../src/lib/types"; |
| 4 | +import type {DefinitionExposesFunction, Expose} from "../src/lib/types"; |
4 | 5 | import {mockDevice} from "./utils"; |
5 | 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 | + const mockShelly2PMCover = (read?: ReturnType<typeof vi.fn>) => |
| 13 | + 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: [], read}, |
| 19 | + {ID: 242, profileID: 41440, deviceID: 97, inputClusterIDs: [], outputClusterIDs: [33]}, |
| 20 | + ], |
| 21 | + }); |
| 22 | + |
| 23 | + it("exposes lift-only covers by default and opt-in tilt controls", async () => { |
| 24 | + const device = mockShelly2PMCover(); |
| 25 | + const definition = await findByDevice(device); |
| 26 | + expect(definition.model).toBe("S4SW-002P16EU-COVER"); |
| 27 | + expect(typeof definition.exposes).toBe("function"); |
| 28 | + const exposes = definition.exposes as DefinitionExposesFunction; |
| 29 | + |
| 30 | + const defaultCover = exposes(device, {}).find((expose) => expose.type === "cover"); |
| 31 | + const tiltCover = exposes(device, {cover_tilt_enabled: "true"}).find((expose) => expose.type === "cover"); |
| 32 | + device.meta.cover_tilt_enabled = true; |
| 33 | + const autoDetectedTiltCover = exposes(device, {cover_tilt_enabled: "auto"}).find((expose) => expose.type === "cover"); |
| 34 | + assert(defaultCover); |
| 35 | + assert(tiltCover); |
| 36 | + assert(autoDetectedTiltCover); |
| 37 | + |
| 38 | + expect(getFeatureNames(defaultCover)).toContain("position"); |
| 39 | + expect(getFeatureNames(defaultCover)).not.toContain("tilt"); |
| 40 | + expect(getFeatureNames(tiltCover)).toContain("position"); |
| 41 | + expect(getFeatureNames(tiltCover)).toContain("tilt"); |
| 42 | + expect(getFeatureNames(autoDetectedTiltCover)).toContain("tilt"); |
| 43 | + expect(definition.options?.some((option) => option.name === "cover_tilt_enabled")).toBe(true); |
| 44 | + }); |
| 45 | + |
| 46 | + it("persists Shelly slat control from Cover.GetConfig during configure", async () => { |
| 47 | + const response = JSON.stringify({id: 1, result: {id: 0, slat: {enable: true}}}); |
| 48 | + const read = vi |
| 49 | + .fn() |
| 50 | + .mockResolvedValueOnce({rxCtl: 0}) |
| 51 | + .mockResolvedValueOnce({rxCtl: 0}) |
| 52 | + .mockResolvedValueOnce({rxCtl: response.length}) |
| 53 | + .mockResolvedValueOnce({data: response}); |
| 54 | + const device = mockShelly2PMCover(read); |
| 55 | + const save = vi.spyOn(device, "save").mockImplementation(() => {}); |
| 56 | + const definition = await findByDevice(device); |
| 57 | + |
| 58 | + await definition.configure?.(device, mockShelly2PMCover().getEndpoint(1), definition); |
| 59 | + |
| 60 | + expect(device.meta.cover_tilt_enabled).toBe(true); |
| 61 | + expect(save).toHaveBeenCalledTimes(1); |
| 62 | + expect(read).toHaveBeenCalledWith("shellyRPCCluster", ["rxCtl"], expect.any(Object)); |
| 63 | + expect(read).toHaveBeenCalledWith("shellyRPCCluster", ["data"], expect.any(Object)); |
| 64 | + expect(device.getEndpoint(239).write).toHaveBeenCalledWith( |
| 65 | + "shellyRPCCluster", |
| 66 | + {data: expect.stringContaining("Cover.GetConfig")}, |
| 67 | + expect.any(Object), |
| 68 | + ); |
| 69 | + }); |
| 70 | + |
| 71 | + it("leaves persisted slat control unchanged when Cover.GetConfig has no boolean slat flag", async () => { |
| 72 | + const response = JSON.stringify({id: 1, result: {id: 0}}); |
| 73 | + const read = vi |
| 74 | + .fn() |
| 75 | + .mockResolvedValueOnce({rxCtl: 0}) |
| 76 | + .mockResolvedValueOnce({rxCtl: 0}) |
| 77 | + .mockResolvedValueOnce({rxCtl: response.length}) |
| 78 | + .mockResolvedValueOnce({data: response}); |
| 79 | + const device = mockShelly2PMCover(read); |
| 80 | + device.meta.cover_tilt_enabled = true; |
| 81 | + const save = vi.spyOn(device, "save").mockImplementation(() => {}); |
| 82 | + const definition = await findByDevice(device); |
| 83 | + |
| 84 | + await definition.configure?.(device, mockShelly2PMCover().getEndpoint(1), definition); |
| 85 | + |
| 86 | + expect(device.meta.cover_tilt_enabled).toBe(true); |
| 87 | + expect(save).not.toHaveBeenCalled(); |
| 88 | + }); |
| 89 | +}); |
| 90 | + |
6 | 91 | describe("Shelly Presence Gen4", () => { |
7 | 92 | const mockShellyPresence = (read?: ReturnType<typeof vi.fn>) => |
8 | 93 | mockDevice({ |
|
0 commit comments