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
JavaScript
/* 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' }
}
})
}