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.
848 lines (800 loc) • 34.9 kB
JavaScript
// Utility function
// until node-red 3.1.0, there is a bug creating a plugin, so for backward compatibility, i must use a JS as a node.
const oOS = require('os')
const fs = require('fs')
const path = require('path')
const yaml = require('js-yaml')
const dptlib = require('knxultimate').dptlib
const customHTTP = require('./utils/http')
const KNXClient = require('knxultimate').KNXClient
const sysLogger = require('./utils/sysLogger')
// DATAPONT MANIPULATION HELPERS
// ####################
const sortBy = (field) => (a, b) => {
if (a[field] > b[field]) {
return 1
} else {
return -1
}
}
const onlyDptKeys = (kv) => {
return kv[0].startsWith('DPT')
}
const extractBaseNo = (kv) => {
return {
subtypes: kv[1].subtypes,
base: parseInt(kv[1].id.replace('DPT', ''))
}
}
const convertSubtype = (baseType) => (kv) => {
const value = `${baseType.base}.${kv[0]}`
// let sRet = value + " " + kv[1].name + (kv[1].unit === undefined ? "" : " (" + kv[1].unit + ")");
const sRet = value + ' ' + kv[1].name
return {
value,
text: sRet
}
}
const toConcattedSubtypes = (acc, baseType) => {
const subtypes = Object.entries(baseType.subtypes).sort(sortBy(0)).map(convertSubtype(baseType))
return acc.concat(subtypes)
}
// ####################
module.exports = (RED) => {
RED.plugins.registerPlugin('commonFunctions', {
type: 'foo',
onadd: function () {
RED.events.on('registry:plugin-added', function (id) {
// console.log(`my-test-plugin: plugin-added event "${id}"`)
commonFunctions()
})
}
})
function commonFunctions () {
const node = this
const ensureBuffer = (rawValue) => {
if (!rawValue) return null
if (Buffer.isBuffer(rawValue)) return rawValue
if (Array.isArray(rawValue)) return Buffer.from(rawValue)
if (typeof rawValue === 'object' && Array.isArray(rawValue.data)) return Buffer.from(rawValue.data)
if (rawValue.type === 'Buffer' && Array.isArray(rawValue.data)) return Buffer.from(rawValue.data)
return null
}
const guessDptFromRawValue = (rawBuffer) => {
if (!rawBuffer || !rawBuffer.length) return null
if (rawBuffer.length === 1) {
if (rawBuffer[0] === 0 || rawBuffer[0] === 1) return '1.001'
return '5.001'
}
if (rawBuffer.length === 4) return '14.056'
if (rawBuffer.length === 2) return '9.001'
if (rawBuffer.length === 3) return '11.001'
if (rawBuffer.length === 14) return '16.001'
const dpts = Object.entries(dptlib).filter(onlyDptKeys).map(extractBaseNo).sort(sortBy('base')).reduce(toConcattedSubtypes, [])
for (let index = 0; index < dpts.length; index++) {
try {
const resolved = dptlib.resolve(dpts[index].value)
if (!resolved) continue
const jsValue = dptlib.fromBuffer(rawBuffer, resolved)
if (typeof jsValue !== 'undefined') return dpts[index].value
} catch (error) { /* empty */ }
}
return null
}
const formatDisplayValue = (value) => {
if (value === null || value === undefined) return ''
if (value instanceof Date) return value.toISOString()
if (typeof value === 'object') {
try {
return JSON.stringify(value)
} catch (error) {
return String(value)
}
}
if (typeof value === 'boolean') return value ? 'true' : 'false'
return String(value)
}
// // Gather infos about all interfaces on the lan and provides a static variable utils.aDiscoveredknxGateways
// try {
// require('./utils/utils').DiscoverKNXGateways()
// } catch (error) {
// }
// 11/03/2020 Delete scene saved file, from html
RED.httpAdmin.get('/knxultimateCheckHueConnected', (req, res) => {
try {
const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
if (!serverId) {
res.json({ ready: false })
return
}
if (serverId.hueAllResources === null || serverId.hueAllResources === undefined) {
(async function main () {
try {
if (typeof serverId.loadResourcesFromHUEBridge === 'function') {
await serverId.loadResourcesFromHUEBridge()
}
} catch (error) {
RED.log.error(`Errore RED.httpAdmin.get('/knxultimateCheckHueConnected' ${error.stack}`)
}
res.json({ ready: false })
}()).catch()
} else {
res.json({ ready: true })
}
} catch (error) {
RED.log.error(`Errore RED.httpAdmin.get('/knxultimateCheckHueConnected' ${error.stack}`)
res.json({ ready: false })
}
})
// 11/03/2020 Delete scene saved file, from html
RED.httpAdmin.get('/knxultimatescenecontrollerdelete'), (req, res) => {
// Delete the file
try {
const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
const newPath = `${serverId.userDir}/scenecontroller/SceneController_${req.query.FileName}`
fs.unlinkSync(newPath)
} catch (error) { if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.warn(`e ${error}`) }
res.json({ status: 220 })
}
// // Find all HUE Bridges in the network
// RED.httpAdmin.get('/KNXUltimateDiscoverHueBridges'), (req, res) => {
// const url = 'https://discovery.meethue.com'; // Use HUE broker server discover process by visiting
// async function fetchData() {
// try {
// const response = await fetch(url); // Effettua la richiesta
// const dataArray = await response.json(); // Parsing dei dati JSON
// // Mostra l'array risultante
// res.json(dataArray);
// } catch (error) {
// if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`Error fetching discovery.meethue.com ${error.stack}`);
// res.json("");
// }
// }
// fetchData();
// };
// Find all HUE Bridges in the network
RED.httpAdmin.get('/KNXUltimateGetHueBridgeInfo', RED.auth.needsPermission('hue-config.read'), (req, res) => {
async function fetchData () {
try {
const response = await customHTTP.getBridgeDetails(req.query.IP)
// Mostra l'array risultante
res.json(response)
} catch (error) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`Error fetching discovery.meethue.com ${error.stack}`)
res.json({ error: error.message })
}
}
fetchData()
})
// Find all HUE Bridges in the network
RED.httpAdmin.get('/KNXUltimateGetPlainHueBridgeCredentials', RED.auth.needsPermission('hue-config.read'), (req, res) => {
try {
const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
const username = serverId.credentials.username
const clientkey = serverId.credentials.clientkey
res.json({ username, clientkey })
} catch (error) {
res.json({ error: error.message })
}
})
// Endpoint for registering with the HUE Bridge
RED.httpAdmin.get('/KNXUltimateRegisterToHueBridge', (req, res) => {
(async () => {
try {
const configNode = RED.nodes.getNode(req.query.serverId)
const ipAddress = req.query.IP
if (!ipAddress) throw new Error('Bridge IP address is required.')
const registration = await customHTTP.registerBridgeUser(ipAddress, 'KNXUltimate', 'Node-RED')
const bridgeInfo = {
data: registration.bridge,
name: registration.bridge?.name || configNode?.name || 'Hue Bridge',
ipaddress: registration.bridge?.ipaddress || ipAddress,
bridgeid: registration.bridge?.bridgeid || configNode?.bridgeid || ''
}
if (configNode) {
try {
configNode.credentials = configNode.credentials || {}
configNode.credentials.username = registration.user.username
configNode.credentials.clientkey = registration.user.clientkey
if (typeof bridgeInfo.bridgeid === 'string' && bridgeInfo.bridgeid) {
try { configNode.bridgeid = bridgeInfo.bridgeid } catch (e) { /* noop */ }
}
} catch (credError) {
if (node.sysLogger) node.sysLogger.warn(`Hue registration: unable to persist credentials for node ${configNode.id}: ${credError.message}`)
}
}
res.json({ bridge: bridgeInfo, user: registration.user })
} catch (error) {
if (node.sysLogger) node.sysLogger.error(`Hue bridge registration failed: ${error.message}`)
res.json({ error: error.message })
}
})()
})
RED.httpAdmin.get('/KNXUltimateDiscoverHueBridges', RED.auth.needsPermission('hue-config.read'), (req, res) => {
customHTTP.discoverHueBridges().then((list) => {
res.json(Array.isArray(list) ? list : [])
}).catch((error) => {
if (node.sysLogger) node.sysLogger.error(`Hue bridge discovery failed: ${error.message}`)
res.json({ error: error.message })
})
})
// Endpoint for reading csv/esf by the other nodes
RED.httpAdmin.get('/knxUltimatecsv', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
try {
if (typeof req.query.nodeID !== 'undefined' && req.query.nodeID !== null && req.query.nodeID !== '') {
const _node = RED.nodes.getNode(req.query.nodeID) // Retrieve node.id of the config node.
if (_node !== null) res.json(RED.nodes.getNode(_node.id).csv)
} else {
// Get the first knxultimate-config having a valid csv
try {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.info('KNXUltimate-config: Requested csv maybe from visu-ultimate?')
RED.nodes.eachNode((_node) => {
if (_node.hasOwnProperty('csv') && _node.type === 'knxUltimate-config' && _node.csv !== '') {
res.json(RED.nodes.getNode(_node.id).csv)
}
})
} catch (error) { }
}
} catch (error) {
}
})
// 14/08/2019 Endpoint for retrieving the ethernet interfaces
RED.httpAdmin.get('/knxUltimateETHInterfaces', (req, res) => {
const jListInterfaces = []
try {
const oiFaces = oOS.networkInterfaces()
Object.keys(oiFaces).forEach((ifname) => {
const ifaceEntries = Array.isArray(oiFaces[ifname]) ? oiFaces[ifname] : []
const externalEntries = ifaceEntries.filter((iface) => iface && iface.internal === false)
if (externalEntries.length === 0) return
const addresses = externalEntries.map((iface) => ({
address: iface.address,
family: iface.family,
netmask: iface.netmask,
cidr: iface.cidr || null
}))
const displayAddress = addresses.map((entry) => entry.address).join(', ')
jListInterfaces.push({
name: ifname,
address: displayAddress,
addresses
})
})
} catch (error) { }
res.json(jListInterfaces)
})
// Discover KNX/IP gateways on demand and return cached results
RED.httpAdmin.get('/knxUltimateDiscoverKNXGateways', RED.auth.needsPermission('knxUltimate-config.read'), async function (req, res) {
try {
const utils = require('./utils/utils')
// Always trigger discovery on request to ensure fresh data
const list = await utils.DiscoverKNXGateways()
res.json(Array.isArray(list) ? list : [])
} catch (error) {
try { RED.log.error(`KNX gateway discovery failed: ${error.message}`) } catch (e) { /* noop */ }
res.json([])
}
})
// 12/08/2021 Endpoint for deleting the GA persistent file for the current gateway
RED.httpAdmin.get('/deletePersistGAFile', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
try {
if (typeof req.query.serverId !== 'undefined' && req.query.serverId !== null && req.query.serverId !== '') {
try {
const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
const sFile = path.join(serverId.userDir, 'knxpersistvalues', `knxpersist${req.query.serverId}.json`)
fs.unlinkSync(sFile)
} catch (error) { res.json({ error: error.stack }) }
res.json({ error: 'No error' })
} else {
res.json({ error: 'No serverId specified' })
}
} catch (error) {
}
})
// 2025-09 List interfaces (IA) from KNX Secure keyring
RED.httpAdmin.get('/knxUltimateKeyringInterfaces', RED.auth.needsPermission('knxUltimate-config.read'), async (req, res) => {
try {
let keyringContent = (req.query.keyring || '').toString()
let password = (req.query.pwd || '').toString()
// If not provided, try to read from existing config node
if ((!keyringContent || !password) && req.query.serverId) {
const cfg = RED.nodes.getNode(req.query.serverId)
if (cfg) {
try { keyringContent = cfg.keyringFileXML || keyringContent } catch (e) { }
try { password = (cfg.credentials && cfg.credentials.keyringFilePassword) ? cfg.credentials.keyringFilePassword : password } catch (e) { }
}
}
if (!keyringContent || !password) {
return res.json([])
}
let Keyring
try {
({ Keyring } = require('knxultimate/build/secure/keyring'))
} catch (e) {
try { RED.log.error(`KNXUltimate: cannot load Keyring module: ${e.message}`) } catch (err) { }
return res.json([])
}
const kr = new Keyring()
try {
await kr.load(keyringContent, password)
} catch (e) {
try { RED.log.error(`KNXUltimate: keyring load error: ${e.message}`) } catch (err) { }
return res.json([])
}
const out = []
try {
for (const [iaStr, iface] of kr.getInterfaces()) {
out.push({ ia: iaStr, userId: iface?.userId })
}
} catch (e) { }
res.json(out)
} catch (error) {
try { RED.log.error(`KNXUltimate: knxUltimateKeyringInterfaces error: ${error.message}`) } catch (e) { }
res.json([])
}
})
RED.httpAdmin.get('/knxUltimateGetHueColor', (req, res) => {
try {
const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
// find wether the light is a light or is grouped_light
let hexColor
const _oDevice = serverId.hueAllResources.filter((a) => a.id === req.query.id)[0]
if (_oDevice.type === 'light') {
hexColor = serverId.getColorFromHueLight(req.query.id)
} else {
// grouped_light, get the first light in the group
const oLight = serverId.getFirstLightInGroup(_oDevice.id)
hexColor = serverId.getColorFromHueLight(oLight.id)
}
res.json(hexColor !== undefined ? hexColor : 'Select the device first!')
} catch (error) {
res.json('Select the device first!')
}
})
// 2025-09 Secure: return list of Data Secure Group Addresses from keyring
RED.httpAdmin.get('/knxUltimateKeyringDataSecureGAs', RED.auth.needsPermission('knxUltimate-config.read'), async (req, res) => {
try {
let keyringContent = (req.query.keyring || '').toString()
let password = (req.query.pwd || '').toString()
// Try to use config node if not provided
if ((!keyringContent || !password) && req.query.serverId) {
const cfg = RED.nodes.getNode(req.query.serverId)
if (cfg) {
try { keyringContent = cfg.keyringFileXML || keyringContent } catch (e) { }
try { password = (cfg.credentials && cfg.credentials.keyringFilePassword) ? cfg.credentials.keyringFilePassword : password } catch (e) { }
}
}
if (!keyringContent || !password) return res.json([])
let Keyring
try { ({ Keyring } = require('knxultimate/build/secure/keyring')) } catch (e) { return res.json([]) }
const kr = new Keyring()
try { await kr.load(keyringContent, password) } catch (e) { return res.json([]) }
const out = []
try {
for (const [gaStr, g] of kr.getGroupAddresses()) {
if (g?.decryptedKey && g.decryptedKey.length > 0) out.push(gaStr)
}
} catch (e) { }
res.json(out)
} catch (error) {
try { RED.log.error(`KNXUltimate: knxUltimateKeyringDataSecureGAs error: ${error.message}`) } catch (e) { }
res.json([])
}
})
RED.httpAdmin.get('/knxUltimateGetKelvinColor', (req, res) => {
try {
// find wether the light is a light or is grouped_light
const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
let kelvinValue
const _oDevice = serverId.hueAllResources.filter((a) => a.id === req.query.id)[0]
if (_oDevice.type === 'light') {
kelvinValue = serverId.getKelvinFromHueLight(req.query.id)
} else {
// grouped_light, get the first light in the group
const oLight = serverId.getFirstLightInGroup(_oDevice.id)
kelvinValue = serverId.getKelvinFromHueLight(oLight.id)
}
res.json(kelvinValue !== undefined ? kelvinValue : 'Select the device first!')
} catch (error) {
res.json('Select the device first!')
}
})
RED.httpAdmin.get('/knxUltimateGetLightObject', (req, res) => {
try {
const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
if (serverId.hueAllResources === null || serverId.hueAllResources === undefined) {
throw (new Error('Resource not yet loaded'))
}
const _lightId = req.query.id
const oLight = serverId.hueAllResources.filter((a) => a.id === _lightId)[0]
// Infer some useful info, so the HTML part can avoid to query the server
// Kelvin
try {
if (oLight.color_temperature !== undefined && oLight.color_temperature.mirek !== undefined) {
oLight.calculatedKelvin = hueColorConverter.ColorConverter.mirekToKelvin(oLight.color_temperature.mirek)
}
} catch (error) {
oLight.calculatedKelvin = undefined
}
// HEX value from XYBri
try {
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()}`
oLight.calculatedHEXColor = ret
} catch (error) {
oLight.calculatedHEXColor = undefined
}
res.json(oLight)
} catch (error) {
if (node.sysLogger !== undefined && node.sysLogger !== null) node.sysLogger.error(`KNXUltimateHue: hueEngine: knxUltimateGetLightObject: error ${error.message}.`)
res.json({})
}
})
RED.httpAdmin.get('/knxUltimateMonitor', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
try {
const server = RED.nodes.getNode(req.query.serverId)
if (!server) {
res.json({ items: [], error: 'NO_SERVER' })
return
}
const items = Array.isArray(server.exposedGAs) ? server.exposedGAs.map((entry) => {
const rawBuffer = ensureBuffer(entry?.rawValue)
const rawHex = rawBuffer ? rawBuffer.toString('hex') : ''
let dpt = entry?.dpt
let guessed = false
if ((!dpt || dpt === '') && rawBuffer) {
const inferred = guessDptFromRawValue(rawBuffer)
if (inferred) {
dpt = inferred
guessed = true
}
}
let value = null
if (rawBuffer && dpt) {
try {
const resolved = dptlib.resolve(dpt)
if (resolved) value = dptlib.fromBuffer(rawBuffer, resolved)
} catch (error) { /* empty */ }
}
return {
ga: entry?.ga || '',
devicename: entry?.devicename || '',
dpt: dpt || '',
dptGuessed: guessed,
rawHex,
value,
valueText: formatDisplayValue(value),
updatedAt: entry?.updatedAt || null
}
}).sort((a, b) => a.ga.localeCompare(b.ga)) : []
res.json({ items })
} catch (error) {
try { RED.log.error(`KNXUltimate: knxUltimateMonitor error: ${error.message}`) } catch (e) { }
res.json({ items: [], error: error.message })
}
})
RED.httpAdmin.post('/knxUltimateMonitorToggle', RED.auth.needsPermission('knxUltimate-config.write'), (req, res) => {
try {
const serverId = req.body?.serverId
const ga = req.body?.ga
const server = serverId ? RED.nodes.getNode(serverId) : null
if (!server) {
res.json({ status: 'error', error: 'NO_SERVER' })
return
}
if (!ga) {
res.json({ status: 'error', error: 'NO_GA' })
return
}
const entry = Array.isArray(server.exposedGAs) ? server.exposedGAs.find((item) => item.ga === ga) : undefined
if (!entry) {
res.json({ status: 'error', error: 'GA_NOT_FOUND' })
return
}
const rawBuffer = ensureBuffer(entry.rawValue)
let dpt = entry.dpt
if (!rawBuffer) {
res.json({ status: 'error', error: 'NO_CURRENT_VALUE' })
return
}
if (!dpt) {
const inferred = guessDptFromRawValue(rawBuffer)
if (inferred) dpt = inferred
}
let currentValue = null
if (dpt) {
try {
const resolved = dptlib.resolve(dpt)
if (resolved) currentValue = dptlib.fromBuffer(rawBuffer, resolved)
} catch (error) { currentValue = null }
}
if (typeof currentValue !== 'boolean') {
res.json({ status: 'error', error: 'NOT_BOOLEAN' })
return
}
const nextValue = !currentValue
const sendDpt = dpt || '1.001'
try {
server.sendKNXTelegramToKNXEngine({
grpaddr: ga,
payload: nextValue,
dpt: sendDpt,
outputtype: 'write',
nodecallerid: 'knxUltimateMonitor'
})
} catch (error) {
res.json({ status: 'error', error: error.message })
return
}
res.json({ status: 'ok', value: nextValue })
} catch (error) {
try { RED.log.error(`KNXUltimate: knxUltimateMonitorToggle error: ${error.message}`) } catch (e) { }
res.json({ status: 'error', error: error.message })
}
})
RED.httpAdmin.get('/knxUltimateDebugLog', RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => {
try {
const rawSince = req.query?.sinceSeq ?? req.query?.since ?? req.query?.after ?? null
let sinceSeq = null
if (rawSince !== null && rawSince !== undefined) {
const parsed = Number(rawSince)
if (!Number.isNaN(parsed) && Number.isFinite(parsed)) {
sinceSeq = Math.floor(parsed)
}
}
const snapshot = sinceSeq === null ? sysLogger.getDebugSnapshot() : sysLogger.getDebugSnapshot({ sinceSeq })
res.json({
entries: snapshot.entries,
latestSeq: snapshot.latestSeq,
total: snapshot.total,
limit: snapshot.limit
})
} catch (error) {
try { RED.log.error(`KNXUltimate: knxUltimateDebugLog error: ${error.message}`) } catch (e) { }
res.json({ entries: [], error: error.message })
}
})
RED.httpAdmin.post('/KNXUltimateLocateHueDevice', async (req, res) => {
const respondError = (status, message) => {
res.status(status).json({ error: message })
}
try {
const rawServerId = req.body?.serverId
const serverId = typeof rawServerId === 'string' ? rawServerId.trim() : (rawServerId ? String(rawServerId).trim() : '')
if (!serverId) {
respondError(400, 'Hue bridge not specified')
return
}
const hueServer = RED.nodes.getNode(serverId)
if (!hueServer) {
respondError(404, 'Hue bridge not found')
return
}
if (!hueServer.hueManager || !hueServer.hueManager.hueApiV2 || typeof hueServer.hueManager.hueApiV2.put !== 'function') {
respondError(503, 'Hue bridge not ready')
return
}
if (hueServer.linkStatus !== 'connected') {
respondError(503, 'Hue bridge is not connected')
return
}
const rawDeviceId = req.body?.deviceId
const deviceId = typeof rawDeviceId === 'string' ? rawDeviceId.trim() : (rawDeviceId ? String(rawDeviceId).trim() : '')
if (!deviceId) {
respondError(400, 'Hue device not specified')
return
}
const rawDeviceType = req.body?.deviceType
const deviceType = typeof rawDeviceType === 'string' ? rawDeviceType.trim().toLowerCase() : (rawDeviceType ? String(rawDeviceType).trim().toLowerCase() : '')
let resourceSnapshot = null
if (typeof hueServer.getHueResourceSnapshot === 'function') {
try {
resourceSnapshot = await hueServer.getHueResourceSnapshot(deviceId, { forceRefresh: false })
} catch (error) {
resourceSnapshot = null
}
}
const resolvedType = (resourceSnapshot?.type || deviceType || 'light').toLowerCase()
const targets = []
const addTarget = (id, type) => {
if (!id || !type) return
const trimmedId = typeof id === 'string' ? id.trim() : String(id).trim()
const trimmedType = typeof type === 'string' ? type.trim().toLowerCase() : String(type).trim().toLowerCase()
if (trimmedId === '' || trimmedType === '') return
targets.push({ id: trimmedId, type: trimmedType })
}
if (resolvedType === 'grouped_light') {
let lights = []
if (typeof hueServer.getAllLightsBelongingToTheGroup === 'function') {
try {
lights = await hueServer.getAllLightsBelongingToTheGroup(deviceId)
} catch (error) {
lights = []
}
}
if (Array.isArray(lights) && lights.length > 0) {
lights.forEach((lightResource) => {
const ownerId = lightResource?.owner?.rid
if (ownerId) {
addTarget(ownerId, 'device')
} else if (lightResource?.id) {
addTarget(lightResource.id, 'light')
}
})
}
if (targets.length === 0 && typeof hueServer.getFirstLightInGroup === 'function') {
const firstLight = hueServer.getFirstLightInGroup(deviceId)
const ownerId = firstLight?.owner?.rid
if (ownerId) {
addTarget(ownerId, 'device')
} else if (firstLight?.id) {
addTarget(firstLight.id, 'light')
}
}
} else if (resolvedType === 'device') {
addTarget(deviceId, 'device')
} else {
const ownerId = resourceSnapshot?.owner?.rid
if (ownerId) {
addTarget(ownerId, 'device')
} else {
addTarget(deviceId, resolvedType || 'light')
}
}
const uniqueTargets = []
const seenTargets = new Set()
targets.forEach((target) => {
const key = `${target.type}:${target.id}`
if (!seenTargets.has(key)) {
seenTargets.add(key)
uniqueTargets.push(target)
}
})
if (uniqueTargets.length === 0) {
respondError(404, 'Hue device resource unavailable')
return
}
const sessionKey = `identify:${deviceId}`
const maxDurationMs = 600000
const intervalMs = 1000
const rawAction = (req.body?.action || '').toString().trim().toLowerCase()
const explicitAction = rawAction === 'start' || rawAction === 'stop' ? rawAction : 'toggle'
const stopIdentifySession = () => {
if (typeof hueServer.isHueIdentifySessionActive === 'function' && hueServer.isHueIdentifySessionActive(sessionKey)) {
if (typeof hueServer.stopHueIdentifySession === 'function') {
hueServer.stopHueIdentifySession(sessionKey, 'manual')
}
return true
}
return false
}
if (explicitAction === 'stop') {
const wasActive = stopIdentifySession()
res.json({ status: 'stopped', wasActive })
return
}
if (explicitAction !== 'start') {
if (stopIdentifySession()) {
res.json({ status: 'stopped', wasActive: true })
return
}
}
if (explicitAction === 'start' && typeof hueServer.isHueIdentifySessionActive === 'function' && hueServer.isHueIdentifySessionActive(sessionKey)) {
res.json({ status: 'started', alreadyActive: true, expiresInMs: maxDurationMs })
return
}
if (typeof hueServer.startHueIdentifySession === 'function') {
const started = await hueServer.startHueIdentifySession({
sessionKey,
targets: uniqueTargets,
intervalMs,
maxDurationMs
})
if (!started) {
respondError(500, 'Unable to start locate session')
return
}
res.json({ status: 'started', expiresInMs: maxDurationMs })
return
}
const identifyPayload = { identify: { action: 'identify' } }
for (const target of uniqueTargets) {
await hueServer.hueManager.hueApiV2.put(`/resource/${target.type}/${target.id}`, identifyPayload)
}
res.json({ status: 'started', expiresInMs: 0 })
} catch (error) {
try { RED.log.error(`KNXUltimate LocateHueDevice error: ${error.message}`) } catch (err) { }
res.status(500).json({ error: error.message })
}
})
RED.httpAdmin.get('/KNXUltimateGetResourcesHUE', async (req, res) => {
try {
// °°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°
const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
if (serverId === null) {
RED.log.warn('Warn KNXUltimateGetResourcesHUE serverId is null')
const jRet = []
jRet.push({ name: 'PLEASE DEPLOY FIRST: then try again.', id: 'error' })
res.json({ devices: jRet })
return
}
const refreshFlag = (req.query.forceRefresh || '').toString().toLowerCase()
const forceRefresh = refreshFlag === '1' || refreshFlag === 'true' || refreshFlag === 'yes'
const jRet = await serverId.getResources(req.query.rtype, { forceRefresh })
if (jRet !== undefined) {
res.json(jRet)
} else {
res.json({ devices: [{ name: "I'm still connecting...Try in some seconds" }] })
}
// °°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°°
} catch (error) {
// RED.log.error(`Errore KNXUltimateGetResourcesHUE non gestito ${error.message}`);
res.json({ devices: error.message })
RED.log.error(`Err KNXUltimateGetResourcesHUE: ${error.message}`)
// (async () => {
// await node.initHUEConnection();
// })();
}
})
RED.httpAdmin.get('/knxUltimateDpts', (req, res) => {
try {
const dpts = Object.entries(dptlib.dpts).filter(onlyDptKeys).map(extractBaseNo).sort(sortBy('base'))
.reduce(toConcattedSubtypes, [])
res.json(dpts)
} catch (error) { }
})
// 15/09/2020 Supergiovane, read datapoint help usage
RED.httpAdmin.get('/knxUltimateDptsGetHelp', (req, res) => {
try {
const serverId = RED.nodes.getNode(req.query.serverId) // Retrieve node.id of the config node.
const sDPT = req.query.dpt.split('.')[0] // Takes only the main type
let jRet
if (sDPT === '0') {
// Special fake datapoint, meaning "Universal Mode"
jRet = {
help: `// KNX-Ultimate set as UNIVERSAL NODE
// Example of a function that sends a message to the KNX-Ultimate
msg.destination = "0/0/1"; // Set the destination
msg.payload = false; // issues a write or response (based on the options Telegram type above) to the KNX bus
msg.event = "GroupValue_Write"; // "GroupValue_Write" or "GroupValue_Response", overrides the option Telegram type above.
msg.dpt = "1.001"; // for example "1.001", overrides the Datapoint option. (Datapoints can be sent as 9 , "9" , "9.001" or "DPT9.001")
return msg;`,
helplink: 'https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki'
}
res.json(jRet)
return
}
jRet = {
help: 'NO',
helplink: 'https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki/-SamplesHome'
}
const dpts = Object.entries(dptlib.dpts).filter(onlyDptKeys)
for (let index = 0; index < dpts.length; index++) {
if (dpts[index][0].toUpperCase() === `DPT${sDPT}`) {
jRet = {
help: dpts[index][1].basetype.hasOwnProperty('help') ? dpts[index][1].basetype.help : 'NO',
helplink: dpts[index][1].basetype.hasOwnProperty('helplink')
? dpts[index][1].basetype.helplink
: 'https://github.com/Supergiovane/node-red-contrib-knx-ultimate/wiki/-SamplesHome'
}
break
}
}
res.json(jRet)
} catch (error) {
res.json({ error: error.stack })
}
})
// RED.httpAdmin.post("/banana", RED.auth.needsPermission("write"), (req, res) => {
// const node = RED.nodes.getNode(req.params.id);
// if (node != null) {
// try {
// if (req.body) {
// console.log(body);
// }
// } catch (err) { }
// }
// res.json(req.body);
// });
}
}