Skip to content

Commit 8560e19

Browse files
authored
Merge pull request #205 from athombv/feature/colorcontrol-colorloopset
Add colorLoopSet command and color loop attributes to Color Control cluster
2 parents 245388e + 4c8849e commit 8560e19

3 files changed

Lines changed: 257 additions & 10 deletions

File tree

index.d.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1835,6 +1835,14 @@ declare module "zigbee-clusters" {
18351835
currentY: { id: 0x04, type: ZCLDataType<number> },
18361836
colorTemperatureMireds: { id: 0x07, type: ZCLDataType<number> },
18371837
colorMode: { id: 0x08, type: ZCLDataType<"currentHueAndCurrentSaturation" | "currentXAndCurrentY" | "colorTemperatureMireds"> },
1838+
options: { id: 0x0f, type: ZCLDataType<Bitmap<"executeIfOff">> },
1839+
enhancedCurrentHue: { id: 0x4000, type: ZCLDataType<number> },
1840+
enhancedColorMode: { id: 0x4001, type: ZCLDataType<"currentHueAndCurrentSaturation" | "currentXAndCurrentY" | "colorTemperatureMireds" | "enhancedCurrentHueAndCurrentSaturation"> },
1841+
colorLoopActive: { id: 0x4002, type: ZCLDataType<number> },
1842+
colorLoopDirection: { id: 0x4003, type: ZCLDataType<number> },
1843+
colorLoopTime: { id: 0x4004, type: ZCLDataType<number> },
1844+
colorLoopStartEnhancedHue: { id: 0x4005, type: ZCLDataType<number> },
1845+
colorLoopStoredEnhancedHue: { id: 0x4006, type: ZCLDataType<number> },
18381846
colorCapabilities: { id: 0x400a, type: ZCLDataType<Bitmap<"hueAndSaturation" | "enhancedHue" | "colorLoop" | "xy" | "colorTemperature">> },
18391847
colorTempPhysicalMinMireds: { id: 0x400b, type: ZCLDataType<number> },
18401848
colorTempPhysicalMaxMireds: { id: 0x400c, type: ZCLDataType<number> },
@@ -1869,6 +1877,14 @@ declare module "zigbee-clusters" {
18691877
transitionTime: ZCLDataType<number>,
18701878
},
18711879
},
1880+
colorLoopSet: { id: 0x44, direction: "DIRECTION_SERVER_TO_CLIENT", args: {
1881+
updateFlags: ZCLDataType<Bitmap<"updateAction" | "updateDirection" | "updateTime" | "updateStartHue">>,
1882+
action: ZCLDataType<"deactivate" | "activateFromColorLoopStartEnhancedHue" | "activateFromEnhancedCurrentHue">,
1883+
direction: ZCLDataType<"decrementHue" | "incrementHue">,
1884+
time: ZCLDataType<number>,
1885+
startHue: ZCLDataType<number>,
1886+
},
1887+
},
18721888
};
18731889
class ColorControlCluster<Attributes extends types.AttributeDefinitions = ColorControlClusterAttributes, Commands extends types.CommandDefinitions = ColorControlClusterCommands> extends Cluster<Attributes, Commands> {
18741890
moveToHue(
@@ -1934,6 +1950,21 @@ declare module "zigbee-clusters" {
19341950
disableDefaultResponse?: boolean,
19351951
},
19361952
): Promise<void>;
1953+
colorLoopSet(
1954+
args?: {
1955+
manufacturerId?: number,
1956+
updateFlags?: Bitmap<"updateAction" | "updateDirection" | "updateTime" | "updateStartHue">,
1957+
action?: "deactivate" | "activateFromColorLoopStartEnhancedHue" | "activateFromEnhancedCurrentHue",
1958+
direction?: "decrementHue" | "incrementHue",
1959+
time?: number,
1960+
startHue?: number,
1961+
},
1962+
opts?: {
1963+
waitForResponse?: boolean,
1964+
timeout?: number,
1965+
disableDefaultResponse?: boolean,
1966+
},
1967+
): Promise<void>;
19371968
}
19381969
type BallastConfigurationClusterAttributes = {
19391970
physicalMinLevel: { id: 0x00, type: ZCLDataType<number> },

lib/clusters/colorControl.js

Lines changed: 117 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,19 @@ const ATTRIBUTES = {
99
currentHue: { id: 0, type: ZCLDataTypes.uint8 },
1010

1111
// Mandatory if colorCapabilities = hueAndSaturation
12-
// Reportable, read-only
12+
// Reportable, read-only, scene extension
1313
currentSaturation: { id: 1, type: ZCLDataTypes.uint8 },
1414

15-
// Mandatory
16-
// Reportable, read-only
15+
// Mandatory if colorCapabilities = xy
16+
// Reportable, read-only, scene extension
1717
currentX: { id: 3, type: ZCLDataTypes.uint16 },
1818

19-
// Mandatory
20-
// Reportable, read-only
19+
// Mandatory if colorCapabilities = xy
20+
// Reportable, read-only, scene extension
2121
currentY: { id: 4, type: ZCLDataTypes.uint16 },
2222

2323
// Mandatory if colorCapabilities = colorTemperature
24-
// Reportable, read-only
24+
// Reportable, read-only, scene extension
2525
colorTemperatureMireds: { id: 7, type: ZCLDataTypes.uint16 },
2626

2727
// Mandatory, read-only
@@ -34,9 +34,69 @@ const ATTRIBUTES = {
3434
}),
3535
},
3636

