Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 25 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1863,32 +1863,42 @@ declare module "zigbee-clusters" {
startUpColorTemperatureMireds: { id: 0x4010, type: ZCLDataType<number> },
};
type ColorControlClusterCommands = {
moveToHue: { id: 0x00, direction: "DIRECTION_SERVER_TO_CLIENT", args: {
moveToHue: { id: 0x00, direction: "DIRECTION_SERVER_TO_CLIENT", encodeMissingFieldsBehavior: "skip", args: {
hue: ZCLDataType<number>,
direction: ZCLDataType<"shortestDistance" | "longestDistance" | "up" | "down">,
transitionTime: ZCLDataType<number>,
optionsMask: ZCLDataType<Bitmap<"executeIfOff">>,
optionsOverride: ZCLDataType<Bitmap<"executeIfOff">>,
},
},
moveToSaturation: { id: 0x03, direction: "DIRECTION_SERVER_TO_CLIENT", args: {
moveToSaturation: { id: 0x03, direction: "DIRECTION_SERVER_TO_CLIENT", encodeMissingFieldsBehavior: "skip", args: {
saturation: ZCLDataType<number>,
transitionTime: ZCLDataType<number>,
optionsMask: ZCLDataType<Bitmap<"executeIfOff">>,
optionsOverride: ZCLDataType<Bitmap<"executeIfOff">>,
},
},
moveToHueAndSaturation: { id: 0x06, direction: "DIRECTION_SERVER_TO_CLIENT", args: {
moveToHueAndSaturation: { id: 0x06, direction: "DIRECTION_SERVER_TO_CLIENT", encodeMissingFieldsBehavior: "skip", args: {
hue: ZCLDataType<number>,
saturation: ZCLDataType<number>,
transitionTime: ZCLDataType<number>,
optionsMask: ZCLDataType<Bitmap<"executeIfOff">>,
optionsOverride: ZCLDataType<Bitmap<"executeIfOff">>,
},
},
moveToColor: { id: 0x07, direction: "DIRECTION_SERVER_TO_CLIENT", args: {
moveToColor: { id: 0x07, direction: "DIRECTION_SERVER_TO_CLIENT", encodeMissingFieldsBehavior: "skip", args: {
colorX: ZCLDataType<number>,
colorY: ZCLDataType<number>,
transitionTime: ZCLDataType<number>,
optionsMask: ZCLDataType<Bitmap<"executeIfOff">>,
optionsOverride: ZCLDataType<Bitmap<"executeIfOff">>,
},
},
moveToColorTemperature: { id: 0x0a, direction: "DIRECTION_SERVER_TO_CLIENT", args: {
moveToColorTemperature: { id: 0x0a, direction: "DIRECTION_SERVER_TO_CLIENT", encodeMissingFieldsBehavior: "skip", args: {
colorTemperature: ZCLDataType<number>,
transitionTime: ZCLDataType<number>,
optionsMask: ZCLDataType<Bitmap<"executeIfOff">>,
optionsOverride: ZCLDataType<Bitmap<"executeIfOff">>,
},
},
colorLoopSet: { id: 0x44, direction: "DIRECTION_SERVER_TO_CLIENT", args: {
Expand All @@ -1907,6 +1917,8 @@ declare module "zigbee-clusters" {
hue?: number,
direction?: "shortestDistance" | "longestDistance" | "up" | "down",
transitionTime?: number,
optionsMask?: Bitmap<"executeIfOff">,
optionsOverride?: Bitmap<"executeIfOff">,
},
opts?: {
waitForResponse?: boolean,
Expand All @@ -1919,6 +1931,8 @@ declare module "zigbee-clusters" {
manufacturerId?: number,
saturation?: number,
transitionTime?: number,
optionsMask?: Bitmap<"executeIfOff">,
optionsOverride?: Bitmap<"executeIfOff">,
},
opts?: {
waitForResponse?: boolean,
Expand All @@ -1932,6 +1946,8 @@ declare module "zigbee-clusters" {
hue?: number,
saturation?: number,
transitionTime?: number,
optionsMask?: Bitmap<"executeIfOff">,
optionsOverride?: Bitmap<"executeIfOff">,
},
opts?: {
waitForResponse?: boolean,
Expand All @@ -1945,6 +1961,8 @@ declare module "zigbee-clusters" {
colorX?: number,
colorY?: number,
transitionTime?: number,
optionsMask?: Bitmap<"executeIfOff">,
optionsOverride?: Bitmap<"executeIfOff">,
},
opts?: {
waitForResponse?: boolean,
Expand All @@ -1957,6 +1975,8 @@ declare module "zigbee-clusters" {
manufacturerId?: number,
colorTemperature?: number,
transitionTime?: number,
optionsMask?: Bitmap<"executeIfOff">,
optionsOverride?: Bitmap<"executeIfOff">,
},
opts?: {
waitForResponse?: boolean,
Expand Down
24 changes: 23 additions & 1 deletion lib/clusters/colorControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
const Cluster = require('../Cluster');
const { ZCLDataTypes } = require('../zclTypes');

// Bits of the Options attribute and of the OptionsMask / OptionsOverride command fields
const OPTIONS_BITMAP = ZCLDataTypes.map8('executeIfOff');

// The OptionsMask & OptionsOverride fields SHALL both be present. Default values are provided to
// interpret missing fields from legacy devices. A temporary Options bitmap SHALL be created from
// the Options attribute, using the OptionsMask & OptionsOverride fields. (ZCL r8 5.2.2.3.3)
// Optional here for backwards compatibility: only encoded when both are provided.
const OPTIONS_ARGS = {
optionsMask: OPTIONS_BITMAP,
optionsOverride: OPTIONS_BITMAP,
};

const ATTRIBUTES = {
// Mandatory if colorCapabilities = hueAndSaturation
// Reportable, read-only
Expand Down Expand Up @@ -39,7 +51,7 @@ const ATTRIBUTES = {
// Determines the default behavior of some cluster commands, meant to be changed only during
// commissioning. When executeIfOff is not set, a command is not executed if the On/Off cluster
// exists on the same endpoint and its onOff attribute is false.
options: { id: 15, type: ZCLDataTypes.map8('executeIfOff') },
options: { id: 15, type: OPTIONS_BITMAP },

// Mandatory if colorCapabilities = enhancedHue
// Read-only, scene extension
Expand Down Expand Up @@ -134,6 +146,7 @@ const ATTRIBUTES = {
const COMMANDS = {
moveToHue: {
id: 0,
encodeMissingFieldsBehavior: 'skip',
args: {
hue: ZCLDataTypes.uint8,
// Direction of travel round the hue circle. shortestDistance is normal usage,
Expand All @@ -147,40 +160,49 @@ const COMMANDS = {
}),
// In 1/10ths of a second
transitionTime: ZCLDataTypes.uint16,
...OPTIONS_ARGS,
},
},
moveToSaturation: {
id: 3,
encodeMissingFieldsBehavior: 'skip',
args: {
saturation: ZCLDataTypes.uint8,
// In 1/10ths of a second
transitionTime: ZCLDataTypes.uint16,
...OPTIONS_ARGS,
},
},
moveToHueAndSaturation: {
id: 6,
encodeMissingFieldsBehavior: 'skip',
args: {
hue: ZCLDataTypes.uint8,
saturation: ZCLDataTypes.uint8,
// In 1/10ths of a second
transitionTime: ZCLDataTypes.uint16,
...OPTIONS_ARGS,
},
},
moveToColor: {
id: 7,
encodeMissingFieldsBehavior: 'skip',
args: {
colorX: ZCLDataTypes.uint16,
colorY: ZCLDataTypes.uint16,
// In 1/10ths of a second
transitionTime: ZCLDataTypes.uint16,
...OPTIONS_ARGS,
},
},
moveToColorTemperature: {
id: 10,
encodeMissingFieldsBehavior: 'skip',
args: {
colorTemperature: ZCLDataTypes.uint16,
// In 1/10ths of a second
transitionTime: ZCLDataTypes.uint16,
...OPTIONS_ARGS,
},
},

Expand Down
95 changes: 95 additions & 0 deletions test/colorControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,30 @@ async function sendColorLoopSet(args) {
return Endpoint.parseFrame(sendFrameSpy.getCall(0).args[2]);
}

// Sends a color command on a fresh node and returns the parsed outgoing frame (see above)
async function sendColorCommand(commandName, args) {
const node = createMockNode({
loopback: true,
endpoints: [{
endpointId: 1,
inputClusters: [ColorControlCluster.ID],
}],
});

node.endpoints[1].bind('colorControl', new (class extends BoundCluster {

async [commandName]() {
// Accept the command so no error response is generated
}

})());

const sendFrameSpy = sandbox.spy(node, 'sendFrame');
await node.endpoints[1].clusters.colorControl[commandName](args);

return Endpoint.parseFrame(sendFrameSpy.getCall(0).args[2]);
}

describe('Color Control', function() {
afterEach(function() {
sandbox.restore();
Expand Down Expand Up @@ -200,4 +224,75 @@ describe('Color Control', function() {
assert.strictEqual(frame.data[0], expectedByte, `wrong bit for ${flag}`);
}
});
// ZCL r8 Figure 5-12: Color Temperature Mireds (uint16), Transition Time (uint16), OptionsMask
// (map8), OptionsOverride (map8); 5.2.2.3.3: missing fields are interpreted for legacy devices
it('should encode moveToColorTemperature without options as before', async function() {
const frame = await sendColorCommand('moveToColorTemperature', {
colorTemperature: 0x011E, // 286, uint16 little endian, so 1e 01
transitionTime: 15, // 0f 00
});

assert.strictEqual(frame.cmdId, 0x0A);
assert.deepStrictEqual(frame.data, Buffer.from([0x1E, 0x01, 0x0F, 0x00]));
});

it('should encode moveToColorTemperature with options as specified by the ZCL', async function() {
const frame = await sendColorCommand('moveToColorTemperature', {
colorTemperature: 0x011E,
transitionTime: 15,
optionsMask: ['executeIfOff'], // Bit 0, so 0x01
optionsOverride: ['executeIfOff'],
});

assert.strictEqual(frame.cmdId, 0x0A);
assert.deepStrictEqual(frame.data, Buffer.from([0x1E, 0x01, 0x0F, 0x00, 0x01, 0x01]));
});

// ZCL r8 Figure 5-9: Color X, Color Y, Transition Time (uint16), OptionsMask, OptionsOverride
it('should encode moveToColor with options as specified by the ZCL', async function() {
const frame = await sendColorCommand('moveToColor', {
colorX: 0x5000,
colorY: 0x3000,
transitionTime: 20,
optionsMask: ['executeIfOff'],
optionsOverride: [], // Mask set, override cleared: do not execute when off
});

assert.strictEqual(frame.cmdId, 0x07);
assert.deepStrictEqual(
frame.data,
Buffer.from([0x00, 0x50, 0x00, 0x30, 0x14, 0x00, 0x01, 0x00]),
);
});

it('should receive moveToColorTemperature with options', async function() {
const node = createMockNode({
loopback: true,
endpoints: [{
endpointId: 1,
inputClusters: [ColorControlCluster.ID],
}],
});

let receivedData = null;
node.endpoints[1].bind('colorControl', new (class extends BoundCluster {

async moveToColorTemperature(data) {
receivedData = data;
}

})());

await node.endpoints[1].clusters.colorControl.moveToColorTemperature({
colorTemperature: 370,
transitionTime: 15,
optionsMask: ['executeIfOff'],
optionsOverride: ['executeIfOff'],
});

assert.strictEqual(receivedData.colorTemperature, 370);
assert.strictEqual(receivedData.transitionTime, 15);
assert.deepStrictEqual(receivedData.optionsMask.getBits(), ['executeIfOff']);
assert.deepStrictEqual(receivedData.optionsOverride.getBits(), ['executeIfOff']);
});
});