UNPKG

node-red-contrib-knx-ultimate

Version:

Control your KNX and KNX Secure intallation via Node-Red! A bunch of KNX nodes, with integrated Philips HUE control, ETS group address importer, KNX AI for diagnosticsand KNX routing between interfaces. Easy to use and highly configurable.

814 lines (778 loc) 36.9 kB
/* eslint-disable camelcase */ /* eslint-disable no-underscore-dangle */ /* eslint-disable no-lonely-if */ /* eslint-disable no-param-reassign */ /* eslint-disable no-inner-declarations */ /* eslint-disable max-len */ const cloneDeep = require('lodash/cloneDeep') // const classHUE = require("./utils/hueEngine").classHUE; const hueColorConverter = require('./utils/colorManipulators/hueColorConverter') // 10/09/2024 Setup the color logger loggerSetup = (options) => { const clog = require('node-color-log').createNamedLogger(options.setPrefix) clog.setLevel(options.loglevel) clog.setDate(() => (new Date()).toLocaleString()) return clog } module.exports = (RED) => { function hueConfig (config) { RED.nodes.createNode(this, config) const node = this node.host = config.host node.nodeClients = [] // Stores the registered clients node.loglevel = config.loglevel !== undefined ? config.loglevel : 'error' // loglevel doesn'e exists yet node.sysLogger = null node.hueAllResources = undefined node.timerHUEConfigCheckState = null // Timer that check the connection to the hue bridge every xx seconds node.pendingHueResourceReload = null node.activeHueIdentifySessions = new Map() try { node.sysLogger = loggerSetup({ loglevel: node.loglevel, setPrefix: 'hue-config.js' }) } catch (error) { console.log(error.stack) } node.name = config.name === undefined || config.name === '' ? node.host : config.name // Helper not to write everytime the "node.hueManager === null || node.hueManager === "undefined" || node.hueManager.HUEBridgeConnectionStatus === undefined" Object.defineProperty(node, 'linkStatus', { get: function () { return node.hueManager?.HUEBridgeConnectionStatus ?? 'disconnected' } }) // Connect to Bridge and get the resources const HUE_IDENTIFY_DEFAULT_INTERVAL_MS = 2000 const HUE_IDENTIFY_MAX_DURATION_MS = 600000 node.isHueIdentifySessionActive = (sessionKey) => { if (!sessionKey) return false return node.activeHueIdentifySessions.has(sessionKey) } node.stopHueIdentifySession = (sessionKey, reason = 'manual') => { if (!sessionKey) return false const session = node.activeHueIdentifySessions.get(sessionKey) if (!session) return false try { if (session.intervalHandle) { clearInterval(session.intervalHandle) } if (session.timeoutHandle) { clearTimeout(session.timeoutHandle) } node.activeHueIdentifySessions.delete(sessionKey) session.intervalHandle = null session.timeoutHandle = null if (typeof session.onStop === 'function') { try { session.onStop(reason) } catch (error) { node.sysLogger?.warn(`Hue identify session stop callback error: ${error.message}`) } } return true } catch (error) { node.sysLogger?.warn(`Hue identify session stop error: ${error.message}`) return false } } node.stopAllHueIdentifySessions = (reason = 'manual') => { const keys = Array.from(node.activeHueIdentifySessions.keys()) keys.forEach((key) => { node.stopHueIdentifySession(key, reason) }) } node.startHueIdentifySession = async ({ sessionKey, targets, intervalMs = HUE_IDENTIFY_DEFAULT_INTERVAL_MS, maxDurationMs = HUE_IDENTIFY_MAX_DURATION_MS }) => { if (!sessionKey) return false if (!Array.isArray(targets) || targets.length === 0) return false node.stopHueIdentifySession(sessionKey, 'restart') const resolvedInterval = HUE_IDENTIFY_DEFAULT_INTERVAL_MS const resolvedDuration = HUE_IDENTIFY_MAX_DURATION_MS const session = { sessionKey, targets, intervalMs: resolvedInterval, maxDurationMs: resolvedDuration, startedAt: Date.now(), inFlight: false, intervalHandle: null, timeoutHandle: null, onStop: (reason) => { node.sysLogger?.debug(`Hue identify session ${sessionKey} stopped (${reason})`) } } session.runIdentify = async (label = 'interval') => { if (session.inFlight) return if (!node.hueManager || !node.hueManager.hueApiV2 || typeof node.hueManager.hueApiV2.put !== 'function') return if (node.linkStatus !== 'connected') return session.inFlight = true try { for (const target of session.targets) { if (!target || !target.id || !target.type) continue try { await node.hueManager.hueApiV2.put(`/resource/${target.type}/${target.id}`, { identify: { action: 'identify' } }) } catch (error) { node.sysLogger?.warn(`Hue identify ${target.type}:${target.id} failed (${label}): ${error.message}`) } } } finally { session.inFlight = false } } session.intervalHandle = setInterval(() => { session.runIdentify('interval') }, resolvedInterval) session.timeoutHandle = setTimeout(() => { node.stopHueIdentifySession(sessionKey, 'timeout') }, resolvedDuration) node.activeHueIdentifySessions.set(sessionKey, session) await session.runIdentify('initial') return true } node.initHUEConnection = async () => { await node.closeConnection() try { if (node.hueManager !== undefined) await node.hueManager.close() } catch (error) { /* empty */ } const safeClientCall = (client, fn, label) => { try { if (!client || typeof fn !== 'function') return fn() } catch (error) { node.sysLogger?.warn(`Hue client ${label} error: ${error.message}`) } } (async () => { try { const { classHUE } = await import('./utils/hueEngine.mjs') node.hueManager = new classHUE(node.host, node.credentials.username, node.credentials.clientkey, config.bridgeid, node.sysLogger) } catch (error) { node.sysLogger?.error(`Errore hue-config: node.initHUEConnection: ${error.message}`) throw (error) } node.hueManager.on('event', (_event) => { node.nodeClients.forEach((_oClient) => { const oClient = _oClient try { if (oClient.handleSendHUE !== undefined) oClient.handleSendHUE(_event) } catch (error) { node.sysLogger?.error(`Errore node.hueManager.on(event): ${error.message}`) } }) }) // Connected node.hueManager.on('connected', () => { if (node.linkStatus === 'disconnected') { // Start the timer to do initial read. if (node.timerDoInitialRead !== null) clearTimeout(node.timerDoInitialRead) node.timerDoInitialRead = setTimeout(() => { (async () => { try { node.sysLogger?.info(`HTTP getting resource from HUE bridge : ${node.name}`) await node.loadResourcesFromHUEBridge() node.sysLogger?.info(`Total HUE resources count : ${node.hueAllResources.length}`) } catch (error) { node.nodeClients.forEach((_oClient) => { setTimeout(() => { safeClientCall(_oClient, () => _oClient.setNodeStatusHue({ fill: 'red', shape: 'ring', text: 'HUE', payload: error.message }), 'setNodeStatusHue') }, 200) }) } })() }, 10000) // 17/02/2020 Do initial read of all nodes requesting initial read } }) node.hueManager.on('disconnected', () => { node.nodeClients.forEach((_oClient) => { safeClientCall(_oClient, () => _oClient.setNodeStatusHue({ fill: 'red', shape: 'ring', text: 'HUE Disconnected', payload: '' }), 'setNodeStatusHue') }) }) try { await node.hueManager.Connect() } catch (error) { } })() } node.startWatchdogTimer = async () => { if (node.timerHUEConfigCheckState !== null) clearTimeout(node.timerHUEConfigCheckState) node.timerHUEConfigCheckState = setTimeout(() => { (async () => { if (node.linkStatus === 'disconnected') { // The hueEngine is already connected to the HUE Bridge try { await node.initHUEConnection() } catch (error) { node.sysLogger?.error(`Errore hue-config: node.startWatchdogTimer: ${error.message}`) } } await node.startWatchdogTimer() })() }, 60000) } setTimeout(() => { (async () => { await node.initHUEConnection() node.startWatchdogTimer() })() }, 5000) // Functions called from the nodes ---------------------------------------------------------------- // Query the HUE Bridge to return the resources node.loadResourcesFromHUEBridge = async () => { if (node.linkStatus === 'disconnected') return // (async () => { // °°°°°° Load ALL resources try { node.hueAllResources = await node.hueManager.hueApiV2.get('/resource') if (node.hueAllResources !== undefined) { node.hueAllRooms = node.hueAllResources.filter((a) => a.type === 'room') // Update all KNX State of the nodes with the new hue device values node.nodeClients.forEach((_node) => { if (_node.hueDevice !== undefined && node.hueAllResources !== undefined) { const oHUEDevice = node.hueAllResources.filter((a) => a.id === _node.hueDevice)[0] if (oHUEDevice !== undefined) { // Add _Node to the clients array try { _node.setNodeStatusHue({ fill: 'green', shape: 'ring', text: 'Ready' }) } catch (error) { node.sysLogger?.warn(`KNXUltimateHue: loadResources setNodeStatusHue error ${error.message}`) } try { _node.currentHUEDevice = cloneDeep(oHUEDevice) // Copy by Value and not by ref } catch (error) { /* empty */ } if (_node.initializingAtStart === true) { try { _node.handleSendHUE(oHUEDevice) // Pass by value } catch (error) { node.sysLogger?.warn(`KNXUltimateHue: loadResources handleSendHUE error ${error.message}`) } } } } }) } else { // The config node cannot read the resources. Signalling disconnected } } catch (error) { if (this.sysLogger !== undefined && this.sysLogger !== null) { this.sysLogger.error(`KNXUltimatehueEngine: loadResourcesFromHUEBridge: ${error.message}`) throw (error) } } // })(); } node.getFirstLightInGroup = function getFirstLightInGroup (_groupID) { if (node.hueAllResources === undefined || node.hueAllResources === null) return try { const group = node.hueAllResources.filter((a) => a.id === _groupID)[0] const owner = node.hueAllResources.filter((a) => a.id === group.owner.rid)[0] if (owner.children !== undefined) { const dev = node.hueAllResources.filter((a) => a.id === owner.children[0].rid)[0] if (dev.type === 'device' && dev.services !== undefined) { const lightID = dev.services.filter((a) => a.rtype === 'light')[0].rid const oLight = node.hueAllResources.filter((a) => a.id === lightID)[0] return oLight } else if (dev.type === 'light') { return dev } } } catch (error) { } } // Return an array of light belonging to the groupID node.getAllLightsBelongingToTheGroup = async function getAllLightsBelongingToTheGroup (_groupID, refreshResourcesFromBridge = true) { if (node.hueAllResources === undefined || node.hueAllResources === null) return const retArr = [] let filteredResource try { if (refreshResourcesFromBridge === true) { await node.loadResourcesFromHUEBridge() } // filteredResource = node.hueAllResources.filter((a) => a.id === _groupID); // if (filteredResource[0].type === "grouped_light") { // filteredResource = node.hueAllResources.filter((a) => a.services); // filteredResource = filteredResource.filter((a) => a.services).filter((b) => b.type === "light"); // if (filteredResource.length > 0) { // console.log(filteredResource) // } // } node.hueAllResources.forEach((res) => { if (res.services !== undefined && res.services.length > 0) { res.services.forEach((serv) => { if (serv.rid === _groupID) { if (res.children !== undefined) { const children = res.children.filter((a) => a.rtype === 'light') for (let index = 0; index < children.length; index++) { const element = children[index] const oLight = node.hueAllResources.filter((a) => a.id === element.rid) // if (oLight !== null && oLight !== undefined) retArr.push({ groupID: _groupID, light: oLight[0] }); if (oLight !== null && oLight !== undefined) retArr.push(oLight[0]) } } } }) } }) return retArr } catch (error) { /* empty */ } } // Returns the cached devices (node.hueAllResources) by type. node.getResources = async function getResources (_rtype, { forceRefresh = false } = {}) { try { if (forceRefresh) { try { await node.loadResourcesFromHUEBridge() } catch (error) { node.sysLogger?.warn(`KNXUltimateHue: getResources force refresh failed ${error.message}`) } } if (node.hueAllResources === undefined) return // Returns capitalized string function capStr (s) { if (typeof s !== 'string') return '' return s.charAt(0).toUpperCase() + s.slice(1) } const retArray = [] let allResources if (_rtype === 'light' || _rtype === 'grouped_light') { allResources = node.hueAllResources.filter((a) => a.type === 'light' || a.type === 'grouped_light') } else if (_rtype === 'plug') { allResources = node.hueAllResources.filter((a) => a.type === 'plug' || a.type === 'smartplug' || a.type === 'smart_plug') } else if (_rtype === 'area_motion') { allResources = node.hueAllResources.filter((a) => a.type === 'convenience_area_motion' || a.type === 'security_area_motion') } else { allResources = node.hueAllResources.filter((a) => a.type === _rtype) } if (allResources === null) return for (let index = 0; index < allResources.length; index++) { const resource = allResources[index] // Get the owner try { let resourceName = '' let sType = '' let sArchetype = '' if (_rtype === 'light' || _rtype === 'grouped_light') { // It's a service, having a owner const owners = node.hueAllResources.filter((a) => a.id === resource.owner.rid) if (owners !== null) { for (let index = 0; index < owners.length; index++) { const owner = owners[index] if (owner.type === 'bridge_home') { resourceName += 'ALL GROUPS and ' } else { resourceName += `${owner.metadata.name} and ` sArchetype += `${owner.metadata.archetype === undefined ? '' : owner.metadata.archetype} and ` // const room = node.hueAllRooms.find((child) => child.children.find((a) => a.rid === owner.id)); // sRoom += room !== undefined ? `${room.metadata.name} + ` : " + "; sType += `${capStr(owner.type)} + ` } } } sType = sType.slice(0, -' + '.length) if (sArchetype !== '') sArchetype = sArchetype.slice(0, -' and '.length) resourceName = resourceName.slice(0, -' and '.length) resourceName += sType !== '' ? ` (${sType}:${sArchetype})` : '' retArray.push({ name: `${capStr(resource.type)}: ${resourceName}`, id: resource.id, deviceObject: resource }) } if (_rtype === 'scene') { resourceName = resource.metadata.name || '**Name Not Found**' // Get the linked zone const zone = node.hueAllResources.find((res) => res.id === resource.group.rid) resourceName += ` - ${capStr(resource.group.rtype)}: ${zone.metadata.name}` retArray.push({ name: `${capStr(_rtype)}: ${resourceName}`, id: resource.id }) } if (_rtype === 'button') { const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' const controlID = resource.metadata !== undefined ? resource.metadata.control_id || '' : '' retArray.push({ name: `${capStr(_rtype)}: ${linkedDevName}, button ${controlID}`, id: resource.id }) } if (_rtype === 'motion' || _rtype === 'camera_motion') { const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `${capStr(_rtype)}: ${linkedDevName}`, id: resource.id }) } if (_rtype === 'convenience_area_motion' || _rtype === 'security_area_motion' || (_rtype === 'area_motion' && (resource.type === 'convenience_area_motion' || resource.type === 'security_area_motion'))) { let areaName = '' let areaGroupLabel = '' const isConvenience = resource.type === 'convenience_area_motion' const areaTypeLabel = isConvenience ? 'Convenience area' : 'Security area' let configuration = node.hueAllResources.find((res) => { if (res.type !== 'motion_area_configuration' || !Array.isArray(res.services)) return false return res.services.some((svc) => svc.rid === resource.id) }) if (!configuration && resource.owner && resource.owner.rid) { const candidate = node.hueAllResources.find((res) => res.id === resource.owner.rid) if (candidate && candidate.type === 'motion_area_configuration') configuration = candidate } if (configuration && typeof configuration.name === 'string' && configuration.name.trim() !== '') { areaName = configuration.name.trim() } else { areaName = resource.id } if (configuration && configuration.group && configuration.group.rid) { const groupRes = node.hueAllResources.find((res) => res.id === configuration.group.rid) if (groupRes && groupRes.metadata && typeof groupRes.metadata.name === 'string') { const groupType = configuration.group.rtype ? capStr(configuration.group.rtype) : 'Group' areaGroupLabel = `, ${groupType} ${groupRes.metadata.name}` } } retArray.push({ name: `${areaTypeLabel}: ${areaName}${areaGroupLabel}`, id: resource.id }) } if (_rtype === 'relative_rotary') { const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `Rotary: ${linkedDevName}`, id: resource.id }) } if (_rtype === 'light_level') { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)) const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `Light Level: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ''}`, id: resource.id }) } if (_rtype === 'plug') { const linkedDevice = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)) const room = node.hueAllRooms?.find((roomItem) => roomItem.children?.find((child) => child.rid === linkedDevice?.id)) const plugName = linkedDevice?.metadata?.name || resource.metadata?.name || 'Unnamed Plug' const stateLabel = resource?.on?.on === true ? 'on' : 'off' console.log('getResources plug direct resource', { id: resource.id, type: resource.type, name: plugName, linkedDevice: linkedDevice?.id }) retArray.push({ name: `Plug: ${plugName}${room !== undefined ? `, room ${room.metadata.name}` : ''} [${stateLabel}]`, id: resource.id, type: resource.type, deviceObject: resource }) } if (_rtype === 'temperature') { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)) const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `Temperature: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ''}`, id: resource.id }) } if (_rtype === 'humidity') { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)) const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `Humidity: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ''}`, id: resource.id }) } if (_rtype === 'device_power') { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)) const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `Battery: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ''}`, id: resource.id }) } if (_rtype === 'zigbee_connectivity') { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)) const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `Zigbee Connectivity: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ''}`, id: resource.id }) // Get zigbee_connectivituy // const bridgeId = node.hueAllResources.filter((a) => a.bridge_id === config.bridgeid).owner.rid; // const zigbee_ConnectivityID = node.hueAllResources.filter((a) => a.id === bridgeId).services.filter((a) => a.rtype === "zigbee_connectivity").rid; // // connected, disconnected, connectivity_issue, unidirectional_incoming // const oZigbeeConnectivityStatus = node.hueAllResources.filter((a) => a.id === zigbee_ConnectivityID).status; // const zigbee = node.hueAllResources.filter((a) => a.services !== undefined).find((a) => a.services.rtype === "zigbee_connectivity"); // const devs = zigbee.filter((a) => a.rtype === "zigbee_connectivity"); } if (_rtype === 'contact') { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)) const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `Contact: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ''}`, id: resource.id }) } if (_rtype === 'device_software_update') { const Room = node.hueAllRooms.find((room) => room.children.find((child) => child.rid === resource.owner.rid)) const linkedDevName = node.hueAllResources.find((dev) => dev.type === 'device' && dev.services.find((serv) => serv.rid === resource.id)).metadata.name || '' retArray.push({ name: `Software status: ${linkedDevName}${Room !== undefined ? `, room ${Room.metadata.name}` : ''}`, id: resource.id }) } } catch (error) { console.log('KNXHue-config: getResources error ', error.trace) retArray.push({ name: `${_rtype}: ERROR ${error.message}`, id: resource.id }) } } if (_rtype === 'plug' && retArray.length === 0) { const plugDevices = node.hueAllResources.filter((dev) => { if (dev.type !== 'device' || !Array.isArray(dev.services)) return false const archetypePlug = dev.product_data?.product_archetype === 'plug' || dev.metadata?.archetype === 'plug' || /plug/i.test(dev.product_data?.product_name || '') || /plug/i.test(dev.metadata?.name || '') const hasService = dev.services.some((serv) => ['plug', 'smartplug', 'smart_plug', 'light'].includes(serv.rtype || '')) return archetypePlug && hasService }) plugDevices.forEach((device) => { try { const plugService = device.services.find((serv) => ['plug', 'smartplug', 'smart_plug', 'light'].includes(serv.rtype || '')) if (!plugService) return const plugResource = node.hueAllResources.find((res) => res.id === plugService.rid) || {} const room = node.hueAllRooms?.find((roomItem) => roomItem.children?.find((child) => child.rid === device.id)) const plugName = device.metadata?.name || plugResource.metadata?.name || 'Unnamed Plug' const stateLabel = plugResource?.on?.on === true ? 'on' : (plugResource?.on?.on === false ? 'off' : '') retArray.push({ name: `Plug: ${plugName}${room !== undefined ? `, room ${room.metadata.name}` : ''}${stateLabel ? ` [${stateLabel}]` : ''}`, id: plugService.rid || device.id, type: plugService.rtype || plugResource.type || 'light', deviceObject: plugResource.on ? plugResource : { id: plugService.rid || device.id, type: plugService.rtype || plugResource.type || 'light', on: plugResource.on, owner: { rid: device.id, rtype: 'device' } } }) } catch (err) { node.sysLogger?.warn(`KNXUltimateHue: getResources plug fallback error ${err.message}`) } }) } node.sysLogger?.debug(`getResources plug returning ${retArray.length}`) return { devices: retArray } } catch (error) { node.sysLogger?.error(`KNXUltimateHue: hueEngine: classHUE: getResources: error ${error.message}`) return { devices: error.message } } } // Get current color in HEX (used in html) node.getColorFromHueLight = (_lightId) => { try { const oLight = node.hueAllResources.filter((a) => a.id === _lightId)[0] const retRGB = hueColorConverter.ColorConverter.xyBriToRgb(oLight.color.xy.x, oLight.color.xy.y, oLight.dimming.brightness) const ret = '#' + hueColorConverter.ColorConverter.rgbHex(retRGB.r, retRGB.g, retRGB.b).toString() return ret } catch (error) { node.sysLogger?.warn(`KNXUltimateHue: hueEngine: getColorFromHueLight: error ${error.message}`) return {} } } // Get current Kelvin (used in html) node.getKelvinFromHueLight = (_lightId) => { try { const oLight = node.hueAllResources.filter((a) => a.id === _lightId)[0] const ret = { kelvin: hueColorConverter.ColorConverter.mirekToKelvin(oLight.color_temperature.mirek), brightness: Math.round(oLight.dimming.brightness, 0) } return JSON.stringify(ret) } catch (error) { node.sysLogger?.error(`KNXUltimateHue: hueEngine: getKelvinFromHueLight: error ${error.message}`) return {} } } /** * Get average color XY from a light array * @param {array} _arrayLights - Light array * @returns { x,y,mirek,brightness } - Object containing all infos */ node.getAverageColorsXYBrightnessAndTemperature = async function getAverageColorsXYBrightnessAndTemperature (_arrayLights) { let x; let y; let mirek; let brightness let countColor = 0; let countColor_Temperature = 0; let countDimming = 0 _arrayLights.forEach((element) => { if (element.color !== undefined && element.color.xy !== undefined) { if (x === undefined) { x = 0; y = 0 } x += element.color.xy.x y += element.color.xy.y countColor += 1 } if (element.color_temperature !== undefined && element.color_temperature.mirek !== undefined) { if (mirek === undefined) mirek = 0 mirek += element.color_temperature.mirek countColor_Temperature += 1 } if (element.dimming !== undefined && element.dimming.brightness !== undefined) { if (brightness === undefined) brightness = 0 brightness += element.dimming.brightness countDimming += 1 } }) // Calculate and return the averages const retX = countColor === 0 ? undefined : x / countColor const retY = countColor === 0 ? undefined : y / countColor const retMirek = countColor_Temperature === 0 ? undefined : mirek / countColor_Temperature const retBrightness = countDimming === 0 ? undefined : brightness / countDimming return { x: retX, y: retY, mirek: retMirek, brightness: retBrightness } } node.refreshHueResources = async (reason = '') => { if (node.linkStatus === 'disconnected') return if (node.pendingHueResourceReload) { try { await node.pendingHueResourceReload } catch (error) { node.sysLogger?.warn(`KNXUltimateHue: refreshHueResources pending error ${error.message}`) } return } node.pendingHueResourceReload = (async () => { try { if (reason) { node.sysLogger?.debug(`KNXUltimateHue: refreshHueResources triggered (${reason})`) } await node.loadResourcesFromHUEBridge() } finally { node.pendingHueResourceReload = null } })() try { await node.pendingHueResourceReload } catch (error) { node.sysLogger?.warn(`KNXUltimateHue: refreshHueResources error ${error.message}`) } } node.getHueResourceSnapshot = async (resourceId, { forceRefresh = false } = {}) => { if (!resourceId) return undefined const lookup = () => { if (!Array.isArray(node.hueAllResources)) return undefined return node.hueAllResources.find((res) => res.id === resourceId) } let resource = lookup() if (resource && !forceRefresh) return cloneDeep(resource) if (node.linkStatus === 'disconnected') return resource ? cloneDeep(resource) : undefined await node.refreshHueResources(forceRefresh ? `force refresh for ${resourceId}` : `ensure resource ${resourceId}`) resource = lookup() return resource ? cloneDeep(resource) : undefined } // END functions called from the nodes ---------------------------------------------------------------- node.addClient = (_Node) => { // Update the node hue device, as soon as a node register itself to hue-config nodeClients if (node.nodeClients.filter((x) => x.id === _Node.id).length !== 0) return node.nodeClients.push(_Node) const applyHueSnapshot = (resource) => { if (!resource) return false try { const snapshot = cloneDeep(resource) _Node.currentHUEDevice = snapshot if (_Node.initializingAtStart === true) { try { _Node.handleSendHUE(snapshot) } catch (error) { node.sysLogger?.warn(`KNXUltimateHue: addClient handleSendHUE error ${error.message}`) } } _Node.setNodeStatusHue({ fill: 'green', shape: 'dot', text: "I'm new and ready." }) return true } catch (error) { node.sysLogger?.warn(`KNXUltimateHue: addClient applyHueSnapshot error ${error.message}`) return false } } const existingResource = Array.isArray(node.hueAllResources) ? node.hueAllResources.find((a) => a.id === _Node.hueDevice) : undefined if (existingResource && applyHueSnapshot(existingResource)) return if (node.linkStatus !== 'connected') { _Node.setNodeStatusHue({ fill: 'grey', shape: 'ring', text: 'Waiting for connection' }) return } _Node.setNodeStatusHue({ fill: 'yellow', shape: 'ring', text: 'Syncing with HUE bridge', payload: '' }); (async () => { try { const refreshed = await node.getHueResourceSnapshot(_Node.hueDevice, { forceRefresh: true }) if (!applyHueSnapshot(refreshed)) { _Node.setNodeStatusHue({ fill: 'red', shape: 'ring', text: 'Hue device info unavailable', payload: '' }) } } catch (error) { node.sysLogger?.warn(`KNXUltimateHue: addClient async fetch error ${error.message}`) _Node.setNodeStatusHue({ fill: 'red', shape: 'ring', text: 'Hue device sync failed', payload: '' }) } })() } node.removeClient = (_Node) => { // Remove the client node from the clients array try { node.nodeClients = node.nodeClients.filter((x) => x.id !== _Node.id) } catch (error) { /* empty */ } } node.closeConnection = async () => { node.stopAllHueIdentifySessions('connection-close') node.hueManager?.removeAllListeners() node.linkStatus === 'disconnected' } node.on('close', (done) => { try { node.sysLogger = null node.nodeClients = [] node.closeConnection() node.stopAllHueIdentifySessions('node-close'); (async () => { try { await node.hueManager.close() node.hueManager = null delete node.hueManager done() } catch (error) { done() } })() } catch (error) { done() } }) } RED.nodes.registerType('hue-config', hueConfig, { credentials: { username: { type: 'password' }, clientkey: { type: 'password' } } }) }