37+
// Mandatory
38+
// Read/write
39+
// Determines the default behavior of some cluster commands, meant to be changed only during
40+
// commissioning. When executeIfOff is not set, a command is not executed if the On/Off cluster
41+
// exists on the same endpoint and its onOff attribute is false.
42+
options: { id: 15, type: ZCLDataTypes.map8('executeIfOff') },
43+
44+
// Mandatory if colorCapabilities = enhancedHue
45+
// Read-only, scene extension
46+
// Represents non-equidistant steps along the CIE 1931 color triangle, providing 16-bits
47+
// precision. The upper 8 bits are used as an index in the implementation specific XY lookup
48+
// table, the lower 8 bits interpolate between these steps in a linear way. For compatibility
49+
// with standard ZCL, currentHue contains a hue value in the range 0 to 254, calculated from
50+
// this attribute.
51+
enhancedCurrentHue: { id: 16384, type: ZCLDataTypes.uint16 },
52+
53+
// Mandatory, read-only
54+
// Specifies which attributes are currently determining the color of the device. Note that for
55+
// compatibility with standard ZCL, colorMode reports currentHueAndCurrentSaturation while the
56+
// device is actually driven by enhancedCurrentHue, so read this attribute instead of colorMode
57+
// to tell those two apart.
58+
enhancedColorMode: {
59+
id: 16385,
60+
type: ZCLDataTypes.enum8({
61+
currentHueAndCurrentSaturation: 0,
62+
currentXAndCurrentY: 1,
63+
colorTemperatureMireds: 2,
64+
enhancedCurrentHueAndCurrentSaturation: 3,
65+
}),
66+
},
67+
68+
// Mandatory if colorCapabilities = colorLoop
69+
// Read-only, scene extension
70+
// Current active status of the color loop: 0x00 = inactive, 0x01 = active,
71+
// all other values (0x02 - 0xff) are reserved
72+
colorLoopActive: { id: 16386, type: ZCLDataTypes.uint8 },
73+
74+
// Mandatory if colorCapabilities = colorLoop
75+
// Read-only, scene extension
76+
// Current direction of the color loop: 0x00 = enhancedCurrentHue is decremented,
77+
// 0x01 = enhancedCurrentHue is incremented, all other values (0x02 - 0xff) are reserved
78+
colorLoopDirection: { id: 16387, type: ZCLDataTypes.uint8 },
79+
80+
// Mandatory if colorCapabilities = colorLoop
81+
// Read-only, scene extension
82+
// Number of seconds it takes to perform a full color loop, i.e. to cycle all values of the
83+
// enhancedCurrentHue attribute (between 0x0000 and 0xffff). Default 0x0019 (25 seconds).
84+
colorLoopTime: { id: 16388, type: ZCLDataTypes.uint16 },
85+
86+
// Mandatory if colorCapabilities = colorLoop
87+
// Read-only
88+
// Value of enhancedCurrentHue from which the color loop starts. Default 0x2300.
89+
colorLoopStartEnhancedHue: { id: 16389, type: ZCLDataTypes.uint16 },
90+
91+
// Mandatory if colorCapabilities = colorLoop
92+
// Read-only
93+
// Value of enhancedCurrentHue before the color loop was started. Once the color loop is
94+
// complete, enhancedCurrentHue is restored to this value.
95+
colorLoopStoredEnhancedHue: { id: 16390, type: ZCLDataTypes.uint16 },
96+
3797
// Mandatory, read-only
3898
// Bit 0: hueAndSaturation mandatory commands: moveToHue, moveHue, stepHue, moveToSaturation,
39-
// moveSaturation, stepSaturation moveToHueAndSaturation, stopMoveStep, related attributes:
99+
// moveSaturation, stepSaturation, moveToHueAndSaturation, stopMoveStep, related attributes:
40100
// currentHue, currentSaturation
41101

