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, and KNX routing between interfaces. Easy to use and highly configurable.

292 lines (265 loc) 9.32 kB
const simpleget = require('simple-get') const DEFAULT_TIMEOUT_MS = 30000 /** * Parameters. * * @param config.key the credentualskey. * @param config.prefix the bridge's URL * @param config.url the resource url * @param config.timeoutMs optional request timeout */ module.exports.use = (config) => { const http = {} const logError = (message) => { try { const logger = config?.logger || config?.sysLogger if (logger?.error) logger.error(message) else console.error(message) } catch (error) { try { console.error(message) } catch (err) { /* empty */ } } } /** * Make a http call. * * @param opt Used as options for simple get. */ http.call = async (opt) => { return new Promise((resolve, reject) => { const requestOptions = { ...opt } requestOptions.rejectUnauthorized = false requestOptions.timeout = Number.isFinite(config?.timeoutMs) && config.timeoutMs > 0 ? config.timeoutMs : DEFAULT_TIMEOUT_MS if (config?.agent) requestOptions.agent = config.agent requestOptions.headers = { ...(opt.headers || {}), 'hue-application-key': config.key } if (requestOptions.body && !requestOptions.headers['Content-Type'] && !requestOptions.headers['content-type']) { requestOptions.headers['Content-Type'] = 'application/json' } requestOptions.url = config.prefix + requestOptions.url // log.trace('http ' + opt.method + ' ' + opt.url); const req = simpleget.concat(requestOptions, (err, res, data) => { if (err) return reject(err) const statusCode = res?.statusCode ?? 0 const statusMessage = res?.statusMessage || '' const responseText = Buffer.isBuffer(data) ? data.toString('utf8') : String(data ?? '') if (statusCode >= 100 && statusCode < 400) { let result try { result = JSON.parse(responseText) } catch (error) { logError(`utils.http: Invalid JSON response from ${requestOptions.url}: ${error.message}`) return reject(new Error(`Invalid JSON response for ${requestOptions.url}: ${error.message}`)) } if (result?.errors && Array.isArray(result.errors) && result.errors.length > 0) { return reject(new Error('The response for ' + requestOptions.url + ' returned errors ' + JSON.stringify(result.errors))) } if (!Object.prototype.hasOwnProperty.call(result || {}, 'data')) { return reject(new Error('Unexpected result with no data. ' + JSON.stringify(result))) } return resolve(result.data) } logError(`utils.http: Error response from ${requestOptions.url}: ${statusCode} ${statusMessage}`) const bodySnippet = responseText ? ` ${responseText}` : '' return reject(new Error(`Error response for ${requestOptions.url} with status ${statusCode} ${statusMessage}${bodySnippet}`)) }) // Ensure timeouts fire even if the underlying client ignores the option. try { if (requestOptions.timeout && typeof req?.setTimeout === 'function') { req.setTimeout(requestOptions.timeout) } } catch (error) { /* empty */ } }) } /** * Make a POST call. * * @param url The target url. * @param data The body data. */ http.post = async (url, data) => { return await http.call({ method: 'POST', url, body: JSON.stringify(data) }) } /** * Makes a put call. * * @param url The target url. */ http.put = async (url, data) => { return await http.call({ method: 'PUT', url, body: JSON.stringify(data) }) } /** * Makes a get call. * * @param url The target url. */ http.get = async (url) => { return await http.call({ method: 'GET', url }) } /** * Makes a delete call. * * @param url The target url. */ http.delete = async (url) => { return await http.call({ method: 'DELETE', url }) } return http } /** * Get Bridgedetails * * @param _ip The target ip. */ module.exports.getBridgeDetails = async (_ip) => { return new Promise((resolve, reject) => { const opt = {} opt.method = 'GET' opt.rejectUnauthorized = false opt.url = 'https://' + _ip + '/api/0/config' simpleget.concat(opt, (err, res, data) => { if (err) return reject(new Error(err.message || 'getBridgeDetails general error')) const statusCode = res?.statusCode ?? 0 const statusMessage = res?.statusMessage || '' const responseText = Buffer.isBuffer(data) ? data.toString('utf8') : String(data ?? '') if (statusCode < 200 || statusCode >= 400) { return reject(new Error('Error response for ' + opt.url + ' with status ' + statusCode + ' ' + statusMessage)) } try { const result = JSON.parse(responseText) if (result?.errors && Array.isArray(result.errors) && result.errors.length > 0) { return reject(new Error('The response for ' + opt.url + ' returned errors ' + JSON.stringify(result.errors))) } if (!result) { return reject(new Error('Unexpected result with no data. ' + JSON.stringify(result))) } return resolve(result) } catch (error) { return reject(new Error(`Invalid Hue bridge configuration response: ${error.message}`)) } }) }) } /** * Register a new application user on the Hue bridge. * * @param {string} ip Bridge IP address. * @param {string} appName Application name (used for devicetype). * @param {string} deviceName Device name (used for devicetype). * @returns {Promise<{bridge: object, user: {username: string, clientkey?: string}}>} */ module.exports.registerBridgeUser = async (ip, appName = 'KNXUltimate', deviceName = 'Node-RED') => { const deviceType = `${appName}#${deviceName}`.substring(0, 40) const payload = JSON.stringify({ devicetype: deviceType, generateclientkey: true }) const postOptions = { method: 'POST', url: `https://${ip}/api`, rejectUnauthorized: false, headers: { 'Content-Type': 'application/json' }, body: payload } const postResponse = await new Promise((resolve, reject) => { simpleget.concat(postOptions, (err, res, data) => { if (err) { return reject(new Error(err.message || 'Hue registration request failed')) } if (!res || res.statusCode < 200 || res.statusCode >= 400) { return reject(new Error(`Hue registration request failed with status ${res?.statusCode ?? 'unknown'}`)) } try { const parsed = JSON.parse(data) resolve(parsed) } catch (parseError) { reject(new Error(`Invalid Hue registration response: ${parseError.message}`)) } }) }) if (!Array.isArray(postResponse) || postResponse.length === 0) { throw new Error('Unexpected Hue registration response') } const firstEntry = postResponse[0] if (firstEntry.error) { throw new Error(firstEntry.error.description || 'Hue Bridge rejected the registration request. Press the link button and try again.') } const success = firstEntry.success || {} if (!success.username) { throw new Error('Hue Bridge did not return a username.') } const username = success.username const clientkey = success.clientkey const configOptions = { method: 'GET', url: `https://${ip}/api/${username}/config`, rejectUnauthorized: false } const bridgeConfig = await new Promise((resolve, reject) => { simpleget.concat(configOptions, (err, res, data) => { if (err) return reject(new Error(err.message || 'Unable to read Hue bridge configuration')) if (!res || res.statusCode < 200 || res.statusCode >= 400) { return reject(new Error(`Hue bridge configuration request failed with status ${res?.statusCode ?? 'unknown'}`)) } try { const parsed = JSON.parse(data) resolve(parsed) } catch (parseError) { reject(new Error(`Invalid Hue bridge configuration response: ${parseError.message}`)) } }) }) return { bridge: bridgeConfig, user: { username, clientkey } } } /** * Discover Hue bridges using the public discovery service. * @returns {Promise<Array<{id?: string, internalipaddress?: string}>>} */ module.exports.discoverHueBridges = async () => { return await new Promise((resolve, reject) => { const opt = { method: 'GET', url: 'https://discovery.meethue.com', rejectUnauthorized: false } simpleget.concat(opt, (err, res, data) => { if (err) { return reject(new Error(err.message || 'Unable to reach discovery.meethue.com')) } if (!res || res.statusCode < 200 || res.statusCode >= 400) { return reject(new Error(`Discovery request failed with status ${res?.statusCode ?? 'unknown'}`)) } try { const parsed = JSON.parse(data) if (!Array.isArray(parsed)) { return reject(new Error('Unexpected discovery response')) } resolve(parsed) } catch (parseError) { reject(new Error(`Invalid discovery response: ${parseError.message}`)) } }) }) }