UNPKG

node-red-contrib-virtual-smart-home

Version:

A Node-RED node that represents a 'virtual device' which can be controlled via Alexa. Requires the virtual smart home skill to be enabled for your Amazon account.

786 lines (697 loc) 16.3 kB
const convert = require('color-convert') //---VALIDATORS--- const wrapValidator = (validatorFn, outputStateKey) => { return (val) => { const validationResult = validatorFn(val) if (!validationResult) { return false } return { key: outputStateKey, value: validationResult.value } } } const booleanValidator = (val) => { const isValid = val === true || val === false if (!isValid) { return false } return { key: 'boolean', value: val } } const powerState = (val) => { val = val.toUpperCase() const isValid = val == 'ON' || val == 'OFF' if (!isValid) { return false } return { key: 'powerState', value: val } } const lockState = (val) => { val = val.toUpperCase() const isValid = val == 'LOCKED' || val == 'UNLOCKED' || val == 'JAMMED' if (!isValid) { return false } return { key: 'lockState', value: val } } const brightness = (val) => { const isValid = Number.isInteger(val) && val >= 0 && val <= 100 if (!isValid) { return false } return { key: 'brightness', value: val } } const detectionState = (val) => { val = val.toUpperCase() const isValid = val == 'DETECTED' || val == 'NOT_DETECTED' if (!isValid) { return false } return { key: 'detectionState', value: val } } const percentage = (val) => { const isValid = Number.isInteger(val) && val >= 0 && val <= 100 if (!isValid) { return false } return { key: 'percentage', value: val } } const speed = (val) => { const isValid = Number.isInteger(val) && val >= 1 && val <= 10 if (!isValid) { return false } return { key: 'speed', value: val } } const channel = (val) => { const isValid = Number.isInteger(val) && val >= 1 && val <= 999 if (!isValid) { return false } return { key: 'channel', value: val, } } const colorTemperatureInKelvin = (val) => { const isValid = Number.isInteger(val) && val >= 1000 && val <= 10000 if (!isValid) { return false } return { key: 'colorTemperatureInKelvin', value: val } } const color = (val) => { if (!typeof val == 'object') { return false } if ( !val.hasOwnProperty('hue') || !val.hasOwnProperty('saturation') || !val.hasOwnProperty('brightness') ) { return false } const isValid = typeof val.hue == 'number' && val.hue >= 0 && val.hue <= 360 && typeof val.saturation == 'number' && val.saturation >= 0 && val.saturation <= 1 && typeof val.brightness == 'number' && val.brightness >= 0 && val.brightness <= 1 if (!isValid) { return false } return { key: 'color', value: { hue: val.hue, saturation: val.saturation, brightness: val.brightness, }, } } const color_rgb = (val) => { if (!typeof val == 'array' || val.length !== 3) { return false } const isValid = Number.isInteger(val[0]) && val[0] >= 0 && val[0] <= 255 && Number.isInteger(val[1]) && val[1] >= 0 && val[1] <= 255 && Number.isInteger(val[2]) && val[2] >= 0 && val[2] <= 255 if (!isValid) { return true } const hsb = convert.rgb.hsv(val) return { key: 'color', value: { hue: hsb[0], saturation: hsb[1] / 100, brightness: hsb[2] / 100, }, } } const color_cmyk = (val) => { if (!typeof val == 'array' || val.length !== 4) { return false } const isValid = Number.isInteger(val[0]) && val[0] >= 0 && val[0] <= 100 && Number.isInteger(val[1]) && val[1] >= 0 && val[1] <= 100 && Number.isInteger(val[2]) && val[2] >= 0 && val[2] <= 100 && Number.isInteger(val[3]) && val[3] >= 0 && val[3] <= 100 if (!isValid) { return true } const hsb = convert.cmyk.hsv(val) return { key: 'color', value: { hue: hsb[0], saturation: hsb[1] / 100, brightness: hsb[2] / 100, }, } } const color_hex = (val) => { const match = `${val}`.match(/^#?([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/) if (!match) { return false } const hsb = convert.hex.hsv(match[1]) return { key: 'color', value: { hue: hsb[0], saturation: hsb[1] / 100, brightness: hsb[2] / 100, }, } } const color_xyz = (val) => { if (!typeof val == 'array' || val.length !== 3) { return false } const isValid = Number.isInteger(val[0]) && val[0] >= 0 && val[0] <= 100 && Number.isInteger(val[1]) && val[1] >= 0 && val[1] <= 100 && Number.isInteger(val[2]) && val[2] >= 0 && val[2] <= 100 if (!isValid) { return false } const hsb = convert.xyz.hsv([val[0], val[1], val[2]]) return { key: 'color', value: { hue: hsb[0], saturation: hsb[1] / 100, brightness: hsb[2] / 100, }, } } const color_lab = (val) => { if (!typeof val == 'array' || val.length !== 3) { return false } const isValid = Number.isInteger(val[0]) && val[0] >= -100 && val[0] <= 100 && Number.isInteger(val[1]) && val[1] >= -100 && val[1] <= 100 && Number.isInteger(val[2]) && val[2] >= -100 && val[2] <= 100 if (!isValid) { return false } const hsb = convert.lab.hsv([val[0], val[1], val[2]]) return { key: 'color', value: { hue: hsb[0], saturation: hsb[1] / 100, brightness: hsb[2] / 100, }, } } const input = (val) => { val = val.toUpperCase() const inputs = [ 'AUX 1', 'AUX 2', 'AUX 3', 'BLURAY', 'CABLE', 'CD', 'COAX 1', 'COAX 2', 'COMPOSITE 1', 'DVD', 'GAME', 'HD RADIO', 'HDMI 1', 'HDMI 2', 'HDMI 3', 'HDMI ARC', 'INPUT 1', 'INPUT 2', 'INPUT 3', 'IPOD', 'LINE 1', 'LINE 2', 'LINE 3', 'MEDIA PLAYER', 'OPTICAL 1', 'OPTICAL 2', 'PHONO', 'PLAYSTATION', 'PLAYSTATION 3', 'PLAYSTATION 4', 'SATELLITE', 'SMARTCAST', 'TUNER', 'TV', 'USB DAC', 'VIDEO 1', 'VIDEO 2', 'VIDEO 3', 'XBOX', ] if (inputs.indexOf(val) == -1) { return false } return { key: 'input', value: val } } const lightMode = (val) => { val = val.toLowerCase() const isValid = val == 'hsb' || val == 'temp' if (!isValid) { return false } return { key: 'lightMode', value: val } } const position = (val) => { val = val.toUpperCase() const acceptableValues = { 'POSITION.UP': 'Position.Up', 'POSITION.DOWN': 'Position.Down', } if (!acceptableValues[val]) { return false } return { key: 'position', value: acceptableValues[val] } } const temperatureValue = (val) => { const floatValue = Number.parseFloat(val) const isValid = floatValue !== NaN if (!isValid) { return false } return { key: 'temperature', value: Math.round(floatValue * 10) / 10 } //23.456789 --> 23.4 } const temperatureScale = (val) => { val = val.toUpperCase() const isValid = val === 'CELSIUS' || val === 'FAHRENHEIT' || val === 'KELVIN' if (!isValid) { return false } return { key: 'scale', value: val } } const thermostatMode = (val) => { val = val.toUpperCase() const isValid = val === 'AUTO' || val === 'HEAT' || val === 'COOL' || val === 'ECO' || val === 'OFF' if (!isValid) { return false } return { key: 'thermostatMode', value: val } } //---DECORATORS--- const diffDecoratorFactory = (anotherDecorator) => { const directiveToAttributesMap = { TurnOn: ['powerState'], TurnOff: ['powerState'], AdjustBrightness: ['brightness'], SetBrightness: ['brightness'], SetColor: [ 'color', 'lightMode', 'color_rgb', 'color_hex', 'color_cmyk', 'color_lab', 'color_xyz', ], SetColorTemperature: ['colorTemperatureInKelvin', 'lightMode'], IncreaseColorTemperature: ['colorTemperatureInKelvin', 'lightMode'], DecreaseColorTemperature: ['colorTemperatureInKelvin', 'lightMode'], SetPercentage: ['percentage'], Lock: ['lockState'], Unlock: ['lockState'], SetMode: ['mode', 'instance'], AdjustRangeValue: ['speed'], SetRangeValue: ['speed'], Activate: ['isActivated'], Deactivate: ['isActivated'], SetTargetTemperature: ['targetTemperature', 'targetScale'], AdjustTargetTemperature: ['targetTemperature', 'targetScale'], } return (decoratorParams) => { const decoratedState = anotherDecorator(decoratorParams) const directive = decoratorParams.localState.directive || 'OverrideLocalState' if (!directiveToAttributesMap[directive]) { return decoratedState } const attribsToKeep = [ 'directive', 'name', 'source', ...directiveToAttributesMap[directive], ] return attribsToKeep.reduce((acc, attrib) => { acc[attrib] = decoratedState[attrib] return acc }, {}) } } const defaultDecorator = ({ localState, template, friendlyName, isPassthrough = false, }) => { localState.name = friendlyName localState.type = template delete localState.template if (isPassthrough) { delete localState.directive } return localState } const colorChangingLightDecorator = ({ localState, template, friendlyName, isPassthrough = false, }) => { localState = defaultDecorator({ localState, template, friendlyName, isPassthrough, }) localState['color_rgb'] = convert.hsv.rgb( localState.color.hue, localState.color.saturation * 100, localState.color.brightness * 100 ) localState['color_hex'] = '#' + convert.hsv.hex( localState.color.hue, localState.color.saturation * 100, localState.color.brightness * 100 ) localState['color_cmyk'] = convert.hsv.cmyk( localState.color.hue, localState.color.saturation * 100, localState.color.brightness * 100 ) localState['color_lab'] = convert.hsv.lab( localState.color.hue, localState.color.saturation * 100, localState.color.brightness * 100 ) localState['color_xyz'] = convert.hsv.xyz( localState.color.hue, localState.color.saturation * 100, localState.color.brightness * 100 ) localState['color_xy'] = (function (red, green, blue) { //apply a gamma correction to the RGB values, which makes the color more vivid and more the like the color displayed on the screen of your device red = red > 0.04045 ? Math.pow((red + 0.055) / (1.0 + 0.055), 2.4) : red / 12.92 green = green > 0.04045 ? Math.pow((green + 0.055) / (1.0 + 0.055), 2.4) : green / 12.92 blue = blue > 0.04045 ? Math.pow((blue + 0.055) / (1.0 + 0.055), 2.4) : blue / 12.92 //RGB values to XYZ using the Wide RGB D65 conversion formula var X = red * 0.664511 + green * 0.154324 + blue * 0.162028 var Y = red * 0.283881 + green * 0.668433 + blue * 0.047685 var Z = red * 0.000088 + green * 0.07231 + blue * 0.986039 //Calculate the xy values from the XYZ values let x = X / (X + Y + Z) let y = Y / (X + Y + Z) x = isNaN(x) ? 0 : x y = isNaN(y) ? 0 : y return [x, y] })( localState['color_rgb'][0], localState['color_rgb'][1], localState['color_rgb'][2] ) return localState } //---TYPES--- const types = { BLINDS: { defaultState: { percentage: 100, }, validators: { percentage, }, decorator: defaultDecorator, }, COLOR_CHANGING_LIGHT_BULB: { defaultState: { powerState: 'OFF', brightness: 100, colorTemperatureInKelvin: 2200, lightMode: 'temp', color: { hue: 60, saturation: 1, brightness: 1 }, }, validators: { powerState, brightness, colorTemperatureInKelvin, color_hex: wrapValidator(color_hex, 'color'), color_rgb: wrapValidator(color_rgb, 'color'), color_cmyk: wrapValidator(color_cmyk, 'color'), color_lab: wrapValidator(color_lab, 'color'), color_xyz: wrapValidator(color_xyz, 'color'), color, lightMode, }, decorator: colorChangingLightDecorator, }, CONTACT_SENSOR: { defaultState: { detectionState: 'NOT_DETECTED', }, validators: { detectionState, }, decorator: defaultDecorator, }, DIMMABLE_LIGHT_BULB: { defaultState: { powerState: 'OFF', brightness: 100, }, validators: { powerState, brightness, }, decorator: defaultDecorator, }, DIMMER_SWITCH: { defaultState: { powerState: 'OFF', brightness: 100, }, validators: { powerState, brightness, }, decorator: defaultDecorator, }, DOORBELL_EVENT_SOURCE: { defaultState: { detectionState: 'NOT_DETECTED', }, validators: { detectionState, }, decorator: defaultDecorator, }, ENTERTAINMENT_DEVICE: { defaultState: { powerState: 'OFF', input: 'TV', channel: 1, volume: 50, muted: false, }, validators: { powerState, input, channel, volume: wrapValidator(percentage, 'volume'), muted: wrapValidator(booleanValidator, 'muted'), }, decorator: defaultDecorator, }, FAN: { defaultState: { powerState: 'OFF', speed: 1, }, validators: { powerState, speed, }, decorator: defaultDecorator, }, GARAGE_DOOR_OPENER: { defaultState: { mode: 'Position.Up', instance: 'GarageDoor.Position', }, validators: { mode: wrapValidator(position, 'mode'), }, decorator: defaultDecorator, }, LOCK: { defaultState: { lockState: 'UNLOCKED', }, validators: { lockState, }, decorator: defaultDecorator, }, MOTION_SENSOR: { defaultState: { detectionState: 'NOT_DETECTED', }, validators: { detectionState, }, decorator: defaultDecorator, }, PLUG: { defaultState: { powerState: 'OFF', }, validators: { powerState, }, decorator: defaultDecorator, }, SCENE: { defaultState: { isActivated: false, }, validators: {}, decorator: defaultDecorator, }, SWITCH: { defaultState: { powerState: 'OFF', }, validators: { powerState, }, decorator: defaultDecorator, }, TEMPERATURE_SENSOR: { defaultState: { temperature: 0, scale: 'CELSIUS', }, validators: { temperature: temperatureValue, scale: temperatureScale, }, decorator: defaultDecorator, }, THERMOSTAT: { defaultState: { temperature: 0, scale: 'CELSIUS', targetTemperature: 0, targetScale: 'CELSIUS', thermostatMode: 'OFF', powerState: 'OFF', }, validators: { temperature: temperatureValue, scale: temperatureScale, thermostatMode, targetTemperature: wrapValidator(temperatureValue, 'targetTemperature'), targetScale: wrapValidator(temperatureScale, 'targetScale'), powerState, }, decorator: defaultDecorator, }, THERMOSTAT_2: { defaultState: { temperature: 0, scale: 'CELSIUS', lowerSetpoint: 0, upperSetpoint: 0, lowerSetpointScale: 'CELSIUS', upperSetpointScale: 'CELSIUS', thermostatMode: 'OFF', powerState: 'OFF', }, validators: { temperature: temperatureValue, scale: temperatureScale, thermostatMode, lowerSetpoint: wrapValidator(temperatureValue, 'lowerSetpoint'), upperSetpoint: wrapValidator(temperatureValue, 'upperSetpoint'), lowerSetpointScale: wrapValidator(temperatureScale, 'lowerSetpointScale'), upperSetpointScale: wrapValidator(temperatureScale, 'upperSetpointScale'), powerState, }, decorator: defaultDecorator, }, } //---HELPERS--- function getValidators(template) { return types[template].validators } function getDecorator(template, isDiffEnabled) { const decorator = types[template].decorator if (isDiffEnabled) { return diffDecoratorFactory(decorator) } else { return decorator } } function getDefaultState(template) { const defaultState = types[template].defaultState defaultState['template'] = template defaultState['source'] = 'alexa' return defaultState } module.exports = { getValidators, getDecorator, getDefaultState, }