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 @@ -1868,32 +1868,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 @@ -1912,6 +1922,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 @@ -1924,6 +1936,8 @@ declare module "zigbee-clusters" {
manufacturerId?: number,
saturation?: number,
transitionTime?: number,
optionsMask?: Bitmap<"executeIfOff">,
optionsOverride?: Bitmap<"executeIfOff">,
},
opts?: {
waitForResponse?: boolean,
Expand All @@ -1937,6 +1951,8 @@ declare module "zigbee-clusters" {
hue?: number,
saturation?: number,
transitionTime?: number,
optionsMask?: Bitmap<"executeIfOff">,
optionsOverride?: Bitmap<"executeIfOff">,
},
opts?: {
waitForResponse?: boolean,
Expand All @@ -1950,6 +1966,8 @@ declare module "zigbee-clusters" {
colorX?: number,
colorY?: number,
transitionTime?: number,
optionsMask?: Bitmap<"executeIfOff">,
optionsOverride?: Bitmap<"executeIfOff">,
},
opts?: {
waitForResponse?: boolean,
Expand All @@ -1962,6 +1980,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 @@ -143,6 +155,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 @@ -156,40 +169,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
86 changes: 80 additions & 6 deletions test/colorControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ const BoundCluster = require('../lib/BoundCluster');
const ColorControlCluster = require('../lib/clusters/colorControl');
const { createVirtualDevice } = require('./util/virtualDevice');

// Sends colorLoopSet on a fresh node and returns the parsed outgoing frame, so tests can assert
// Sends a color command on a fresh node and returns the parsed outgoing frame, so tests can assert
// the payload bytes against the ZCL spec instead of round-tripping through our own definition
async function sendColorLoopSet(args) {
async function sendColorCommand(commandName, args) {
const { zclNode, virtualDevice, frames } = createVirtualDevice({
endpoints: [
{
Expand All @@ -22,13 +22,13 @@ async function sendColorLoopSet(args) {
virtualDevice.endpoints[1].bind(
'colorControl',
new (class extends BoundCluster {
async colorLoopSet() {
async [commandName]() {
// Accept the command so no error response is generated
}
})(),
);

await zclNode.endpoints[1].clusters.colorControl.colorLoopSet(args);
await zclNode.endpoints[1].clusters.colorControl[commandName](args);

assert.strictEqual(frames[0].from, 'zclNode');
return frames[0].frame;
Expand Down Expand Up @@ -171,7 +171,7 @@ describe('Color Control', function () {
// ZCL r8 Figure 5-17 (payload layout), Figure 5-18 (updateFlags bits), Table 5.25 (action
// values), Table 5.26 (direction values), Table 5.13 (command id).
it('should encode colorLoopSet as specified by the ZCL', async function () {
const frame = await sendColorLoopSet({
const frame = await sendColorCommand('colorLoopSet', {
updateFlags: ['updateAction', 'updateStartHue'], // Bits 0 and 3, so 0x09
action: 'activateFromEnhancedCurrentHue', // 0x02
direction: 'decrementHue', // 0x00
Expand All @@ -193,7 +193,7 @@ describe('Color Control', function () {
];

for (const [flag, expectedByte] of flagBits) {
const frame = await sendColorLoopSet({
const frame = await sendColorCommand('colorLoopSet', {
updateFlags: [flag],
action: 'deactivate',
direction: 'decrementHue',
Expand All @@ -204,4 +204,78 @@ 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 { zclNode, virtualDevice } = createVirtualDevice({
endpoints: [
{
endpointId: 1,
inputClusters: [ColorControlCluster.ID],
},
],
});

let receivedData = null;
virtualDevice.endpoints[1].bind(
'colorControl',
new (class extends BoundCluster {
async moveToColorTemperature(data) {
receivedData = data;
}
})(),
);

await zclNode.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']);
});
});
Loading