Skip to content
Merged
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
31 changes: 31 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1835,6 +1835,14 @@ declare module "zigbee-clusters" {
currentY: { id: 0x04, type: ZCLDataType<number> },
colorTemperatureMireds: { id: 0x07, type: ZCLDataType<number> },
colorMode: { id: 0x08, type: ZCLDataType<"currentHueAndCurrentSaturation" | "currentXAndCurrentY" | "colorTemperatureMireds"> },
options: { id: 0x0f, type: ZCLDataType<Bitmap<"executeIfOff">> },
enhancedCurrentHue: { id: 0x4000, type: ZCLDataType<number> },
enhancedColorMode: { id: 0x4001, type: ZCLDataType<"currentHueAndCurrentSaturation" | "currentXAndCurrentY" | "colorTemperatureMireds" | "enhancedCurrentHueAndCurrentSaturation"> },
colorLoopActive: { id: 0x4002, type: ZCLDataType<number> },
colorLoopDirection: { id: 0x4003, type: ZCLDataType<number> },
colorLoopTime: { id: 0x4004, type: ZCLDataType<number> },
colorLoopStartEnhancedHue: { id: 0x4005, type: ZCLDataType<number> },
colorLoopStoredEnhancedHue: { id: 0x4006, type: ZCLDataType<number> },
colorCapabilities: { id: 0x400a, type: ZCLDataType<Bitmap<"hueAndSaturation" | "enhancedHue" | "colorLoop" | "xy" | "colorTemperature">> },
colorTempPhysicalMinMireds: { id: 0x400b, type: ZCLDataType<number> },
colorTempPhysicalMaxMireds: { id: 0x400c, type: ZCLDataType<number> },
Expand Down Expand Up @@ -1869,6 +1877,14 @@ declare module "zigbee-clusters" {
transitionTime: ZCLDataType<number>,
},
},
colorLoopSet: { id: 0x44, direction: "DIRECTION_SERVER_TO_CLIENT", args: {
updateFlags: ZCLDataType<Bitmap<"updateAction" | "updateDirection" | "updateTime" | "updateStartHue">>,
action: ZCLDataType<"deactivate" | "activateFromColorLoopStartEnhancedHue" | "activateFromEnhancedCurrentHue">,
direction: ZCLDataType<"decrementHue" | "incrementHue">,
time: ZCLDataType<number>,
startHue: ZCLDataType<number>,
},
},
};
class ColorControlCluster<Attributes extends types.AttributeDefinitions = ColorControlClusterAttributes, Commands extends types.CommandDefinitions = ColorControlClusterCommands> extends Cluster<Attributes, Commands> {
moveToHue(
Expand Down Expand Up @@ -1934,6 +1950,21 @@ declare module "zigbee-clusters" {
disableDefaultResponse?: boolean,
},
): Promise<void>;
colorLoopSet(
args?: {
manufacturerId?: number,
updateFlags?: Bitmap<"updateAction" | "updateDirection" | "updateTime" | "updateStartHue">,
action?: "deactivate" | "activateFromColorLoopStartEnhancedHue" | "activateFromEnhancedCurrentHue",
direction?: "decrementHue" | "incrementHue",
time?: number,
startHue?: number,
},
opts?: {
waitForResponse?: boolean,
timeout?: number,
disableDefaultResponse?: boolean,
},
): Promise<void>;
}
type BallastConfigurationClusterAttributes = {
physicalMinLevel: { id: 0x00, type: ZCLDataType<number> },
Expand Down
127 changes: 117 additions & 10 deletions lib/clusters/colorControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,19 @@ const ATTRIBUTES = {
currentHue: { id: 0, type: ZCLDataTypes.uint8 },

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

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

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

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

// Mandatory, read-only
Expand All @@ -34,9 +34,69 @@ const ATTRIBUTES = {
}),
},

// Mandatory
// Read/write
// 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') },

// Mandatory if colorCapabilities = enhancedHue
// Read-only, scene extension
// Represents non-equidistant steps along the CIE 1931 color triangle, providing 16-bits
// precision. The upper 8 bits are used as an index in the implementation specific XY lookup
// table, the lower 8 bits interpolate between these steps in a linear way. For compatibility
// with standard ZCL, currentHue contains a hue value in the range 0 to 254, calculated from
// this attribute.
enhancedCurrentHue: { id: 16384, type: ZCLDataTypes.uint16 },

