UNPKG

zwave-js

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

451 lines 19.2 kB
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