UNPKG

hb-deconz-tools

Version:
236 lines (225 loc) 7.34 kB
// hb-deconz-tools/lib/Discovery.js // // Homebridge deCONZ Tools. // Copyright © 2018-2026 Erik Baauw. All rights reserved. import { HttpClient } from 'hb-lib-tools/HttpClient' import { OptionParser } from 'hb-lib-tools/OptionParser' /** deCONZ gateway disovery. * <br>See {@link Discovery}. * @name Discovery * @type {Class} * @memberof module:hb-deconz-tools */ /** Class for discovery of deCONZ gateways. * * See the [deCONZ API](https://dresden-elektronik.github.io/deconz-rest-doc/) * documentation for a better understanding of the API. */ class Discovery { /** Create a new instance. * @param {object} params - Parameters. * @param {?*} params.logger - Logger to use for logging. If null, no logging is done. * @param {integer} [params.timeout=5] - Timeout (in seconds) for requests. */ constructor (params = {}) { this._options = { timeout: 5 } const optionParser = new OptionParser(this._options) optionParser .intKey('timeout', 1, 60) .instanceKey('logger') .parse(params) for (const f of ['warn', 'log', 'debug', 'vdebug', 'vvdebug']) { this[f] = this._options.logger?.[f]?.bind(this._options.logger) ?? (() => {}) } } /** Issue an unauthenticated GET request of `/api/config` to given host. * * @param {string} host - The IP address or hostname and port of the deCONZ gateway. * @param {boolean} [https=false] - Connect to the deCONZ gateway over HTTPS. * @return {object|null} response - The JSON response body converted to * JavaScript, or null when the response doesn't come from deCONZ. * @throws {HttpError} In case of error. */ async config (host, https = false) { const options = { host, https, json: true, name: host + ' config', logger: this._options.logger, path: '/api', selfSignedCertificate: https, timeout: this._options.timeout } const client = new HttpClient(options) let retry = false client .on('error', (error) => { if ( error.message === 'socket hang up' || error.message === 'Client network socket disconnected before secure TLS connection was established' ) { retry = true } }) try { const { body } = await client.get('/config') if ( body != null && typeof body === 'object' && typeof body.apiversion === 'string' && /[0-9A-Fa-f]{16}/.test(body.bridgeid) && typeof body.devicename === 'string' && typeof body.name === 'string' && typeof body.swversion === 'string' ) { if (body.bridgeid.startsWith('00212E')) { body.https = https return body } throw new Error(`${body.bridgeid}: not a RaspBee/ConBee mac address`) } throw new Error('not a deCONZ gateway') } catch (error) { if (retry) { return this.config(host, !https) } throw error } } /** Issue an unauthenticated GET request of `/description.xml` to given host. * * @param {string} host - The IP address or hostname and port of the deCONZ gateway. * @param {boolean} [https=false] - Connect to the deCONZ gateway over HTTPS. * @return {object} response - The description, converted to JavaScript. * @throws {Error} In case of error. */ async description (host, https = false) { if (this.xmlParser == null) { const { Parser } = await import('xml2js') this.xmlParser = new Parser({ explicitArray: false }) } const options = { host, https, logger: this._options.logger, name: host + ' description', selfSignedCertificate: https, timeout: this._options.timeout, xmlParser: async (xml) => { return this.xmlParser.parseStringPromise(xml) } } const client = new HttpClient(options) const { body } = await client.get('/description.xml') return body } /** Discover deCONZ gateways. * * Queries the Phoscon portal for known gateways and does a local search * over UPnP. * Calls {@link Discovery#config config()} for each discovered gateway * for verification. * @param {boolean} [stealth=false] - Don't query discovery portals. * @return {object} response - Response object with a key/value pair per * found gateway. The key is the host (IP address or hostname and port), * the value is the return value of {@link Discovery#config config()}. */ async discover (stealth = false) { this.foundMap = {} this.gatewayMap = {} this.jobs = [] this.jobs.push(this.#upnp()) if (!stealth) { this.jobs.push(this.#nupnp()) } for (const job of this.jobs) { await job } return this.gatewayMap } #found (name, id, host) { this.debug('%s: found %s at %s', name, id, host) if (this.foundMap[host] == null) { this.foundMap[host] = id this.jobs.push( this.config(host).then((config) => { this.description(host).then((description) => { if (description?.root?.device?.['deconz:info']?.httpsPort != null) { config.https = true host = host.split(':')[0] + ':' + description.root.device['deconz:info'].httpsPort } this.gatewayMap[host] = config }).catch((error) => { if (error.request == null) { this.warn(error) } }) }).catch((error) => { delete this.foundMap[host] if (error.request == null) { this.warn(error) } }) ) } } async #upnp () { if (this.upnpClient == null) { const { UpnpClient } = await import('hb-lib-tools/UpnpClient') this.upnpClient = new UpnpClient({ filter: (message) => { return /^[0-9A-F]{16}$/.test(message['gwid.phoscon.de']) }, logger: this, timeout: this._options.timeout }) this.upnpClient .on('deviceFound', (address, obj, message) => { let host const a = obj.location.split('/') if (a.length > 3 && a[2] != null) { host = a[2] const b = host.split(':') const port = parseInt(b[1]) if (port === 80) { host = b[0] } this.#found('upnp', obj['gwid.phoscon.de'], host) } }) } await this.upnpClient.search() } async #nupnp (options) { const name = 'phoscon.de' if (this.client == null) { this.client = new HttpClient({ host: name, https: true, json: true, name, logger: this._options.logger, path: '/discover', timeout: this._options.timeout }) } try { const { body } = await this.client.get() if (Array.isArray(body)) { for (const gateway of body) { let host = gateway.internalipaddress if (gateway.internalport != null && gateway.internalport !== 80) { host += ':' + gateway.internalport } this.#found(name, gateway.id.toUpperCase(), host) } } } catch (error) { if (error instanceof HttpClient.HttpError) { return } this.warn(error) } } } export { Discovery }