// Mandatory, read-only
// Specifies which attributes are currently determining the color of the device. Note that for
// compatibility with standard ZCL, colorMode reports currentHueAndCurrentSaturation while the
// device is actually driven by enhancedCurrentHue, so read this attribute instead of colorMode
// to tell those two apart.
enhancedColorMode: {
id: 16385,
type: ZCLDataTypes.enum8({
currentHueAndCurrentSaturation: 0,
currentXAndCurrentY: 1,
colorTemperatureMireds: 2,
enhancedCurrentHueAndCurrentSaturation: 3,
}),
},

// Mandatory if colorCapabilities = colorLoop
// Read-only, scene extension
// Current active status of the color loop: 0x00 = inactive, 0x01 = active,
// all other values (0x02 - 0xff) are reserved
colorLoopActive: { id: 16386, type: ZCLDataTypes.uint8 },

// Mandatory if colorCapabilities = colorLoop
// Read-only, scene extension
// Current direction of the color loop: 0x00 = enhancedCurrentHue is decremented,
// 0x01 = enhancedCurrentHue is incremented, all other values (0x02 - 0xff) are reserved
colorLoopDirection: { id: 16387, type: ZCLDataTypes.uint8 },

// Mandatory if colorCapabilities = colorLoop
// Read-only, scene extension
// Number of seconds it takes to perform a full color loop, i.e. to cycle all values of the
// enhancedCurrentHue attribute (between 0x0000 and 0xffff). Default 0x0019 (25 seconds).
colorLoopTime: { id: 16388, type: ZCLDataTypes.uint16 },

// Mandatory if colorCapabilities = colorLoop
// Read-only
// Value of enhancedCurrentHue from which the color loop starts. Default 0x2300.
colorLoopStartEnhancedHue: { id: 16389, type: ZCLDataTypes.uint16 },

// Mandatory if colorCapabilities = colorLoop
// Read-only
// Value of enhancedCurrentHue before the color loop was started. Once the color loop is
// complete, enhancedCurrentHue is restored to this value.
colorLoopStoredEnhancedHue: { id: 16390, type: ZCLDataTypes.uint16 },

// Mandatory, read-only
// Bit 0: hueAndSaturation mandatory commands: moveToHue, moveHue, stepHue, moveToSaturation,
// moveSaturation, stepSaturation moveToHueAndSaturation, stopMoveStep, related attributes:
// moveSaturation, stepSaturation, moveToHueAndSaturation, stopMoveStep, related attributes:
// currentHue, currentSaturation

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

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

Expand All @@ -73,19 +136,24 @@ const COMMANDS = {
id: 0,
args: {
hue: ZCLDataTypes.uint8,
direction: ZCLDataTypes.enum8({ // TODO: ?
// Direction of travel round the hue circle. shortestDistance is normal usage,
// longestDistance can be used for rainbow effects. For those two values only, the up
// direction is taken when both paths are equally long. up and down are explicit.
direction: ZCLDataTypes.enum8({
shortestDistance: 0,
longestDistance: 1,
up: 2,
down: 3,
}),
// In 1/10ths of a second
transitionTime: ZCLDataTypes.uint16,
},
},
moveToSaturation: {
id: 3,
args: { // TODO
args: {
saturation: ZCLDataTypes.uint8,
// In 1/10ths of a second
transitionTime: ZCLDataTypes.uint16,
},
},
Expand All @@ -94,6 +162,7 @@ const COMMANDS = {
args: {
hue: ZCLDataTypes.uint8,
saturation: ZCLDataTypes.uint8,
// In 1/10ths of a second
transitionTime: ZCLDataTypes.uint16,
},
},
Expand All @@ -102,16 +171,54 @@ const COMMANDS = {
args: {
colorX: ZCLDataTypes.uint16,
colorY: ZCLDataTypes.uint16,
// In 1/10ths of a second
transitionTime: ZCLDataTypes.uint16,
},
},
moveToColorTemperature: {
id: 10,
args: {
colorTemperature: ZCLDataTypes.uint16,
// In 1/10ths of a second
transitionTime: ZCLDataTypes.uint16,
},
},

// Mandatory if colorCapabilities = colorLoop
// Activates a color loop such that the color lamp cycles through its range of hues
colorLoopSet: {
id: 68, // 0x44
args: {
// Specifies which color loop attributes to update before the color loop is started. The
// action, direction, time and startHue fields below are ignored by the device unless their
// flag is set here.
updateFlags: ZCLDataTypes.map8(
'updateAction', 'updateDirection', 'updateTime', 'updateStartHue',
),

// Action to take for the color loop, only applied when updateFlags.updateAction is set
action: ZCLDataTypes.enum8({
deactivate: 0,
activateFromColorLoopStartEnhancedHue: 1,
activateFromEnhancedCurrentHue: 2,
}),

// Direction for the color loop, written to colorLoopDirection when
// updateFlags.updateDirection is set
direction: ZCLDataTypes.enum8({
decrementHue: 0,
incrementHue: 1,
}),

// Number of seconds over which to perform a full color loop, written to colorLoopTime when
// updateFlags.updateTime is set
time: ZCLDataTypes.uint16,

// Starting hue to use for the color loop, written to colorLoopStartEnhancedHue when
// updateFlags.updateStartHue is set
startHue: ZCLDataTypes.uint16,
},
},
};

class ColorControlCluster extends Cluster {
Expand Down
109 changes: 109 additions & 0 deletions test/colorControl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,45 @@
'use strict';

const assert = require('assert');
const sinon = require('sinon');

const BoundCluster = require('../lib/BoundCluster');
const ColorControlCluster = require('../lib/clusters/colorControl');
const Endpoint = require('../lib/Endpoint');
const { createMockNode } = require('./util');

const sandbox = sinon.createSandbox();

// Sends colorLoopSet 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) {
const node = createMockNode({
loopback: true,
endpoints: [{
endpointId: 1,
inputClusters: [ColorControlCluster.ID],
}],
});

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

async colorLoopSet() {
// Accept the command so no error response is generated
}

})());

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

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