42102
// Bit 1: enhancedHue (hueAndSaturation must also be supported) mandatory commands:
@@ -63,8 +123,11 @@ const ATTRIBUTES = {
63123
// Read-only
64124
colorTempPhysicalMaxMireds: { id: 16396, type: ZCLDataTypes.uint16 },
65125

126+
// Mandatory if colorTemperatureMireds is supported
127+
// Read/write, scene extension
66128
// Startup color temperature, reflected in colorTemperatureMireds on power up
67-
// (0xffff = restore previous value)
129+
// (0xffff = restore previous value). On power up colorMode and enhancedColorMode are set to
130+
// colorTemperatureMireds.
68131
startUpColorTemperatureMireds: { id: 16400, type: ZCLDataTypes.uint16 },
69132
};
70133

@@ -73,19 +136,24 @@ const COMMANDS = {
73136
id: 0,
74137
args: {
75138
hue: ZCLDataTypes.uint8,
76-
direction: ZCLDataTypes.enum8({ // TODO: ?
139+
// Direction of travel round the hue circle. shortestDistance is normal usage,
140+
// longestDistance can be used for rainbow effects. For those two values only, the up
141+
// direction is taken when both paths are equally long. up and down are explicit.
142+
direction: ZCLDataTypes.enum8({
77143
shortestDistance: 0,
78144
longestDistance: 1,
79145
up: 2,
80146
down: 3,
81147
}),
148+
// In 1/10ths of a second
82149
transitionTime: ZCLDataTypes.uint16,
83150
},
84151
},
85152
moveToSaturation: {
86153
id: 3,
87-
args: { // TODO
154+
args: {
88155
saturation: ZCLDataTypes.uint8,
156+
// In 1/10ths of a second
89157
transitionTime: ZCLDataTypes.uint16,
90158
},
91159
},
@@ -94,6 +162,7 @@ const COMMANDS = {
94162
args: {
95163
hue: ZCLDataTypes.uint8,
96164
saturation: ZCLDataTypes.uint8,
165+
// In 1/10ths of a second
97166
transitionTime: ZCLDataTypes.uint16,
98167
},
99168
},
@@ -102,16 +171,54 @@ const COMMANDS = {
102171
args: {
103172
colorX: ZCLDataTypes.uint16,
104173
colorY: ZCLDataTypes.uint16,
174+
// In 1/10ths of a second
105175
transitionTime: ZCLDataTypes.uint16,
106176
},
107177
},
108178
moveToColorTemperature: {
109179
id: 10,
110180
args: {
111181
colorTemperature: ZCLDataTypes.uint16,
182+
// In 1/10ths of a second
112183
transitionTime: ZCLDataTypes.uint16,
113184
},
114185
},
186+
187+
// Mandatory if colorCapabilities = colorLoop
188+
// Activates a color loop such that the color lamp cycles through its range of hues
189+
colorLoopSet: {
190+
id: 68, // 0x44
191+
args: {
192+
// Specifies which color loop attributes to update before the color loop is started. The
193+
// action, direction, time and startHue fields below are ignored by the device unless their
194+
// flag is set here.
195+
updateFlags: ZCLDataTypes.map8(
196+
'updateAction', 'updateDirection', 'updateTime', 'updateStartHue',
197+
),
198+
199+
// Action to take for the color loop, only applied when updateFlags.updateAction is set
200+
action: ZCLDataTypes.enum8({
201+
deactivate: 0,
202+
activateFromColorLoopStartEnhancedHue: 1,
203+
activateFromEnhancedCurrentHue: 2,
204+
}),
205+
206+
// Direction for the color loop, written to colorLoopDirection when
207+
// updateFlags.updateDirection is set
208+
direction: ZCLDataTypes.enum8({
209+
decrementHue: 0,
210+
incrementHue: 1,
211+
}),
212+
213+
// Number of seconds over which to perform a full color loop, written to colorLoopTime when
214+
// updateFlags.updateTime is set
215+
time: ZCLDataTypes.uint16,
216+
217+
// Starting hue to use for the color loop, written to colorLoopStartEnhancedHue when
218+
// updateFlags.updateStartHue is set
219+
startHue: ZCLDataTypes.uint16,
220+
},
221+
},
115222
};
116223

117224
class ColorControlCluster extends Cluster {

test/colorControl.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,45 @@
22
'use strict';
33

44
const assert = require('assert');
5+
const sinon = require('sinon');
6+
57
const BoundCluster = require('../lib/BoundCluster');
68
const ColorControlCluster = require('../lib/clusters/colorControl');
9+
const Endpoint = require('../lib/Endpoint');
710
const { createMockNode } = require('./util');
811

12+
const sandbox = sinon.createSandbox();
13+
14+
// Sends colorLoopSet on a fresh node and returns the parsed outgoing frame, so tests can assert
15+
// the payload bytes against the ZCL spec instead of round-tripping through our own definition
16+
async function sendColorLoopSet(args) {
17+
const node = createMockNode({
18+
loopback: true,
19+
endpoints: [{
20+
endpointId: 1,
21+
inputClusters: [ColorControlCluster.ID],
22+
}],
23+
});
24+
25+
node.endpoints[1].bind('colorControl', new (class extends BoundCluster {
26+
27+
async colorLoopSet() {
28+
// Accept the command so no error response is generated
29+
}
30+
31+
})());
32+
33+
const sendFrameSpy = sandbox.spy(node, 'sendFrame');
34+
await node.endpoints[1].clusters.colorControl.colorLoopSet(args);
35+
36+
return Endpoint.parseFrame(sendFrameSpy.getCall(0).args[2]);
37+
}
38+
939
describe('Color Control', function() {
40+
afterEach(function() {
41+
sandbox.restore();
42+
});
43+
1044
it('should receive moveToColor', async function() {
1145
const node = createMockNode({
1246
loopback: true,
@@ -91,4 +125,79 @@ describe('Color Control', function() {
91125
assert.strictEqual(receivedData.saturation, 200);
92126
assert.strictEqual(receivedData.transitionTime, 10);
93127
});
128+
129+
it('should receive colorLoopSet', async function() {
130+
const node = createMockNode({
131+
loopback: true,
132+
endpoints: [{
133+
endpointId: 1,
134+
inputClusters: [ColorControlCluster.ID],
135+
}],
136+
});
137+
138+
let receivedData = null;
139+
node.endpoints[1].bind('colorControl', new (class extends BoundCluster {
140+
141+
async colorLoopSet(data) {
142+
receivedData = data;
143+
}
144+
145+
})());
146+
147+
await node.endpoints[1].clusters.colorControl.colorLoopSet({
148+
updateFlags: ['updateAction', 'updateDirection', 'updateTime', 'updateStartHue'],
149+
action: 'activateFromColorLoopStartEnhancedHue',
150+
direction: 'incrementHue',
151+
time: 30,
152+
startHue: 0x2300,
153+
});
154+
155+
assert.deepStrictEqual(receivedData.updateFlags.getBits(), [
156+
'updateAction', 'updateDirection', 'updateTime', 'updateStartHue',
157+
]);
158+
assert.strictEqual(receivedData.action, 'activateFromColorLoopStartEnhancedHue');
159+
assert.strictEqual(receivedData.direction, 'incrementHue');
160+
assert.strictEqual(receivedData.time, 30);
161+
assert.strictEqual(receivedData.startHue, 0x2300);
162+
});
163+
164+
// The loopback test above encodes and decodes with the same definition, so it passes even when
165+
// that definition is wrong on the wire. These bytes come from the spec, not from our types:
166+
// ZCL r8 Figure 5-17 (payload layout), Figure 5-18 (updateFlags bits), Table 5.25 (action
167+
// values), Table 5.26 (direction values), Table 5.13 (command id).
168+
it('should encode colorLoopSet as specified by the ZCL', async function() {
169+
const frame = await sendColorLoopSet({
170+
updateFlags: ['updateAction', 'updateStartHue'], // Bits 0 and 3, so 0x09
171+
action: 'activateFromEnhancedCurrentHue', // 0x02
172+
direction: 'decrementHue', // 0x00
173+
time: 0x1234, // uint16 little endian, so 34 12
174+
startHue: 0x2300, // uint16 little endian, so 00 23
175+
});
176+
177+
assert.strictEqual(frame.cmdId, 0x44);
178+
assert.deepStrictEqual(frame.data, Buffer.from([0x09, 0x02, 0x00, 0x34, 0x12, 0x00, 0x23]));
179+
});
180+
181+
// Setting every flag at once cannot tell the bit positions apart, so pin each one separately
182+
it('should encode each colorLoopSet update flag on its own bit', async function() {
183+
const flagBits = [
184+
['updateAction', 0x01],
185+
['updateDirection', 0x02],
186+
['updateTime', 0x04],
187+
['updateStartHue', 0x08],
188+
];
189+
190+
for (const [flag, expectedByte] of flagBits) {
191+
// eslint-disable-next-line no-await-in-loop
192+
const frame = await sendColorLoopSet({
193+
updateFlags: [flag],
194+
action: 'deactivate',
195+
direction: 'decrementHue',
196+
time: 0,
197+
startHue: 0,
198+
});
199+
200+
assert.strictEqual(frame.data[0], expectedByte, `wrong bit for ${flag}`);
201+
}
202+
});
94203
});

0 commit comments

Comments
 (0)