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.

896 lines (846 loc) 37.1 kB
// 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') const { normalizeAuthFromAccessTokenQuery } = require('./utils/httpAdminAccessToken') // 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) }) RED.httpAdmin.get('/knxUltimateSerialInterfaces', RED.auth.needsPermission('knxUltimate-config.read'), async (req, res) => { try { const list = await KNXClient.listSerialInterfaces() res.json(Array.isArray(list) ? list : []) } catch (error) { try { RED.log.error(`KNXUltimate serial discovery failed: ${error.message}`) } catch (e) { } res.json([]) } }) // 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-12 Download logger XML file configured in a knxUltimateLogger node RED.httpAdmin.get('/knxUltimateLoggerDownload', normalizeAuthFromAccessTokenQuery, RED.auth.needsPermission('knxUltimate-config.read'), (req, res) => { try { const nodeId = (req.query.nodeId || req.query.id || '').toString() if (!nodeId) { res.status(400).json({ error: 'NO_NODE_ID' }) return } const loggerNode = RED.nodes.getNode(nodeId) if (!loggerNode || loggerNode.isLogger !== true) { res.status(404).json({ error: 'LOGGER_NOT_FOUND' }) return } const filePath = (loggerNode.filePath || '').toString() if (!filePath) { res.status(404).json({ error: 'NO_FILE_PATH' }) return } if (!fs.existsSync(filePath)) { res.status(404).json({ error: 'FILE_NOT_FOUND' }) return } const safeName = path.basename(filePath) || 'knx-logger.xml' res.setHeader('Content-Type', 'application/xml; charset=utf-8') res.setHeader('Content-Disposition', `attachment; filename="${safeName}"`) const stream = fs.createReadStream(filePath) stream.on('error', (err) => { try { RED.log.error(`KNXUltimateLoggerDownload error reading file ${filePath}: ${err.message}`) } catch (e) {} if (!res.headersSent) res.status(500).json({ error: 'READ_ERROR' }) }) stream.pipe(res) } catch (error) { try { RED.log.error(`KNXUltimateLoggerDownload error: ${error.message}`) } catch (e) {} if (!res.headersSent) res.status(500).json({ error: 'UNEXPECTED_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://supergiovane.github.io/node-red-contrib-knx-ultimate/wiki/Device' } res.json(jRet) return } jRet = { help: 'NO', helplink: 'https://supergiovane.github.io/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://supergiovane.github.io/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); // }); } }