zwave-js
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
451 lines • 19.2 kB
JavaScript
import { BinarySwitchCCValues, ColorComponent, ColorSwitchCCValues, ConfigurationCCValues, SetValueStatus, } from "@zwave-js/cc";
import { ConfigValueFormat, MessagePriority, getCCName, parseBitMask, } from "@zwave-js/core";
import { FunctionType, Message, MessageType } from "@zwave-js/serial";
import { Bytes } from "@zwave-js/shared";
import { roundTo } from "alcalzone-shared/math";
export const FUNC_ID_NABUCASA = FunctionType.Proprietary_F0;
export var NabuCasaCommand;
(function (NabuCasaCommand) {
NabuCasaCommand[NabuCasaCommand["GetSupportedCommands"] = 0] = "GetSupportedCommands";
NabuCasaCommand[NabuCasaCommand["GetLED"] = 1] = "GetLED";
NabuCasaCommand[NabuCasaCommand["SetLED"] = 2] = "SetLED";
NabuCasaCommand[NabuCasaCommand["ReadGyro"] = 3] = "ReadGyro";
NabuCasaCommand[NabuCasaCommand["SetSystemIndication"] = 4] = "SetSystemIndication";
NabuCasaCommand[NabuCasaCommand["GetConfig"] = 5] = "GetConfig";
NabuCasaCommand[NabuCasaCommand["SetConfig"] = 6] = "SetConfig";
NabuCasaCommand[NabuCasaCommand["GetLEDBinary"] = 7] = "GetLEDBinary";
NabuCasaCommand[NabuCasaCommand["SetLEDBinary"] = 8] = "SetLEDBinary";
})(NabuCasaCommand || (NabuCasaCommand = {}));
export var NabuCasaIndicationSeverity;
(function (NabuCasaIndicationSeverity) {
NabuCasaIndicationSeverity[NabuCasaIndicationSeverity["None"] = 0] = "None";
NabuCasaIndicationSeverity[NabuCasaIndicationSeverity["Warning"] = 1] = "Warning";
NabuCasaIndicationSeverity[NabuCasaIndicationSeverity["Error"] = 2] = "Error";
})(NabuCasaIndicationSeverity || (NabuCasaIndicationSeverity = {}));
export var NabuCasaConfigKey;
(function (NabuCasaConfigKey) {
NabuCasaConfigKey[NabuCasaConfigKey["EnableTiltIndicator"] = 0] = "EnableTiltIndicator";
})(NabuCasaConfigKey || (NabuCasaConfigKey = {}));
const binarySwitchCurrentValue = BinarySwitchCCValues.currentValue.id;
const binarySwitchCurrentValueTranslated = {
...binarySwitchCurrentValue,
commandClassName: getCCName(binarySwitchCurrentValue.commandClass),
propertyName: "Current value",
};
const binarySwitchTargetValue = BinarySwitchCCValues.targetValue.id;
const binarySwitchTargetValueTranslated = {
...binarySwitchTargetValue,
commandClassName: getCCName(binarySwitchTargetValue.commandClass),
propertyName: "Target value",
};
const configEnableTiltIndicator = ConfigurationCCValues.paramInformation(NabuCasaConfigKey.EnableTiltIndicator).id;
const configEnableTiltIndicatorTranslated = {
...configEnableTiltIndicator,
commandClassName: getCCName(configEnableTiltIndicator.commandClass),
propertyName: "enableTiltIndicator",
};
const configEnableTiltIndicatorMeta = {
type: "number",
readable: true,
writeable: true,
min: 0,
max: 1,
allowManualEntry: false,
default: 1,
format: ConfigValueFormat.UnsignedInteger,
label: "Enable Tilt Indicator",
states: {
0: "Disable",
1: "Enable",
},
valueSize: 1,
};
const WHITE = { r: 255, g: 255, b: 255 };
const BLACK = { r: 0, g: 2, b: 0 };
function parseGyro(msg) {
// According to datasheet: 8g range => 977 µg/LSB
const x = roundTo(msg.payload.readInt16BE(1) / 1024 * 9.77, 2);
const y = roundTo(msg.payload.readInt16BE(3) / 1024 * 9.77, 2);
const z = roundTo(msg.payload.readInt16BE(5) / 1024 * 9.77, 2);
return { x, y, z };
}
export class ControllerProprietary_NabuCasa {
constructor(driver, controller) {
this.driver = driver;
this.controller = controller;
}
driver;
controller;
supportedCommands;
async interview() {
const valueDB = this.controller.valueDB;
const supported = await this.getSupportedCommands();
this.supportedCommands = supported;
if (supported.includes(NabuCasaCommand.GetLEDBinary)
|| supported.includes(NabuCasaCommand.GetLED)) {
// Fake a binary switch for the LED
valueDB.setMetadata(BinarySwitchCCValues.currentValue.id, BinarySwitchCCValues.currentValue.meta);
valueDB.setMetadata(BinarySwitchCCValues.targetValue.id, BinarySwitchCCValues.targetValue.meta);
// Clean up RGB values if they exist
valueDB.setMetadata(ColorSwitchCCValues.currentColor.id, undefined);
valueDB.removeValue(ColorSwitchCCValues.currentColor.id);
valueDB.setMetadata(ColorSwitchCCValues.targetColor.id, undefined);
valueDB.removeValue(ColorSwitchCCValues.targetColor.id);
valueDB.setMetadata(ColorSwitchCCValues.hexColor.id, undefined);
valueDB.removeValue(ColorSwitchCCValues.hexColor.id);
valueDB.setMetadata(ColorSwitchCCValues
.currentColorChannel(ColorComponent.Red).id, undefined);
valueDB.removeValue(ColorSwitchCCValues
.currentColorChannel(ColorComponent.Red).id);
valueDB.setMetadata(ColorSwitchCCValues
.currentColorChannel(ColorComponent.Green).id, undefined);
valueDB.removeValue(ColorSwitchCCValues
.currentColorChannel(ColorComponent.Green).id);
valueDB.setMetadata(ColorSwitchCCValues
.currentColorChannel(ColorComponent.Blue).id, undefined);
valueDB.removeValue(ColorSwitchCCValues
.currentColorChannel(ColorComponent.Blue).id);
}
if (supported.includes(NabuCasaCommand.GetLEDBinary)) {
const state = await this.getLEDBinary();
this.persistLEDState(valueDB, state);
}
else if (supported.includes(NabuCasaCommand.GetLED)) {
const rgb = await this.getLED();
this.persistRGBValue(valueDB, rgb);
}
if (supported.includes(NabuCasaCommand.GetConfig)) {
const enableTiltIndicator = await this.getConfig(NabuCasaConfigKey.EnableTiltIndicator);
valueDB.setMetadata(configEnableTiltIndicator, configEnableTiltIndicatorMeta);
valueDB.setValue(configEnableTiltIndicator, enableTiltIndicator);
}
}
persistRGBValue(valueDB, rgb) {
// Treat any other color than black as "on"
valueDB.setValue(BinarySwitchCCValues.currentValue.id, rgb.r > BLACK.r || rgb.g > BLACK.g || rgb.b > BLACK.b);
valueDB.setValue(BinarySwitchCCValues.targetValue.id, rgb.r > BLACK.r || rgb.g > BLACK.g || rgb.b > BLACK.b);
}
persistLEDState(valueDB, state) {
valueDB.setValue(BinarySwitchCCValues.currentValue.id, state);
valueDB.setValue(BinarySwitchCCValues.targetValue.id, state);
}
async getSupportedCommands() {
// HOST->ZW: NABU_CASA_CMD_SUPPORTED
// ZW->HOST: NABU_CASA_CMD_SUPPORTED | supportedBitmask
const getSupportedCmd = new Message({
type: MessageType.Request,
functionType: FUNC_ID_NABUCASA,
payload: Bytes.from([NabuCasaCommand.GetSupportedCommands]),
expectedResponse: (self, msg) => {
return (msg.functionType === FUNC_ID_NABUCASA
&& msg.type === MessageType.Response
&& msg.payload[0] === NabuCasaCommand.GetSupportedCommands);
},
});
const result = await this.driver.sendMessage(getSupportedCmd, {
priority: MessagePriority.Controller,
supportCheck: false,
});
const supported = result.payload.subarray(1);
return parseBitMask(supported, NabuCasaCommand.GetSupportedCommands);
}
async getLED() {
// HOST->ZW: NABU_CASA_LED_GET
// ZW->HOST: NABU_CASA_LED_GET | r | g | b |
const getLEDStateCmd = new Message({
type: MessageType.Request,
functionType: FUNC_ID_NABUCASA,
payload: Bytes.from([NabuCasaCommand.GetLED]),
expectedResponse: (self, msg) => {
return (msg.functionType === FUNC_ID_NABUCASA
&& msg.type === MessageType.Response
&& msg.payload[0] === NabuCasaCommand.GetLED);
},
});
const resultPromise = this.driver.sendMessage(getLEDStateCmd, {
priority: MessagePriority.Controller,
supportCheck: false,
});
const { payload: result } = await resultPromise;
return {
r: result[1],
g: result[2],
b: result[3],
};
}
async setLED(rgb) {
// HOST->ZW: NABU_CASA_LED_SET | r | g | b | [ effect | [ speed ]]
// ZW->HOST: NABU_CASA_LED_SET | success
const payload = Bytes.from([
NabuCasaCommand.SetLED,
rgb.r,
rgb.g,
rgb.b,
1, // SOLID
]);
const setLEDStateCmd = new Message({
type: MessageType.Request,
functionType: FUNC_ID_NABUCASA,
payload,
expectedResponse: (self, msg) => {
return (msg.functionType === FUNC_ID_NABUCASA
&& msg.type === MessageType.Response
&& msg.payload[0] === NabuCasaCommand.SetLED);
},
});
const result = await this.driver.sendMessage(setLEDStateCmd, {
priority: MessagePriority.Controller,
supportCheck: false,
});
const success = !!result.payload[1];
return success;
}
async getLEDBinary() {
// HOST->ZW: NABU_CASA_LED_GET_BINARY
// ZW->HOST: NABU_CASA_LED_GET | state
const getLEDStateCmd = new Message({
type: MessageType.Request,
functionType: FUNC_ID_NABUCASA,
payload: Bytes.from([NabuCasaCommand.GetLEDBinary]),
expectedResponse: (self, msg) => {
return (msg.functionType === FUNC_ID_NABUCASA
&& msg.type === MessageType.Response
&& msg.payload[0] === NabuCasaCommand.GetLEDBinary);
},
});
const resultPromise = this.driver.sendMessage(getLEDStateCmd, {
priority: MessagePriority.Controller,
supportCheck: false,
});
const { payload: result } = await resultPromise;
return !!result[1]; // 0 = off, 1 = on
}
async setLEDBinary(state) {
// HOST->ZW: NABU_CASA_LED_SET | state
// ZW->HOST: NABU_CASA_LED_SET | success
const payload = Bytes.from([
NabuCasaCommand.SetLEDBinary,
state ? 0x01 : 0x00,
]);
const setLEDStateCmd = new Message({
type: MessageType.Request,
functionType: FUNC_ID_NABUCASA,
payload,
expectedResponse: (self, msg) => {
return (msg.functionType === FUNC_ID_NABUCASA
&& msg.type === MessageType.Response
&& msg.payload[0] === NabuCasaCommand.SetLEDBinary);
},
});
const result = await this.driver.sendMessage(setLEDStateCmd, {
priority: MessagePriority.Controller,
supportCheck: false,
});
const success = !!result.payload[1];
return success;
}
async readGyro() {
// HOST->ZW (REQ): NABU_CASA_GYRO_MEASURE
// ZW->HOST (RES): NABU_CASA_GYRO_MEASURE | success
// later
// ZW->HOST (CB): NABU_CASA_GYRO_MEASURE
// | accel_x (MSB) | accel_x (LSB)
// | accel_y (MSB) | accel_y (LSB)
// | accel_z (MSB) | accel_z (LSB)
const readGyroCmd = new Message({
type: MessageType.Request,
functionType: FUNC_ID_NABUCASA,
payload: Bytes.from([NabuCasaCommand.ReadGyro]),
expectedResponse: (self, msg) => {
return (msg.functionType === FUNC_ID_NABUCASA
&& msg.type === MessageType.Response
&& msg.payload[0] === NabuCasaCommand.ReadGyro);
},
});
const callbackPromise = this.driver.waitForMessage((msg) => {
return (msg.functionType === FUNC_ID_NABUCASA
&& msg.type === MessageType.Request
&& msg.payload[0] === NabuCasaCommand.ReadGyro);
}, 1000);
const response = await this.driver.sendMessage(readGyroCmd, {
priority: MessagePriority.Controller,
supportCheck: false,
});
if (!response.payload[0]) {
return;
}
const callback = await callbackPromise;
// According to datasheet: 8g range => 977 µg/LSB
return parseGyro(callback);
}
async setSystemIndication(severity) {
// HOST->ZW: NABU_CASA_SET_SYSTEM_INDICATION | severity
// ZW->HOST: NABU_CASA_SET_SYSTEM_INDICATION | success
const payload = Bytes.from([
NabuCasaCommand.SetSystemIndication,
severity,
]);
const systemIndicationCmd = new Message({
type: MessageType.Request,
functionType: FUNC_ID_NABUCASA,
payload,
expectedResponse: (self, msg) => {
return (msg.functionType === FUNC_ID_NABUCASA
&& msg.type === MessageType.Response
&& msg.payload[0] === NabuCasaCommand.SetSystemIndication);
},
});
const result = await this.driver.sendMessage(systemIndicationCmd, {
priority: MessagePriority.Controller,
supportCheck: false,
});
const success = !!result.payload[1];
return success;
}
async getConfig(key) {
// HOST->ZW (REQ): NABU_CASA_CONFIG_GET | key
// ZW->HOST (RES): NABU_CASA_CONFIG_GET | key | size | value...
const payload = Bytes.from([
NabuCasaCommand.GetConfig,
key,
]);
const getConfigCmd = new Message({
type: MessageType.Request,
functionType: FUNC_ID_NABUCASA,
payload,
expectedResponse: (self, msg) => {
return (msg.functionType === FUNC_ID_NABUCASA
&& msg.type === MessageType.Response
&& msg.payload[0] === NabuCasaCommand.GetConfig
&& msg.payload[1] === key);
},
});
const resultPromise = this.driver.sendMessage(getConfigCmd, {
priority: MessagePriority.Controller,
supportCheck: false,
});
const { payload: result } = await resultPromise;
const size = result[2];
if (result.length > 3 + size) {
// TODO: Decide what to do when the response is invalid
return Number.NEGATIVE_INFINITY;
}
else {
return result.readIntBE(3, size);
}
}
async setConfig(key, value) {
// HOST->ZW (REQ): NABU_CASA_CONFIG_SET | key | size | value...
// ZW->HOST (RES): NABU_CASA_CONFIG_SET | success
const payload = Bytes.from([
NabuCasaCommand.SetConfig,
key,
// For now, only 1 byte is supported
1,
value,
]);
const setConfigCmd = new Message({
type: MessageType.Request,
functionType: FUNC_ID_NABUCASA,
payload,
expectedResponse: (self, msg) => {
return (msg.functionType === FUNC_ID_NABUCASA
&& msg.type === MessageType.Response
&& msg.payload[0] === NabuCasaCommand.SetConfig);
},
});
const result = await this.driver.sendMessage(setConfigCmd, {
priority: MessagePriority.Controller,
supportCheck: false,
});
const success = !!result.payload[1];
return success;
}
getDefinedValueIDs() {
// TODO: Make dynamic
return [
// LED: Binary Switch
binarySwitchCurrentValueTranslated,
binarySwitchTargetValueTranslated,
// Configuration
configEnableTiltIndicatorTranslated,
];
}
async pollValue(valueId) {
if (BinarySwitchCCValues.targetValue.is(valueId)
|| BinarySwitchCCValues.currentValue.is(valueId)) {
if (this.supportedCommands?.includes(NabuCasaCommand.GetLEDBinary)) {
const state = await this.getLEDBinary();
this.persistLEDState(this.controller.valueDB, state);
return state;
}
else {
// The binary LED command is not supported, fall back to the RGB variant
const rgb = await this.getLED();
this.persistRGBValue(this.controller.valueDB, rgb);
return rgb;
}
}
if (ConfigurationCCValues.paramInformation.is(valueId)
&& valueId.propertyKey == undefined) {
switch (valueId.property) {
case NabuCasaConfigKey.EnableTiltIndicator:
// OK
break;
default:
return undefined;
}
const value = await this.getConfig(valueId.property);
if (value != undefined) {
this.controller.valueDB.setValue(valueId, value);
return value;
}
}
}
async setValue(valueId, value) {
if (BinarySwitchCCValues.targetValue.is(valueId)
&& typeof value === "boolean") {
if (this.supportedCommands?.includes(NabuCasaCommand.SetLEDBinary)) {
await this.setLEDBinary(value);
this.persistLEDState(this.controller.valueDB, value);
return { status: SetValueStatus.Success };
}
else {
// The binary LED command is not supported, fall back to the RGB variant
const rgb = value ? WHITE : BLACK;
await this.setLED(rgb);
this.persistRGBValue(this.controller.valueDB, rgb);
return { status: SetValueStatus.Success };
}
}
if (ConfigurationCCValues.paramInformation.is(valueId)
&& valueId.propertyKey == undefined
&& typeof value === "number"
&& [
NabuCasaConfigKey.EnableTiltIndicator,
].includes(valueId.property)) {
await this.setConfig(valueId.property, value);
this.controller.valueDB.setValue(valueId, value);
return { status: SetValueStatus.Success };
}
return { status: SetValueStatus.NoDeviceSupport };
}
// eslint-disable-next-line @typescript-eslint/require-await
async handleUnsolicited(_msg) {
// if (
// msg.functionType === FUNC_ID_NABUCASA
// && msg.type === MessageType.Request
// ) {
// const command = msg.payload[0];
// if (
// command === NabuCasaCommand.ReadGyro && msg.payload.length >= 7
// ) {
// const gyro = parseGyro(msg);
// this.persistGyroValues(this.controller.valueDB, gyro);
// return true;
// }
// }
return false;
}
}
//# sourceMappingURL=NabuCasa.js.map