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 and ETS group address importer. Easy to use and highly configurable.
764 lines (729 loc) • 34.1 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 */ }
(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(() => {
_oClient.setNodeStatusHue({
fill: 'red',
shape: 'ring',
text: 'HUE',
payload: error.message
})
}, 200)
})
}
})()
}, 10000) // 17/02/2020 Do initial read of all nodes requesting initial read
}
})
node.hueManager.on('disconnected', () => {
node.nodeClients.forEach((_oClient) => {
_oClient.setNodeStatusHue({
fill: 'red',
shape: 'ring',
text: 'HUE Disconnected',
payload: ''
})
})
})
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
_node.setNodeStatusHue({
fill: 'green',
shape: 'ring',
text: 'Ready'
})
_node.currentHUEDevice = cloneDeep(oHUEDevice) // Copy by Value and not by ref
if (_node.initializingAtStart === true) {
_node.handleSendHUE(oHUEDevice) // Pass by value
}
}
}
})
} 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')
console.log('getResources plug raw resources', allResources.map((res) => ({ id: res.id, type: res.type, owner: res.owner?.rtype })))
} 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 === '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' }
}
})
}