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.

202 lines (165 loc) 5.34 kB
const merge = require('deepmerge') const deepEql = require('deep-eql') const { getValidators, getDecorator, getDefaultState, } = require('./device-types') module.exports = function (RED) { function VirtualDeviceNode(config) { RED.nodes.createNode(this, config) const that = this const deviceId = 'vshd-' + that.id.replace('.', '') const getConnectionNode = () => { return RED.nodes.getNode(config.connection) } const validators = getValidators(config.template) const decorator = getDecorator(config.template, config.diff) let isActive = false // default logger. Will be overridden by connection node if available let logger = (logMessage, variable = undefined, logLevel = 'log') => { if (variable) { logMessage = logMessage + ': ' + JSON.stringify(variable) } that[logLevel](logMessage) } const getLocalState = () => { let contextState = that.context().get('state') if (!contextState) { contextState = getDefaultState(config.template) } return { ...contextState } } const setLocalState = (targetState) => { const oldLocalState = getLocalState() let newLocalState = merge(oldLocalState, targetState) that.context().set('state', newLocalState) return newLocalState } const emitLocalState = ({ topic = null, rawDirective = null }) => { let payload payload = decorator({ localState: getLocalState(), template: config.template, friendlyName: config.name, }) let metadata try { metadata = JSON.parse(config.metadata ?? '{}') } catch (e) { metadata = {} } if (Object.keys(payload).length > 0) { const msg = { topic: topic ? topic : config.topic, metadata, payload, } if (rawDirective) { payload['rawDirective'] = rawDirective } that.send(msg) } } const validateState = (state) => { const approvedState = {} for (const key in state) { if (validators[key]) { let validatorResult = validators[key](state[key]) if (false !== validatorResult) { approvedState[validatorResult.key] = validatorResult.value } } } return approvedState } if (getConnectionNode()) { //connection is configured logger = getConnectionNode().getLogger() //register callbacks. This way connectionNode can communicate with us: getConnectionNode().registerChildNode(deviceId, { setStatus: (status, force = false) => { if (isActive || force) { that.status(status) } }, setActive: (isActiveToggle) => { isActive = isActiveToggle }, isActive: () => isActive, getLocalState, setLocalState, emitLocalState, getDeviceConfig: () => { return { friendlyName: config.name || config.template.toLowerCase(), template: config.template, retrievable: config.retrievable, } }, }) } that.on('input', function (msg, send, done) { if (!getConnectionNode() || !isActive) { logger(`ignoring inbound msg for non-active device ID ${deviceId}'`) if (done) { done() } return } const ignoreBecauseOfNameFilter = config.filter && msg.payload.name !== config.name if (ignoreBecauseOfNameFilter) { if (done) { done() } logger( `ignoring inbound msg for device ID ${deviceId} because msg.payload.name (${ msg.payload.name ? `'${msg.payload.name}'` : '<undefined>' }) does not match '${config.name}'` ) return } const ignoreBecauseOfTopicFilter = config.filterTopic && msg.topic !== config.topic if (ignoreBecauseOfTopicFilter) { if (done) { done() } logger( `ignoring inbound msg for device ID ${deviceId} because msg.topic (${ msg.topic ? `'${msg.topic}'` : '<undefined>' }) does not match '${config.topic}'` ) return } const oldLocalState = getLocalState() const approvedState = validateState(msg.payload) approvedState['directive'] = 'OverrideLocalState' const mergedState = merge(oldLocalState, approvedState) const newLocalState = { ...mergedState, source: 'device' } const confirmedNewLocalState = setLocalState(newLocalState) if (!deepEql(oldLocalState, newLocalState)) { getConnectionNode().handleLocalDeviceStateChange({ deviceId, oldState: oldLocalState, newState: confirmedNewLocalState, }) } if (config.passthrough && Object.keys(approvedState).length > 0) { emitLocalState({ topic: msg.topic }) } if (done) { done() } }) that.on('close', async function (_removed, done) { if (getConnectionNode()) { await getConnectionNode().unregisterChildNode(deviceId) } that.status({}) return done() }) } RED.nodes.registerType('vsh-virtual-device', VirtualDeviceNode) }