describe('Color Control', function() {
afterEach(function() {
sandbox.restore();
});

it('should receive moveToColor', async function() {
const node = createMockNode({
loopback: true,
Expand Down Expand Up @@ -91,4 +125,79 @@ describe('Color Control', function() {
assert.strictEqual(receivedData.saturation, 200);
assert.strictEqual(receivedData.transitionTime, 10);
});

it('should receive colorLoopSet', 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 colorLoopSet(data) {
receivedData = data;
}

})());

await node.endpoints[1].clusters.colorControl.colorLoopSet({
updateFlags: ['updateAction', 'updateDirection', 'updateTime', 'updateStartHue'],
action: 'activateFromColorLoopStartEnhancedHue',
direction: 'incrementHue',
time: 30,
startHue: 0x2300,
});

assert.deepStrictEqual(receivedData.updateFlags.getBits(), [
'updateAction', 'updateDirection', 'updateTime', 'updateStartHue',
]);
assert.strictEqual(receivedData.action, 'activateFromColorLoopStartEnhancedHue');
assert.strictEqual(receivedData.direction, 'incrementHue');
assert.strictEqual(receivedData.time, 30);
assert.strictEqual(receivedData.startHue, 0x2300);
});

// The loopback test above encodes and decodes with the same definition, so it passes even when
// that definition is wrong on the wire. These bytes come from the spec, not from our types:
// 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({
updateFlags: ['updateAction', 'updateStartHue'], // Bits 0 and 3, so 0x09
action: 'activateFromEnhancedCurrentHue', // 0x02
direction: 'decrementHue', // 0x00
time: 0x1234, // uint16 little endian, so 34 12
startHue: 0x2300, // uint16 little endian, so 00 23
});

assert.strictEqual(frame.cmdId, 0x44);
assert.deepStrictEqual(frame.data, Buffer.from([0x09, 0x02, 0x00, 0x34, 0x12, 0x00, 0x23]));
});

// Setting every flag at once cannot tell the bit positions apart, so pin each one separately
it('should encode each colorLoopSet update flag on its own bit', async function() {
const flagBits = [
['updateAction', 0x01],
['updateDirection', 0x02],
['updateTime', 0x04],
['updateStartHue', 0x08],
];

for (const [flag, expectedByte] of flagBits) {
// eslint-disable-next-line no-await-in-loop
const frame = await sendColorLoopSet({
updateFlags: [flag],
action: 'deactivate',
direction: 'decrementHue',
time: 0,
startHue: 0,
});

assert.strictEqual(frame.data[0], expectedByte, `wrong bit for ${flag}`);
}
});
});
Loading