From d83085ea7f57b88945084e43ddf9c1536077b0fa Mon Sep 17 00:00:00 2001 From: Koen Kanters Date: Mon, 9 Sep 2019 19:48:09 +0200 Subject: [PATCH] Zigbee-herdsman (#1945) * Update zigbee-herdsman and zigbee-shepherd-converters. * Force Aqara S2 Lock endvices (#1764) * Start on zigbee-herdsman controller refactor. * More updates. * Cleanup zapp. * updates. * Propagate adapter disconnected event. * Updates. * Initial refactor to zigbee-herdsman. * Refactor deviceReceive to zigbee-herdsman. * Rename * Refactor deviceConfigure. * Finish bridge config. * Refactor availability. * Active homeassistant extension and more refactors. * Refactor groups. * Enable soft reset. * Activate group membership * Start on tests. * Enable reporting. * Add more controller tests. * Add more tests * Fix linting error. * Data en deviceReceive tests. * Move to zigbee-herdsman-converters. * More device publish tests. * Cleanup dependencies. * Bring device publish coverage to 100. * Bring home assistant test coverage to 100. * Device configure tests. * Attempt to fix tests. * Another attempt. * Another one. * Another one. * Another. * Add wait. * Longer wait. * Debug. * Update dependencies. * Another. * Begin on availability tests. * Improve availability tests. * Complete deviceAvailability tests. * Device bind tests. * More tests. * Begin networkmap refactors. * start on networkmap tests. * Network map tests. * Add utils tests. * Logger tests. * Settings and logger tests. * Ignore some stuff for coverage and add todos. * Add remaining missing tests. * Enforce 100% test coverage. * Start on groups test and refactor entityPublish to resolveEntity * Remove joinPathStorage, not used anymore as group information is stored into zigbee-herdsman database. * Fix linting issues. * Improve tests. * Add groups. * fix group membership. * Group: log names. * Convert MQTT message to string by default. * Fix group name. * Updates. * Revert configuration.yaml. * Add new line. * Fixes. * Updates. * Fix tests. * Ignore soft reset extension. --- .eslintignore | 3 +- .travis.yml | 2 +- docs/_config.yml | 1 - docs/index.md | 1 - index.js | 2 +- lib/controller.js | 380 +- lib/extension/bridgeConfig.js | 174 +- lib/extension/deviceAvailability.js | 154 +- lib/extension/deviceBind.js | 124 +- lib/extension/deviceConfigure.js | 123 +- lib/extension/deviceGroupMembership.js | 67 +- lib/extension/devicePublish.js | 235 - lib/extension/deviceReceive.js | 137 +- lib/extension/deviceReport.js | 161 +- lib/extension/entityPublish.js | 201 + lib/extension/extensionTemplate.js | 2 + lib/extension/groups.js | 292 +- lib/extension/homeassistant.js | 68 +- lib/extension/livolo.js | 25 +- lib/extension/networkMap.js | 190 +- lib/extension/responder.js | 75 - lib/extension/softReset.js | 38 +- lib/extension/xiaomi.js | 70 - lib/mqtt.js | 75 +- lib/state.js | 8 - lib/util/data.js | 11 - lib/util/fs.js | 16 - lib/util/logger.js | 21 +- lib/util/settings.js | 404 +- lib/util/utils.js | 121 +- lib/util/yaml.js | 16 + lib/util/zigbeeQueue.js | 97 - lib/zapp/cie.js | 23 - lib/zigbee.js | 801 +-- npm-shrinkwrap.json | 8376 +++--------------------- package.json | 32 +- test/bridgeConfig.test.js | 412 +- test/controller.test.js | 464 +- test/data.test.js | 11 +- test/deviceAvailability.test.js | 255 +- test/deviceBind.test.js | 337 +- test/deviceConfigure.test.js | 144 + test/devicePublish.test.js | 1376 ---- test/deviceReceive.test.js | 593 +- test/deviceReport.test.js | 152 + test/entityPublish.test.js | 827 +++ test/group.test.js | 462 +- test/homeassistant.test.js | 877 ++- test/logger.test.js | 36 + test/networkmap.test.js | 163 + test/settings.test.js | 625 +- test/stub/data.js | 171 + test/stub/logger.js | 13 + test/stub/mqtt.js | 24 + test/stub/zigbeeHerdsman.js | 176 + test/utils.js | 14 - test/utils.test.js | 95 +- 57 files changed, 6326 insertions(+), 13427 deletions(-) delete mode 100644 docs/_config.yml delete mode 100644 docs/index.md delete mode 100644 lib/extension/devicePublish.js create mode 100644 lib/extension/entityPublish.js delete mode 100644 lib/extension/responder.js delete mode 100644 lib/extension/xiaomi.js delete mode 100644 lib/util/fs.js create mode 100644 lib/util/yaml.js delete mode 100644 lib/util/zigbeeQueue.js delete mode 100644 lib/zapp/cie.js create mode 100644 test/deviceConfigure.test.js delete mode 100644 test/devicePublish.test.js create mode 100644 test/deviceReport.test.js create mode 100644 test/entityPublish.test.js create mode 100644 test/logger.test.js create mode 100644 test/networkmap.test.js create mode 100644 test/stub/data.js create mode 100644 test/stub/logger.js create mode 100644 test/stub/mqtt.js create mode 100644 test/stub/zigbeeHerdsman.js delete mode 100644 test/utils.js diff --git a/.eslintignore b/.eslintignore index dbf082131..7406675ad 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1 +1,2 @@ -node_modules/* \ No newline at end of file +node_modules/* +test/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 2226c2355..e829db60c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ install: - npm install before_script: - - npm test + - npm run test-with-coverage - npm run eslint script: diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index c4192631f..000000000 --- a/docs/_config.yml +++ /dev/null @@ -1 +0,0 @@ -theme: jekyll-theme-cayman \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 71f72e1d0..000000000 --- a/docs/index.md +++ /dev/null @@ -1 +0,0 @@ -Moved to [https://www.zigbee2mqtt.io](https://www.zigbee2mqtt.io) diff --git a/index.js b/index.js index b881e2dab..66b24bc0d 100644 --- a/index.js +++ b/index.js @@ -18,6 +18,6 @@ let stopping = false; function handleQuit() { if (!stopping) { stopping = true; - controller.stop(() => process.exit()); + controller.stop(); } } diff --git a/lib/controller.js b/lib/controller.js index 8c74844cc..bdd1fcb68 100644 --- a/lib/controller.js +++ b/lib/controller.js @@ -3,26 +3,23 @@ const Zigbee = require('./zigbee'); const State = require('./state'); const logger = require('./util/logger'); const settings = require('./util/settings'); -const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); const objectAssignDeep = require('object-assign-deep'); const utils = require('./util/utils'); // Extensions +const ExtensionEntityPublish = require('./extension/entityPublish'); +const ExtensionDeviceReceive = require('./extension/deviceReceive'); const ExtensionNetworkMap = require('./extension/networkMap'); const ExtensionSoftReset = require('./extension/softReset'); -const ExtensionXiaomi = require('./extension/xiaomi'); -const ExtensionDevicePublish = require('./extension/devicePublish'); const ExtensionHomeAssistant = require('./extension/homeassistant'); const ExtensionDeviceConfigure = require('./extension/deviceConfigure'); const ExtensionDeviceGroupMembership = require('./extension/deviceGroupMembership'); -const ExtensionDeviceReceive = require('./extension/deviceReceive'); const ExtensionBridgeConfig = require('./extension/bridgeConfig'); const ExtensionGroups = require('./extension/groups'); const ExtensionDeviceAvailability = require('./extension/deviceAvailability'); const ExtensionDeviceBind = require('./extension/deviceBind'); const ExtensionDeviceReport = require('./extension/deviceReport'); -const ExtensionLivolo = require('./extension/livolo'); -const ExtensionResponder = require('./extension/responder'); +// const ExtensionLivolo = require('./extension/livolo'); class Controller { constructor() { @@ -30,24 +27,19 @@ class Controller { this.mqtt = new MQTT(); this.state = new State(); - // Bind methods - this.onMQTTConnected = this.onMQTTConnected.bind(this); - this.onZigbeeMessage = this.onZigbeeMessage.bind(this); - this.onMQTTMessage = this.onMQTTMessage.bind(this); this.publishEntityState = this.publishEntityState.bind(this); + this.onZigbeeAdapterDisconnected = this.onZigbeeAdapterDisconnected.bind(this); // Initialize extensions. this.extensions = [ + new ExtensionEntityPublish(this.zigbee, this.mqtt, this.state, this.publishEntityState), new ExtensionDeviceReceive(this.zigbee, this.mqtt, this.state, this.publishEntityState), new ExtensionDeviceGroupMembership(this.zigbee, this.mqtt, this.publishEntityState), new ExtensionDeviceConfigure(this.zigbee, this.mqtt, this.state, this.publishEntityState), - new ExtensionDevicePublish(this.zigbee, this.mqtt, this.state, this.publishEntityState), new ExtensionNetworkMap(this.zigbee, this.mqtt, this.state, this.publishEntityState), - new ExtensionXiaomi(this.zigbee, this.mqtt, this.state, this.publishEntityState), new ExtensionBridgeConfig(this.zigbee, this.mqtt, this.state, this.publishEntityState), new ExtensionGroups(this.zigbee, this.mqtt, this.state, this.publishEntityState), new ExtensionDeviceBind(this.zigbee, this.mqtt, this.state, this.publishEntityState), - new ExtensionResponder(this.zigbee, this.mqtt, this.state, this.publishEntityState), ]; if (settings.get().advanced.report) { @@ -62,6 +54,7 @@ class Controller { )); } + /* istanbul ignore next */ if (settings.get().advanced.soft_reset_timeout !== 0) { this.extensions.push(new ExtensionSoftReset( this.zigbee, this.mqtt, this.state, this.publishEntityState @@ -74,29 +67,48 @@ class Controller { )); } - if (settings.get().experimental.livolo) { - // https://github.com/Koenkk/zigbee2mqtt/issues/592 - this.extensions.push(new ExtensionLivolo( - this.zigbee, this.mqtt, this.state, this.publishEntityState - )); + // TODO + // if (settings.get().experimental.livolo) { + // // https://github.com/Koenkk/zigbee2mqtt/issues/592 + // this.extensions.push(new ExtensionLivolo( + // this.zigbee, this.mqtt, this.state, this.publishEntityState + // )); + // } + } + + async start() { + logger.info(`Logging to directory: '${logger.directory}'`); + logger.cleanup(); + this.state.start(); + + const info = await utils.getZigbee2mqttVersion(); + logger.info(`Starting zigbee2mqtt version ${info.version} (commit #${info.commitHash})`); + + // Start zigbee + try { + this.zigbee.on('event', this.onZigbeeEvent.bind(this)); + this.zigbee.on('adapterDisconnected', this.onZigbeeAdapterDisconnected); + await this.zigbee.start(); + } catch (error) { + logger.error('Failed to start zigbee'); + logger.error('Exiting...'); + process.exit(1); } - } - onMQTTConnected() { - // Resend all cached states. - this.sendAllCachedStates(); - - // Call extensions - this.extensions.filter((e) => e.onMQTTConnected).forEach((e) => e.onMQTTConnected()); - } - - onZigbeeStarted() { - // Log zigbee clients on startup and configure. - const devices = this.zigbee.getAllClients(); + // Log zigbee clients on startup + const devices = await this.zigbee.getClients(); logger.info(`Currently ${devices.length} devices are joined:`); - devices.forEach((device) => { - logger.info(this.getDeviceStartupLogMessage(device)); - }); + for (const device of devices) { + const entity = await this.zigbee.resolveEntity(device); + logger.info( + (entity.settings ? entity.settings.friendlyName : entity.device.ieeeAddr) + + ` (${entity.device.ieeeAddr}): ` + + (entity.mapped ? + `${entity.mapped.model} - ${entity.mapped.vendor} ${entity.mapped.description} ` : + 'Not supported ') + + `(${entity.device.type})` + ); + } // Enable zigbee join. if (settings.get().permit_join) { @@ -107,167 +119,169 @@ class Controller { this.zigbee.permitJoin(settings.get().permit_join); - // Connect to MQTT broker - this.mqtt.connect(this.onMQTTMessage, this.onMQTTConnected); - // Call extensions - this.extensions.filter((e) => e.onZigbeeStarted).forEach((e) => e.onZigbeeStarted()); - } + this.callExtensionMethod('onZigbeeStarted', []); - onZigbeeMessage(message) { - // Variables - let device = null; - let mappedDevice = null; + // MQTT + this.mqtt.on('message', this.onMQTTMessage.bind(this)); + await this.mqtt.connect(); - // Check if message has a device - if (message.endpoints && message.endpoints.length && message.endpoints[0].device) { - device = message.endpoints[0].device; - } - - // Retrieve modelId from message - if (device && device.modelId) { - mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); - } - - // Log - logger.debug( - `Received zigbee message of type '${message.type}' with data '${JSON.stringify(message.data)}'` + - (device ? ` of device '${device.modelId}' (${device.ieeeAddr})` : '') + - (message.groupid ? ` with groupID ${message.groupid}` : '') + - (message.endpoints && message.endpoints.length ? ` of endpoint ${message.endpoints[0].epId}` : '') - ); - - // Call extensions. - this.extensions - .filter((e) => e.onZigbeeMessage) - .forEach((e) => e.onZigbeeMessage(message, device, mappedDevice)); - } - - onMQTTMessage(topic, message) { - logger.debug(`Received MQTT message on '${topic}' with data '${message}'`); - - // Call extensions - const results = this.extensions - .filter((e) => e.onMQTTMessage) - .map((e) => e.onMQTTMessage(topic, message)); - - if (!results.includes(true)) { - logger.warn(`Cannot handle MQTT message on '${topic}' with data '${message}'`); - } - } - - start() { - logger.info(`Logging to directory: '${logger.directory}'`); - - logger.cleanup(); - - this.state.start(); - - this.startupLogVersion(() => { - this.zigbee.start(this.onZigbeeMessage, (error) => { - if (error) { - logger.error('Failed to start', error); - logger.error('Exiting...'); - process.exit(1); - } else { - this.onZigbeeStarted(); - } - }); - }); - } - - stop(callback) { - // Call extensions - this.extensions.filter((e) => e.stop).forEach((e) => e.stop()); - - // Wrap-up - this.state.stop(); - - this.mqtt.disconnect(); - this.zigbee.stop(callback); - } - - startupLogVersion(callback) { - utils.getZigbee2mqttVersion((info) => { - logger.info(`Starting zigbee2mqtt version ${info.version} (commit #${info.commitHash})`); - callback(); - }); - } - - getDeviceStartupLogMessage(device) { - let friendlyName = 'unknown'; - let type = 'unknown'; - let friendlyDevice = {model: 'unkown', description: 'unknown'}; - const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); - if (mappedModel) { - friendlyDevice = mappedModel; - } - - if (settings.getDevice(device.ieeeAddr)) { - friendlyName = settings.getDevice(device.ieeeAddr).friendly_name; - } - - if (device.type) { - type = utils.correctDeviceType(device); - } - - return `${friendlyName} (${device.ieeeAddr}): ${friendlyDevice.model} - ` + - `${friendlyDevice.vendor} ${friendlyDevice.description} (${type})`; - } - - sendAllCachedStates() { - this.zigbee.getAllClients().forEach((device) => { + // Send all cached states. + for (const device of await this.zigbee.getClients()) { if (this.state.exists(device.ieeeAddr)) { this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr)); } - }); + } + + // Call extensions + await this.callExtensionMethod('onMQTTConnected', []); } - publishEntityState(entityID, payload) { - const entity = settings.resolveEntity(entityID); - const appSettings = settings.get(); - let messagePayload = {...payload}; + async stop() { + // Call extensions + await this.callExtensionMethod('stop', []); - const currentState = this.state.exists(entityID) ? this.state.get(entityID) : {}; + // Wrap-up + this.state.stop(); + await this.mqtt.disconnect(); + + try { + await this.zigbee.stop(); + process.exit(0); + } catch (error) { + logger.error('Failed to stop zigbee'); + process.exit(1); + } + } + + async onZigbeeAdapterDisconnected() { + logger.error('Adapter disconnected, stopping'); + await this.stop(); + } + + async onZigbeeEvent(type, data) { + const entity = await this.zigbee.resolveEntity(data.device || data.ieeeAddr); + if (!entity.settings && data.device) { + // Only deviceLeave doesn't have a device (not interesting to add to settings) + entity.settings = settings.addDevice(data.device.ieeeAddr); + } + + const friendlyName = entity.settings.friendlyName; + + if (type === 'message') { + logger.debug( + `Received Zigbee message from '${entity.settings.friendlyName}' of type '${data.type}' ` + + `with data '${JSON.stringify(data.data)}' from endpoint ${data.endpoint.ID}` + + (data.hasOwnProperty('groupID') ? ` with groupID ${data.groupID}` : ``) + ); + } else if (type === 'deviceJoined') { + logger.info(`Device '${friendlyName}' joined`); + this.mqtt.log('device_connected', friendlyName); + } else if (type === 'deviceInterview') { + if (data.status === 'successful') { + logger.info(`Successfully interviewed '${friendlyName}', device has succesfully been paired`); + + if (entity.mapped) { + const {vendor, description, model} = entity.mapped; + logger.info( + `Device '${friendlyName}' is supported, identified as: ${vendor} ${description} (${model})` + ); + + const log = {friendlyName, model, vendor, description, supported: true}; + this.mqtt.log('pairing', 'interview_successful', log); + } else { + logger.warn( + `Device '${friendlyName}' with Zigbee model '${data.device.modelID}' is NOT supported, ` + + `please follow https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html` + ); + this.mqtt.log('pairing', 'interview_successful', {friendlyName, supported: false}); + } + } else if (data.status === 'failed') { + logger.error(`Failed to interview '${friendlyName}', device has not succesfully been paired`); + this.mqtt.log('pairing', 'interview_failed', {friendlyName}); + } else { + /* istanbul ignore else */ + if (data.status === 'started') { + logger.info(`Starting interview of '${friendlyName}'`); + this.mqtt.log('pairing', 'interview_started', {friendlyName}); + } + } + } else if (type === 'deviceAnnounce') { + logger.debug(`Device '${friendlyName}' announced itself`); + } else { + /* istanbul ignore else */ + if (type === 'deviceLeave') { + logger.warn(`Device '${friendlyName}' left the network`); + this.mqtt.log('device_removed', 'left_network', {friendlyName}); + } + } + + // Call extensions + this.callExtensionMethod('onZigbeeEvent', [type, data, entity.mapped, entity.settings]); + } + + onMQTTMessage(payload) { + const {topic, message} = payload; + logger.debug(`Received MQTT message on '${topic}' with data '${message}'`); + + // Call extensions + this.callExtensionMethod('onMQTTMessage', [topic, message]); + } + + async publishEntityState(IDorName, payload) { + const entity = settings.getEntity(IDorName); + if (!entity) { + logger.error(`'${IDorName}' does not exist, skipping publish`); + return; + } + + let messagePayload = {...payload}; + const currentState = this.state.exists(entity.ID) ? this.state.get(entity.ID) : {}; const newState = objectAssignDeep.noMutate(currentState, payload); // Update state cache with new state. - this.state.set(entityID, newState); + this.state.set(entity.ID, newState); if (settings.get().advanced.cache_state) { // Add cached state to payload messagePayload = newState; } - const entitySettings = entity.type === 'device' ? settings.getDevice(entityID) : settings.getGroup(entityID); const options = { - retain: entitySettings ? entitySettings.retain : false, - qos: entitySettings && entitySettings.qos ? entitySettings.qos : 0, + retain: entity.hasOwnProperty('retain') ? entity.retain : false, + qos: entity.hasOwnProperty('qos') ? entity.qos : 0, }; - if (entity.type === 'device' && appSettings.mqtt.include_device_information) { - messagePayload.device = this.getDeviceInfoForMqtt(entityID); + if (entity.type === 'device' && settings.get().mqtt.include_device_information) { + const device = await this.zigbee.getDevice({ieeeAddr: entity.ID}); + const attributes = [ + 'ieeeAddr', 'networkAddress', 'type', 'manufacturerID', 'manufacturerName', 'powerSource', + 'applicationVersion', 'stackVersion', 'zclVersion', 'hardwareVersion', 'dateCode', 'softwareBuildID', + ]; + + messagePayload.device = {friendlyName: entity.friendly_name}; + attributes.forEach((a) => messagePayload.device[a] = device[a]); } if (Object.entries(messagePayload).length) { if (settings.get().experimental.output === 'json') { - this.mqtt.publish(entity.friendlyName, JSON.stringify(messagePayload), options); - } else if (settings.get().experimental.output === 'attribute') { - this.iteratePayloadForAttrOutput(entity.friendlyName+'/', messagePayload, options); + await this.mqtt.publish(entity.friendly_name, JSON.stringify(messagePayload), options); + } else { + /* istanbul ignore else */ + if (settings.get().experimental.output === 'attribute') { + await this.iteratePayloadAttributeOutput(`${entity.friendly_name}/`, messagePayload, options); + } } } } - iteratePayloadForAttrOutput(topicRoot, payload, options) { - Object.keys(payload).forEach((key) => { - let subPayload = payload[key]; + async iteratePayloadAttributeOutput(topicRoot, payload, options) { + for (const [key, value] of Object.entries(payload)) { + let subPayload = value; let message; // Special cases - if (key === 'color' && - subPayload.r !== undefined && - subPayload.g !== undefined && - subPayload.b !== undefined) { + if (key === 'color' && utils.objectHasProperties(subPayload, ['r', 'g', 'b'])) { subPayload = [subPayload.r, subPayload.g, subPayload.b]; } @@ -275,36 +289,30 @@ class Controller { if (Array.isArray(subPayload)) { message = subPayload.map((x) => `${x}`).join(','); } else if (typeof subPayload === 'object') { - return this.iteratePayloadForAttrOutput(topicRoot+key+'-', subPayload, options); + return this.iteratePayloadAttributeOutput(`${topicRoot}${key}-`, subPayload, options); } else { message = typeof subPayload === 'string' ? subPayload : JSON.stringify(subPayload); } - this.mqtt.publish(`${topicRoot}${key}`, message, options); - }); + await this.mqtt.publish(`${topicRoot}${key}`, message, options); + } } - getDeviceInfoForMqtt(ieeeAddr) { - const device = this.zigbee.getDevice(ieeeAddr); - const { - type, nwkAddr, manufId, manufName, powerSource, modelId, hwVersion, swBuildId, status, dateCode, - } = device; - const deviceSettings = settings.getDevice(device.ieeeAddr); - - return { - ieeeAddr, - friendlyName: deviceSettings ? (deviceSettings.friendly_name || '') : '', - type, - nwkAddr, - manufId, - manufName, - powerSource, - modelId, - hwVersion: hwVersion ? hwVersion : 'unknown', - swBuildId: swBuildId ? swBuildId : 'unknown', - dateCode: dateCode ? dateCode : 'unknown', - status, - }; + async callExtensionMethod(method, parameters) { + for (const extension of this.extensions) { + if (extension[method]) { + try { + await extension[method](...parameters); + } catch (error) { + /* istanbul ignore next */ + logger.error(`Failed to call '${extension.constructor.name}' '${method}' (${error.stack})`); + /* istanbul ignore next */ + if (process.env.JEST_WORKER_ID !== undefined) { + throw error; + } + } + } + } } } diff --git a/lib/extension/bridgeConfig.js b/lib/extension/bridgeConfig.js index 7d12e4995..17b759ad2 100644 --- a/lib/extension/bridgeConfig.js +++ b/lib/extension/bridgeConfig.js @@ -1,7 +1,8 @@ const settings = require('../util/settings'); const logger = require('../util/logger'); -const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); const utils = require('../util/utils'); +const assert = require('assert'); const configRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/config/((?:\\w+/get)|(?:\\w+))`); const allowedLogLevels = ['error', 'warn', 'info', 'debug']; @@ -50,18 +51,21 @@ class BridgeConfig { } whitelist(topic, message) { - message = message.toString(); - const IDByFriendlyName = settings.getIeeeAddrByFriendlyName(message); - const deviceID = IDByFriendlyName ? IDByFriendlyName : message; - const whitelisted = settings.whitelistDevice(deviceID); - whitelisted ? logger.info(`Whitelisted '${deviceID}'`) : logger.error(`Failed to whitelist '${deviceID}'`); - this.mqtt.log('device_whitelisted', deviceID); + try { + const entity = settings.getEntity(message); + assert(entity, `Entity '${message}' does not exist`); + settings.whitelistDevice(entity.ID); + logger.info(`Whitelisted '${entity.friendlyName}'`); + this.mqtt.log('device_whitelisted', entity.friendlyName); + } catch (error) { + logger.error(`Failed to whitelist '${message}' '${error}'`); + } } deviceOptions(topic, message) { let json = null; try { - json = JSON.parse(message.toString()); + json = JSON.parse(message); } catch (e) { logger.error('Failed to parse message as JSON'); return; @@ -72,34 +76,28 @@ class BridgeConfig { return; } - const ieeeAddr = settings.getIeeeAddrByFriendlyName(json.friendly_name); - if (!ieeeAddr) { - logger.error(`Failed to find device '${json.friendly_name}'`); - return; - } - - settings.changeDeviceOptions(ieeeAddr, json.options); + const entity = settings.getEntity(json.friendly_name); + assert(entity, `Entity '${json.friendly_name}' does not exist`); + settings.changeDeviceOptions(entity.ID, json.options); logger.info(`Changed device specific options of '${json.friendly_name}' (${JSON.stringify(json.options)})`); } - permitJoin(topic, message) { - this.zigbee.permitJoin(message.toString().toLowerCase() === 'true', () => this.publish()); + async permitJoin(topic, message) { + await this.zigbee.permitJoin(message.toLowerCase() === 'true'); + this.publish(); } - reset(topic, message) { - this.zigbee.softReset((error) => { - if (error) { - logger.error('Soft reset failed'); - } else { - logger.info('Soft resetted ZNP'); - } - }); + async reset(topic, message) { + try { + await this.zigbee.softReset(); + logger.info('Soft resetted ZNP'); + } catch (error) { + logger.error('Soft reset failed'); + } } lastSeen(topic, message) { const allowed = ['disable', 'ISO_8601', 'epoch', 'ISO_8601_local']; - message = message.toString(); - if (!allowed.includes(message)) { logger.error(`${message} is not an allowed value, possible: ${allowed}`); return; @@ -111,8 +109,6 @@ class BridgeConfig { elapsed(topic, message) { const allowed = ['true', 'false']; - message = message.toString(); - if (!allowed.includes(message)) { logger.error(`${message} is not an allowed value, possible: ${allowed}`); return; @@ -123,7 +119,7 @@ class BridgeConfig { } logLevel(topic, message) { - const level = message.toString().toLowerCase(); + const level = message.toLowerCase(); if (allowedLogLevels.includes(level)) { logger.info(`Switching log level to '${level}'`); logger.transports.console.level = level; @@ -135,23 +131,23 @@ class BridgeConfig { this.publish(); } - devices(topic, message) { - const devices = this.zigbee.getDevices().map((device) => { + async devices(topic, message) { + const devices = (await this.zigbee.getDevices({})).map((device) => { const payload = { ieeeAddr: device.ieeeAddr, type: device.type, }; if (device.type !== 'Coordinator') { - const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); + const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); const friendlyDevice = settings.getDevice(device.ieeeAddr); - payload.model = mappedDevice ? mappedDevice.model : device.modelId; + payload.model = mappedDevice ? mappedDevice.model : device.modelID; payload.friendly_name = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr; - payload.nwkAddr = device.nwkAddr; - payload.manufId = device.manufId; - payload.manufName = device.manufName; + payload.networkAddress = device.networkAddress; + payload.manufacturerID = device.manufacturerID; + payload.manufacturerName = device.manufacturerName; payload.powerSource = device.powerSource; - payload.modelId = device.modelId; + payload.modelID = device.modelID; payload.hwVersion = device.hwVersion; payload.swBuildId = device.swBuildId; payload.dateCode = device.dateCode; @@ -168,16 +164,19 @@ class BridgeConfig { } groups(topic, message) { - this.mqtt.log('groups', settings.getGroups()); + this.mqtt.log('groups', settings.getGroups().map((g) => { + const group = {...g}; + delete group.friendlyName; + return group; + })); } rename(topic, message) { - const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} ` + - `got ${message.toString()}`; + const invalid = `Invalid rename message format expected {old: 'friendly_name', new: 'new_name} got ${message}`; let json = null; try { - json = JSON.parse(message.toString()); + json = JSON.parse(message); } catch (e) { logger.error(invalid); return; @@ -189,72 +188,66 @@ class BridgeConfig { return; } - if (settings.changeFriendlyName(json.old, json.new)) { + try { + settings.changeFriendlyName(json.old, json.new); logger.info(`Successfully renamed - ${json.old} to ${json.new} `); this.mqtt.log('device_renamed', {from: json.old, to: json.new}); - } else { - logger.error(`Failed to renamed - ${json.old} to ${json.new}`); - return; + } catch (error) { + logger.error(`Failed to rename - ${json.old} to ${json.new}`); } } - addGroup(topic, message) { - const name = message.toString(); - const added = settings.addGroup(name); - added ? logger.info(`Added group '${name}'`) : logger.error(`Failed to add group '${name}'`); + async addGroup(topic, message) { + const name = message; + const group = settings.addGroup(name); + await this.zigbee.createGroup(group.ID); + logger.info(`Added group '${name}'`); } removeGroup(topic, message) { - const name = message.toString(); - const removed = settings.removeGroup(name); - removed ? logger.info(`Removed group '${name}'`) : logger.error(`Failed to remove group '${name}'`); + const name = message; + settings.removeGroup(name); + logger.info(`Removed group '${name}'`); } - remove(topic, message) { - this.removeOrBan(false, message); + async remove(topic, message) { + await this.removeOrBan(false, message); } - ban(topic, message) { - this.removeOrBan(true, message); + async ban(topic, message) { + await this.removeOrBan(true, message); } - removeOrBan(ban, message) { - message = message.toString(); - const IDByFriendlyName = settings.getIeeeAddrByFriendlyName(message); - const deviceID = IDByFriendlyName ? IDByFriendlyName : message; - const device = this.zigbee.getDevice(deviceID); + async removeOrBan(ban, message) { + const entity = await this.zigbee.resolveEntity(message); const cleanup = () => { // Remove from configuration.yaml - settings.removeDevice(deviceID); + settings.removeDevice(entity.settings.ID); // Remove from state - this.state.remove(deviceID); + this.state.remove(entity.settings.ID); - logger.info(`Successfully ${ban ? 'banned' : 'removed'} ${deviceID}`); + logger.info(`Successfully ${ban ? 'banned' : 'removed'} ${entity.settings.friendlyName}`); this.mqtt.log(ban ? 'device_banned' : 'device_removed', message); }; // Remove from zigbee network. - if (device) { - this.zigbee.removeDevice(deviceID, ban, (error) => { - if (!error) { - cleanup(); - } else { - logger.error(`Failed to ${ban ? 'ban' : 'remove'} ${deviceID}`); - } - }); - } else { + try { + logger.info(`Removing '${entity.settings.friendlyName}'`); + await entity.device.removeFromNetwork(); cleanup(); + } catch (error) { + logger.error(`Failed to ${ban ? 'ban' : 'remove'} ${entity.settings.friendlyName} (${error})`); } } - onMQTTConnected() { + async onMQTTConnected() { this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/config/+`); - this.publish(); + await this.publish(); } - onMQTTMessage(topic, message) { + async onMQTTMessage(topic, message) { if (!topic.match(configRegex)) { return false; } @@ -265,24 +258,25 @@ class BridgeConfig { return false; } - this.supportedOptions[option](topic, message); + await this.supportedOptions[option](topic, message); return true; } - publish() { - utils.getZigbee2mqttVersion((info) => { - const topic = `bridge/config`; - const payload = { - version: info.version, - commit: info.commitHash, - coordinator: this.zigbee.getFirmwareVersion(), - log_level: logger.transports.console.level, - permit_join: this.zigbee.getPermitJoin(), - }; + async publish() { + const info = await utils.getZigbee2mqttVersion(); + const coordinator = await this.zigbee.getCoordinatorVersion(); - this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null); - }); + const topic = `bridge/config`; + const payload = { + version: info.version, + commit: info.commitHash, + coordinator, + log_level: logger.transports.console.level, + permit_join: await this.zigbee.getPermitJoin(), + }; + + await this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}); } } diff --git a/lib/extension/deviceAvailability.js b/lib/extension/deviceAvailability.js index ad5ef5afc..462e99400 100644 --- a/lib/extension/deviceAvailability.js +++ b/lib/extension/deviceAvailability.js @@ -1,15 +1,15 @@ const logger = require('../util/logger'); const settings = require('../util/settings'); const utils = require('../util/utils'); -const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); // Some EndDevices should be pinged // e.g. E11-G13 https://github.com/Koenkk/zigbee2mqtt/issues/775#issuecomment-453683846 -const pingableDevices = [ - zigbeeShepherdConverters.devices.find((d) => d.model === 'E11-G13'), +const forcedPingable = [ + zigbeeHerdsmanConverters.devices.find((d) => d.model === 'E11-G13'), ]; -const toZigbeeCandidates = ['state', 'color', 'color_temp']; +const toZigbeeCandidates = ['state', 'brightness', 'color', 'color_temp']; /** * This extensions pings devices to check if they are online. @@ -20,24 +20,22 @@ class DeviceAvailability { this.mqtt = mqtt; this.availability_timeout = settings.get().advanced.availability_timeout; this.timers = {}; - this.pending = []; this.state = {}; // Initialize blacklist this.blacklist = settings.get().advanced.availability_blacklist.map((e) => { - return settings.getIeeeAddrByFriendlyName(e) || e; + return settings.getEntity(e).ID; }); } isPingable(device) { logger.trace(`Checking if ${device.ieeeAddr} is pingable`); - if (this.blacklist.includes(device.ieeeAddr)) { logger.trace(`${device.ieeeAddr} is not pingable because of blacklist`); return false; } - if (pingableDevices.find((d) => d.zigbeeModel.includes(device.modelId))) { + if (forcedPingable.find((d) => d.zigbeeModel.includes(device.modelID))) { logger.trace(`${device.ieeeAddr} is pingable because in pingable devices`); return true; } @@ -47,47 +45,34 @@ class DeviceAvailability { return result; } - getAllPingableDevices() { - return this.zigbee.getAllClients().filter((d) => this.isPingable(d)); + async getAllPingableDevices() { + return (await this.zigbee.getClients()).filter((d) => this.isPingable(d)); } - onMQTTConnected() { + async onMQTTConnected() { // As some devices are not checked for availability (e.g. battery powered devices) // we mark all device as online by default. - this.zigbee.getDevices() - .filter((d) => d.type !== 'Coordinator') - .forEach((device) => this.publishAvailability(device.ieeeAddr, true)); + (await this.zigbee.getClients()) + .forEach((device) => this.publishAvailability(device, true)); // Start timers for all devices - this.getAllPingableDevices().forEach((device) => this.setTimer(device)); + (await this.getAllPingableDevices()).forEach((device) => this.setTimer(device)); } - handleInterval(device) { - // Check if a job is already pending. - // This avoids overflowing of the queue in case the queue is not able to catch-up with the jobs being added. - const ieeeAddr = device.ieeeAddr; - if (this.pending.includes(ieeeAddr)) { - logger.debug(`Skipping ping for ${ieeeAddr} becuase job is already in queue`); - return; - } - - this.pending.push(ieeeAddr); - + async handleInterval(device) { // When a device is already unavailable, log the ping failed on 'debug' instead of 'error'. - const errorLogLevel = this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] ? 'debug' : 'error'; - const mechanism = utils.isXiaomiDevice(device) ? 'basic' : 'default'; - - this.zigbee.ping(ieeeAddr, errorLogLevel, (error) => { - this.publishAvailability(ieeeAddr, !error); - - // Remove from pending jobs. - const index = this.pending.indexOf(ieeeAddr); - if (index !== -1) { - this.pending.splice(index, 1); - } - + const ieeeAddr = device.ieeeAddr; + const level = this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] ? 'debug' : 'error'; + try { + await device.ping(); + this.publishAvailability(device, true); + logger.debug(`Succesfully pinged '${device.ieeeAddr}'`); + } catch (error) { + this.publishAvailability(device, false); + logger[level](`Failed to ping '${device.ieeeAddr}'`); + } finally { this.setTimer(device); - }, mechanism); + } } setTimer(device) { @@ -95,47 +80,40 @@ class DeviceAvailability { clearTimeout(this.timers[device.ieeeAddr]); } - this.timers[device.ieeeAddr] = setTimeout(() => { - this.handleInterval(device); + this.timers[device.ieeeAddr] = setTimeout(async () => { + await this.handleInterval(device); }, utils.secondsToMilliseconds(this.availability_timeout)); } - stop() { - this.zigbee.getDevices() - .filter((d) => d.type !== 'Coordinator') - .forEach((device) => this.publishAvailability(device.ieeeAddr, false)); - } - - onReconnect(ieeeAddr) { - const device = this.zigbee.getDevice(ieeeAddr); - let mappedDevice = null; - - if (device && device.modelId) { - mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); + async stop() { + for (const timer of Object.values(this.timers)) { + clearTimeout(timer); } - if (mappedDevice) { - const converters = mappedDevice.toZigbee.filter((tz) => { - return tz.key.find((k) => toZigbeeCandidates.includes(k)); - }); + (await this.zigbee.getClients()).forEach((device) => this.publishAvailability(device, false)); + } - converters.forEach((converter) => { - const convertedResults = converter.convert(null, null, null, 'get'); - if (convertedResults && convertedResults.length) { - convertedResults.forEach((converted) => { - this.zigbee.publish( - ieeeAddr, 'device', converted.cid, converted.cmd, converted.cmdType, - converted.zclData, converted.cfg, null, () => {} - ); - }); + async onReconnect(device) { + if (device && device.modelID) { + const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); + + if (mappedDevice) { + const used = []; + for (const key of toZigbeeCandidates) { + const converter = mappedDevice.toZigbee.find((tz) => tz.key.includes(key)); + if (converter && !used.includes(converter)) { + converter.convertGet(device.endpoints[0], key, {}); + used.push(converter); + } } - }); + } } } - publishAvailability(ieeeAddr, available) { + publishAvailability(device, available) { + const ieeeAddr = device.ieeeAddr; if (this.state.hasOwnProperty(ieeeAddr) && !this.state[ieeeAddr] && available) { - this.onReconnect(ieeeAddr); + this.onReconnect(device); } this.state[ieeeAddr] = available; @@ -146,8 +124,13 @@ class DeviceAvailability { this.mqtt.publish(topic, payload, {retain: true, qos: 0}); } - onZigbeeMessage(message, device, mappedDevice) { - if (device && this.isPingable(this.zigbee.getDevice(device.ieeeAddr))) { + onZigbeeEvent(type, data, mappedDevice, settingsDevice) { + const device = data.device; + if (!device) { + return; + } + + if (this.isPingable(device)) { // When a zigbee message from a device is received we know the device is still alive. // => reset the timer. this.setTimer(device); @@ -157,22 +140,23 @@ class DeviceAvailability { if (!online && !offline) { // A new device has been connected - this.publishAvailability(device.ieeeAddr, true); + this.publishAvailability(device, true); } else if (offline) { // When a message is received and the device is marked as offline, mark it online. - this.publishAvailability(device.ieeeAddr, true); - } else if (online) { - /** - * In case the device is powered off AND on within the availability timeout, - * zigbee2qmtt does not detect the device as offline (device is still marked online). - * When a device is turned on again the state could be out of sync. - * https://github.com/Koenkk/zigbee2mqtt/issues/1383#issuecomment-489412168 - * endDeviceAnnce is typically send when a device comes online. - * - * This isn't needed for TRADFRI devices as they already send the state themself. - */ - if (message.type === 'endDeviceAnnce' && !utils.isIkeaTradfriDevice(device)) { - this.onReconnect(device.ieeeAddr); + this.publishAvailability(device, true); + } else { + /* istanbul ignore else */ + if (online && type === 'deviceAnnounce' && !utils.isIkeaTradfriDevice(device)) { + /** + * In case the device is powered off AND on within the availability timeout, + * zigbee2qmtt does not detect the device as offline (device is still marked online). + * When a device is turned on again the state could be out of sync. + * https://github.com/Koenkk/zigbee2mqtt/issues/1383#issuecomment-489412168 + * endDeviceAnnce is typically send when a device comes online. + * + * This isn't needed for TRADFRI devices as they already send the state themself. + */ + this.onReconnect(device); } } } diff --git a/lib/extension/deviceBind.js b/lib/extension/deviceBind.js index 0d35b7fef..dc1127108 100644 --- a/lib/extension/deviceBind.js +++ b/lib/extension/deviceBind.js @@ -1,16 +1,9 @@ const settings = require('../util/settings'); const logger = require('../util/logger'); -const utils = require('../util/utils'); - -const postfixes = utils.getPostfixes(); +const assert = require('assert'); const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/(bind|unbind)/.+$`); -const allowedClusters = [ - 5, // genScenes - 6, // genOnOff - 8, // genLevelCtrl - 768, // lightingColorCtrl -]; +const clusters = ['genScenes', 'genOnOff', 'genLevelCtrl', 'lightingColorCtrl']; class DeviceBind { constructor(zigbee, mqtt, state, publishEntityState) { @@ -27,101 +20,58 @@ class DeviceBind { this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/unbind/+/+`); } - getIDAndPostfix(topic) { - let postfix = null; - if (postfixes.find((p) => topic.endsWith(`/${p}`))) { - postfix = topic.substr(topic.lastIndexOf('/') + 1, topic.length); - - // Remove postfix from topic - topic = topic.replace(`/${postfix}`, ''); - } - - return {ID: topic, postfix}; - } - - parseTopic(topic) { + async onMQTTMessage(topic, message) { if (!topic.match(topicRegex)) { return null; } - // Remove base from topic + // Parse topic, retrieve type (bind or unbind) and source topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/`, ''); - - // Parse type from topic const type = topic.split('/')[0]; + const sourceKey = topic.replace(`${type}/`, ''); + const targetKey = message; - // Remove type from topic - topic = topic.replace(`${type}/`, ''); - return {type, ...this.getIDAndPostfix(topic)}; - } + // Find source; can only be a device and target + const source = await this.zigbee.resolveEntity(sourceKey); + assert(source != null && source.type === 'device', 'Source undefined or not a device'); + const target = await this.zigbee.resolveEntity(targetKey); + assert(target != null, 'Target is unknown'); - onMQTTMessage(topic, message) { - topic = this.parseTopic(topic); - - if (!topic) { - return false; - } - - // Find source; can only be a device. - const sourceEntity = settings.resolveEntity(topic.ID); - const source = utils.getEndpointByEntityID(this.zigbee, sourceEntity.ID, topic.postfix); - - const targetEntityIDPostfix= this.getIDAndPostfix(message.toString()); - const targetEntity = settings.resolveEntity(targetEntityIDPostfix.ID); - let target = null; - if (targetEntity.type === 'device') { - target = utils.getEndpointByEntityID(this.zigbee, targetEntity.ID, targetEntityIDPostfix.postfix); - } else if (targetEntity.type === 'group') { - target = targetEntity.ID; - } - - if (!source || !target) { - return false; - } + const sourceName = source.settings.friendlyName; + const targetName = target.settings.friendlyName; // Find which clusters are supported by both the source and target. - // Groups are assumed to support all clusters (as we don't know which devices are in) - let supported = []; - if (targetEntity.type === 'device') { - supported = target.getSimpleDesc().inClusterList.filter((cluster) => { - return allowedClusters.includes(cluster); - }); - } else if (targetEntity.type === 'group') { - supported = allowedClusters; - } + // Groups are assumed to support all clusters. + for (const cluster of clusters) { + const targetValid = target.type === 'group' || + target.device.type === 'Coordinator' || target.endpoint.supportsInputCluster(cluster); - const clusters = source.getSimpleDesc().outClusterList.filter((cluster) => { - return supported.includes(cluster); - }); + if (source.endpoint.supportsOutputCluster(cluster) && targetValid) { + logger.debug(`${type}ing cluster '${cluster}' from '${sourceName}' to '${targetName}'`); + try { + const bindTarget = target.type === 'group' ? target.group : target.endpoint; + if (type === 'bind') { + await source.endpoint.bind(cluster, bindTarget); + } else { + await source.endpoint.unbind(cluster, bindTarget); + } - // Bind - clusters.forEach((cluster) => { - logger.debug( - `${topic.type}ing cluster '${cluster}' from '${sourceEntity.ID}' ` + - `(${source.epId}) to '${targetEntity.ID}'` - ); - - this.zigbee[topic.type](source, cluster, target, (error) => { - if (error) { - logger.error( - `Failed to ${topic.type} cluster '${cluster}' from '${sourceEntity.ID}' to ` + - `'${targetEntity.ID}' (${error})` - ); - } else { logger.info( - `Successfully ${topic.type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from ` + - `'${sourceEntity.ID}' to '${targetEntity.ID}'` + `Successfully ${type === 'bind' ? 'bound' : 'unbound'} cluster '${cluster}' from ` + + `'${sourceName}' to '${targetName}'` ); - this.mqtt.log( - `device_${topic.type}`, - {from: sourceEntity.ID, to: targetEntity.ID, cluster} + `device_${type}`, + {from: sourceName, to: targetName, cluster} + ); + } catch (error) { + logger.error( + `Failed to ${type} cluster '${cluster}' from '${sourceName}' to ` + + `'${targetName}' (${error})` ); } - }); - }); - - return true; + } + } } } diff --git a/lib/extension/deviceConfigure.js b/lib/extension/deviceConfigure.js index b51d41c9d..fd7241d79 100644 --- a/lib/extension/deviceConfigure.js +++ b/lib/extension/deviceConfigure.js @@ -1,88 +1,81 @@ const settings = require('../util/settings'); const logger = require('../util/logger'); -const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); -const Queue = require('queue'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -/** - * This extensions handles configuration of devices. - */ class DeviceConfigure { constructor(zigbee, mqtt, state, publishEntityState) { this.zigbee = zigbee; - this.configured = {}; + this.configuring = new Set(); this.attempts = {}; - - this.queue = new Queue(); - this.queue.concurrency = 1; - this.queue.autostart = true; } - onZigbeeStarted() { - this.zigbee.getAllClients().forEach((device) => { - const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); + shouldConfigure(device, mappedDevice) { + if (!device) { + return false; + } - if (mappedDevice) { - this.configure(device.ieeeAddr, mappedDevice); - } - }); + if (device.meta.hasOwnProperty('configured') && device.meta.configured === mappedDevice.meta.configureKey) { + return false; + } + + if (!mappedDevice || !mappedDevice.configure) { + return false; + } + + if (device.interviewing || !device.interviewCompleted) { + return false; + } + + return true; } - onZigbeeMessage(message, device, mappedDevice) { - if (device && mappedDevice) { - const ieeeAddr = device.ieeeAddr; - if (!this.configured[ieeeAddr]) { - this.configure(ieeeAddr, mappedDevice); + async onZigbeeStarted() { + this.coordinatorEndpoint = (await this.zigbee.getDevice({type: 'Coordinator'})).endpoints[0]; + + const devices = await this.zigbee.getClients(); + for (const device of devices) { + const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); + const settingsDevice = settings.getDevice(device.ieeeAddr); + if (this.shouldConfigure(device, mappedDevice)) { + await this.configure(device, mappedDevice, settingsDevice); } } } - configure(ieeeAddr, mappedDevice) { - if (!this.attempts.hasOwnProperty(ieeeAddr)) { - this.attempts[ieeeAddr] = 0; + onZigbeeEvent(type, data, mappedDevice, settingsDevice) { + const device = data.device; + if (this.shouldConfigure(device, mappedDevice)) { + this.configure(device, mappedDevice, settingsDevice); + } + } + + async configure(device, mappedDevice, settingsDevice) { + if (this.configuring.has(device.ieeeAddr) || this.attempts[device.ieeeAddr] >= 3) { + return false; } - if (mappedDevice.configure) { - const friendlyName = settings.getDevice(ieeeAddr) ? settings.getDevice(ieeeAddr).friendly_name : 'unknown'; - // mark device as currently being configured to prevent race conditions - this.configured[ieeeAddr] = 'configuring'; + this.configuring.add(device.ieeeAddr); - this.queue.push((queueCallback) => { - logger.debug(`Configuring ${friendlyName} (${ieeeAddr}) ...`); - - // Call configure function of this device. - mappedDevice.configure(ieeeAddr, this.zigbee.shepherd, this.zigbee.getCoordinator(), (ok, msg) => { - if (ok) { - logger.info(`Successfully configured ${friendlyName} (${ieeeAddr})`); - this.configured[ieeeAddr] = 'configured'; - } else { - // If a device is not joined in the network the configure command may fail - // (especially, immediately after zigbee start) - // also, this command may fail if the network is busy or lost data packets - if (this.attempts[ieeeAddr] > 1) { - // Give up after 3 attempts. - this.configured[ieeeAddr] = 'failed'; - } else { - // we're not done yet so mark not busy - delete this.configured[ieeeAddr]; - } - - logger.warn( - `Failed to configure ${friendlyName} (${ieeeAddr}) ('${msg}')` + - ` (attempt #${this.attempts[ieeeAddr] + 1})` - ); - - logger.warn(`This can be ignored if the device is working properly`); - - this.attempts[ieeeAddr] += 1; - } - - queueCallback(); - }); - }); - } else { - // no need to configure anyting. So just mark as done - this.configured[ieeeAddr] = 'configured'; + if (!this.attempts.hasOwnProperty(device.ieeeAddr)) { + this.attempts[device.ieeeAddr] = 0; } + + logger.info(`Configuring '${settingsDevice.friendlyName}'`); + try { + await mappedDevice.configure(device, this.coordinatorEndpoint); + logger.info(`Succesfully configured '${settingsDevice.friendlyName}'`); + // eslint-disable-next-line + device.meta.configured = mappedDevice.meta.configureKey; + await device.save(); + } catch (error) { + logger.error( + `Failed to configure '${settingsDevice.friendlyName}', ` + + `attempt ${this.attempts[device.ieeeAddr] + 1} (${error})` + ); + this.attempts[device.ieeeAddr]++; + } + + this.configuring.delete(device.ieeeAddr); } } diff --git a/lib/extension/deviceGroupMembership.js b/lib/extension/deviceGroupMembership.js index c77fd721a..951e6edd5 100644 --- a/lib/extension/deviceGroupMembership.js +++ b/lib/extension/deviceGroupMembership.js @@ -1,7 +1,8 @@ +/* istanbul ignore file */ const settings = require('../util/settings'); const logger = require('../util/logger'); -const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/device/.+/get_group_membership$`); +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/device/(.+)/get_group_membership$`); class DeviceGroupMembership { constructor(zigbee, mqtt, publishEntityState) { @@ -14,59 +15,33 @@ class DeviceGroupMembership { this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/device/+/get_group_membership`); } - parseTopic(topic) { - if (!topic.match(topicRegex)) { + async onMQTTMessage(topic, message) { + const match = topic.match(topicRegex); + if (!match) { return null; } - // Remove base from topic - topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/device/`, ''); - - // Remove command from topic - topic = topic.replace(`/get_group_membership`, ''); - - return {friendlyName: topic}; - } - - onMQTTMessage(topic, message) { - topic = this.parseTopic(topic); - - if (!topic) { - return false; - } - - // Map message to ieeeAddr and check if device exist. - const friendlyName = topic.friendlyName; - const ieeeAddr = settings.getIeeeAddrByFriendlyName(friendlyName) || friendlyName; - if (!this.zigbee.getDevice(ieeeAddr)) { - logger.error(`Failed to find device '${friendlyName}'`); + const entity = await this.zigbee.resolveEntity(match[1]); + if (!entity || entity.type !== 'device') { + logger.error(`Device '${match[1]}' does not exist`); return; } - const callback = (error, rsp) => { - if (error) { - logger.error(`Failed to get membership of ${ieeeAddr}`); - } else { - const {grouplist, capacity} = rsp; - const msgGroupList = `${ieeeAddr} is in groups [${grouplist}]`; - let msgCapacity; - if (capacity === 254) { - msgCapacity = 'it can be a part of at least 1 more group'; - } else { - msgCapacity = `its remaining group capacity is ${capacity === 255 ? 'unknown' : capacity}`; - } - logger.info(`${msgGroupList} and ${msgCapacity}`); - - this.publishEntityState(ieeeAddr, {group_list: grouplist, group_capacity: capacity}); - } - }; - - this.zigbee.publish( - ieeeAddr, 'device', 'genGroups', 'getMembership', 'functional', - {groupcount: 0, grouplist: []}, null, null, callback, + const response = await entity.endpoint.command( + `genGroups`, 'getMembership', {groupcount: 0, grouplist: []}, {}, true ); - return true; + const {grouplist, capacity} = response; + const msgGroupList = `${entity.device.ieeeAddr} is in groups [${grouplist}]`; + let msgCapacity; + if (capacity === 254) { + msgCapacity = 'it can be a part of at least 1 more group'; + } else { + msgCapacity = `its remaining group capacity is ${capacity === 255 ? 'unknown' : capacity}`; + } + logger.info(`${msgGroupList} and ${msgCapacity}`); + + this.publishEntityState(entity.device.ieeeAddr, {group_list: grouplist, group_capacity: capacity}); } } diff --git a/lib/extension/devicePublish.js b/lib/extension/devicePublish.js deleted file mode 100644 index 5a8ad6a90..000000000 --- a/lib/extension/devicePublish.js +++ /dev/null @@ -1,235 +0,0 @@ - -const settings = require('../util/settings'); -const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); -const logger = require('../util/logger'); -const utils = require('../util/utils'); -const objectAssignDeep = require(`object-assign-deep`); - -const postfixes = utils.getPostfixes(); -const topicRegex = new RegExp(`^(.+?)(?:/(${postfixes.join('|')}))?/(get|set)(?:/(.+))?`); - -const maxDepth = 20; - -const groupConverters = [ - zigbeeShepherdConverters.toZigbeeConverters.light_onoff_brightness, - zigbeeShepherdConverters.toZigbeeConverters.light_colortemp, - zigbeeShepherdConverters.toZigbeeConverters.light_color, - zigbeeShepherdConverters.toZigbeeConverters.light_alert, - zigbeeShepherdConverters.toZigbeeConverters.ignore_transition, -]; - -class DevicePublish { - constructor(zigbee, mqtt, state, publishEntityState) { - this.zigbee = zigbee; - this.mqtt = mqtt; - this.state = state; - this.publishEntityState = publishEntityState; - } - - onMQTTConnected() { - // Subscribe to topics. - const baseTopic = settings.get().mqtt.base_topic; - for (let step = 1; step < maxDepth; step++) { - const topic = `${baseTopic}/${'+/'.repeat(step)}`; - this.mqtt.subscribe(`${topic}set`); - this.mqtt.subscribe(`${topic}set/+`); - this.mqtt.subscribe(`${topic}get`); - this.mqtt.subscribe(`${topic}get/+`); - } - } - - parseTopic(topic) { - const match = topic.match(topicRegex); - if (!match) { - return null; - } - - const ID = match[1].replace(`${settings.get().mqtt.base_topic}/`, ''); - // If we didn't repalce base_topic we received something we don't care about - if (ID === match[1] || ID.match(/bridge\/config/)) { - return null; - } - - return {ID: ID, postfix: match[2] || '', type: match[3], attribute: match[4]}; - } - - handlePublishError(entity, message, error) { - const meta = { - entity, - message: message.toString(), - }; - - this.mqtt.log('zigbee_publish_error', error.toString(), meta); - } - - handlePublished(entity, topic, converter, converted, key, value) { - if (topic.type === 'set' && converted.hasOwnProperty('newState') && converted.newState) { - const msg = converted.newState; - - if (topic.postfix) { - msg[`state_${topic.postfix}`] = msg.state; - delete msg.state; - } - - if (settings.get().advanced.last_seen !== 'disable') { - msg.last_seen = utils.formatDate(Date.now(), settings.get().advanced.last_seen); - } - - this.publishEntityState(entity.ID, msg); - } - } - - onMQTTMessage(topic, message) { - topic = this.parseTopic(topic); - - if (!topic) { - return false; - } - - // Resolve the entity - const entity = settings.resolveEntity(topic.ID); - - // Get entity details - let endpoint = null; - let converters = null; - let device = null; - let options = {}; - - if (entity.type === 'device') { - device = this.zigbee.getDevice(entity.ID); - if (!device) { - logger.error(`Failed to find device with ieeeAddr: '${entity.ID}'`); - this.mqtt.log('entity_not_found', entity.ID); - return; - } - - // Map device to a model - const model = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); - if (!model) { - logger.warn(`Device with modelID '${device.modelId}' is not supported.`); - logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`); - return; - } - - // Determine endpoint to publish to. - if (model.hasOwnProperty('ep')) { - const eps = model.ep(device); - endpoint = eps.hasOwnProperty(topic.postfix) ? eps[topic.postfix] : null; - if (endpoint === null && eps.hasOwnProperty('default')) { - endpoint = eps['default']; - } - } - - converters = model.toZigbee; - - const deviceSettings = settings.getDevice(entity.ID); - options = objectAssignDeep.noMutate({}, model.options, deviceSettings); - } else if (entity.type === 'group') { - converters = groupConverters; - options = objectAssignDeep.noMutate({}, settings.getGroup(entity.ID)); - } - - // Convert the MQTT message to a Zigbee message. - let json = {}; - if (topic.hasOwnProperty('attribute') && topic.attribute) { - json[topic.attribute] = message.toString(); - } else { - try { - json = JSON.parse(message); - } catch (e) { - // Cannot be parsed to JSON, assume state message. - json = {state: message.toString()}; - } - } - - /** - * Home Assistant always publishes 'state', even when e.g. only setting - * the color temperature. This would lead to 2 zigbee publishes, where the first one - * (state) is probably unecessary. - */ - if (settings.get().homeassistant) { - const deviceState = this.state.get(entity.ID); - const hasColorTemp = json.hasOwnProperty('color_temp'); - const hasColor = json.hasOwnProperty('color'); - const hasBrightness = json.hasOwnProperty('brightness'); - const isOn = deviceState && deviceState.state === 'ON' ? true : false; - if (isOn && (hasColorTemp || hasColor) && !hasBrightness) { - delete json.state; - logger.debug('Skipping state because of Home Assistant'); - } - } - - // Ensure that state and brightness are executed before other commands. - const keys = Object.keys(json); - keys.sort((a, b) => (['state', 'brightness'].includes(a) ? -1 : 1)); - - // For each key in the JSON message find the matching converter. - const usedConverters = []; - keys.forEach((key) => { - const converter = converters.find((c) => c.key.includes(key)); - - if (usedConverters.includes(converter)) { - // Use a converter only once (e.g. light_onoff_brightness converters can convert state and brightness) - return; - } - - if (!converter) { - logger.error(`No converter available for '${key}' (${json[key]})`); - return; - } - - // Converter didn't return a result, skip - const convertedResults = converter.convert(key, json[key], json, topic.type, topic.postfix, options); - if (!convertedResults || !convertedResults.length) { - return; - } - - convertedResults.forEach((converted) => { - this.zigbee.publish( - entity.ID, - entity.type, - converted.cid, - converted.cmd, - converted.cmdType, - converted.zclData, - converted.cfg, - endpoint, - (error, rsp) => { - if (!error) { - this.handlePublished(entity, topic, converter, converted, key, json[key]); - } else { - this.handlePublishError(entity, message, error); - } - } - ); - - // It's possible for devices to get out of sync when writing an attribute that's not reportable. - // So here we re-read the value after a specified timeout, this timeout could for example be the - // transition time of a color change or for forcing a state read for devices that don't - // automatically report a new state when set. - // When reporting is requested for a device (report: true in device-specific settings) we won't - // ever issue a read here, as we assume the device will properly report changes. - // Only do this when the retrieve_state option is enabled for this device. - const deviceSettings = settings.getDevice(entity.ID); - if (topic.type === 'set' && entity.type === 'device' && - converted.hasOwnProperty('readAfterWriteTime') && deviceSettings && deviceSettings.retrieve_state) { - const getConvertedResults = converter.convert(key, json[key], json, 'get', '', options); - getConvertedResults.forEach((getConverted) => { - setTimeout(() => { - this.zigbee.publish( - entity.ID, entity.type, getConverted.cid, getConverted.cmd, getConverted.cmdType, - getConverted.zclData, getConverted.cfg, endpoint, () => {} - ); - }, converted.readAfterWriteTime); - }); - } - - usedConverters.push(converter); - }); - }); - - return true; - } -} - -module.exports = DevicePublish; diff --git a/lib/extension/deviceReceive.js b/lib/extension/deviceReceive.js index c2c2a5894..260e5c4f9 100644 --- a/lib/extension/deviceReceive.js +++ b/lib/extension/deviceReceive.js @@ -3,9 +3,6 @@ const logger = require('../util/logger'); const utils = require('../util/utils'); const debounce = require('debounce'); -/** - * This extensions handles messages received from devices. - */ class DeviceReceive { constructor(zigbee, mqtt, state, publishEntityState) { this.zigbee = zigbee; @@ -17,8 +14,8 @@ class DeviceReceive { this.debouncers = {}; } - onZigbeeStarted() { - this.coordinator = this.zigbee.getCoordinator().device.ieeeAddr; + async onZigbeeStarted() { + this.coordinator = await this.zigbee.getDevice({type: 'Coordinator'}); } publishDebounce(ieeeAddr, payload, time) { @@ -36,48 +33,14 @@ class DeviceReceive { this.debouncers[ieeeAddr].publish(); } - onZigbeeMessage(message, device, mappedDevice) { - if (message.type == 'devInterview') { - if (!settings.getDevice(message.data)) { - logger.info('Connecting with device...'); - this.mqtt.log('pairing', 'connecting with device'); - } - - return; + canHandleEvent(type, data, mappedDevice) { + if (type !== 'message') { + return false; } - if (message.type == 'devIncoming') { - logger.info('Device incoming...'); - this.mqtt.log('pairing', 'device incoming'); - } - - if (!device) { - logger.warn('Message without device!'); - return; - } - - if (device.ieeeAddr === this.coordinator) { + if (data.device.ieeeAddr === this.coordinator.ieeeAddr) { logger.debug('Ignoring message from coordinator'); - return; - } - - // Check if this is a new device. - const settingsDevice = settings.getDevice(device.ieeeAddr); - if (!settingsDevice) { - logger.info(`New device '${device.modelId}' with address ${device.ieeeAddr} connected!`); - settings.addDevice(device.ieeeAddr); - this.mqtt.log('device_connected', device.ieeeAddr, {modelID: device.modelId}); - } - - if (!mappedDevice) { - logger.warn(`Device with modelID '${device.modelId}' is not supported.`); - logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`); - return; - } - - // After this point we cant handle message withoud cid or cmdId anymore. - if (!message.data || (!message.data.cid && !message.data.cmdId)) { - return; + return false; } /** @@ -93,55 +56,46 @@ class DeviceReceive { * As the same message is also received directly from the end device, it makes no sense * to handle these messages. */ - const hasGroupID = message.hasOwnProperty('groupid') && message.groupid != 0; - if (utils.isXiaomiDevice(device) && utils.isRouter(device) && hasGroupID) { + const hasGroupID = data.hasOwnProperty('groupID') && data.groupID != 0; + if (utils.isXiaomiDevice(data.device) && utils.isRouter(data.device) && hasGroupID) { logger.debug('Skipping re-transmitted Xiaomi message'); + return false; + } + + if (data.device.modelID === null && data.device.interviewing) { + logger.debug(`Skipping message, modelID is undefined and still interviewing`); + return false; + } + + if (!mappedDevice) { + logger.warn(`Received message from unsupported device with Zigbee model '${data.device.modelID}'`); + logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`); + return false; + } + + return true; + } + + onZigbeeEvent(type, data, mappedDevice, settingsDevice) { + if (!this.canHandleEvent(type, data, mappedDevice)) { return; } - // Find a conveter for this message. - const cid = message.data.cid; - const cmdId = message.data.cmdId; const converters = mappedDevice.fromZigbee.filter((c) => { - if (cid) { - // readRsp messages have the same structure as attReport messages. - // search for attReport converters on readRsp. - if (c.cid === cid) { - if (c.type instanceof Array) { - return c.type.includes(message.type); - } else { - return c.type === message.type; - } - } - - return false; - } else if (cmdId) { - return c.cmd === cmdId; - } - - return false; + const type = Array.isArray(c.type) ? c.type.includes(data.type) : c.type === data.type; + return c.cluster === data.cluster && type; }); // Check if there is an available converter if (!converters.length) { - if (cid) { - // Don't log readRsp messages, they are not interesting most of the time. - if (message.type === 'readRsp') { - return; - } - + // Don't log readRsp messages, they are not interesting most of the time. + if (data.type !== 'readResponse') { logger.warn( - `No converter available for '${mappedDevice.model}' with cid '${cid}', ` + - `type '${message.type}' and data '${JSON.stringify(message.data)}'` - ); - } else if (cmdId) { - logger.warn( - `No converter available for '${mappedDevice.model}' with cmd '${cmdId}' ` + - `and data '${JSON.stringify(message.data)}'` + `No converter available for '${mappedDevice.model}' with cluster '${data.cluster}' ` + + `and type '${data.type}' and data '${JSON.stringify(data.data)}'` ); + logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`); } - - logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`); return; } @@ -152,8 +106,8 @@ class DeviceReceive { // for e.g. click switches where we need to count number of clicks and detect long presses. const publish = (payload) => { // Add device linkquality. - if (message.hasOwnProperty('linkquality')) { - payload.linkquality = message.linkquality; + if (data.hasOwnProperty('linkquality')) { + payload.linkquality = data.linkquality; } // Add last seen timestamp @@ -163,18 +117,18 @@ class DeviceReceive { } if (settings.get().advanced.elapsed) { - if (this.elapsed[device.ieeeAddr]) { - payload.elapsed = now - this.elapsed[device.ieeeAddr]; + if (this.elapsed[data.device.ieeeAddr]) { + payload.elapsed = now - this.elapsed[data.device.ieeeAddr]; } - this.elapsed[device.ieeeAddr] = now; + this.elapsed[data.device.ieeeAddr] = now; } // Check if we have to debounce if (settingsDevice && settingsDevice.hasOwnProperty('debounce')) { - this.publishDebounce(device.ieeeAddr, payload, settingsDevice.debounce); + this.publishDebounce(data.device.ieeeAddr, payload, settingsDevice.debounce); } else { - this.publishEntityState(device.ieeeAddr, payload); + this.publishEntityState(data.device.ieeeAddr, payload); if (settings.get().homeassistant) { /** @@ -186,7 +140,7 @@ class DeviceReceive { if (['action', 'click'].includes(key)) { const counterPayload = {}; counterPayload[key] = ''; - this.publishEntityState(device.ieeeAddr, counterPayload); + this.publishEntityState(data.device.ieeeAddr, counterPayload); } }); } @@ -195,9 +149,8 @@ class DeviceReceive { let payload = {}; converters.forEach((converter) => { - const options = {...settings.get().device_options, ...settings.getDevice(device.ieeeAddr)}; - const converted = converter.convert(mappedDevice, message, publish, options); - + const options = {...settings.get().device_options, ...settings.getDevice(data.device.ieeeAddr)}; + const converted = converter.convert(mappedDevice, data, publish, options); if (converted) { payload = {...payload, ...converted}; } diff --git a/lib/extension/deviceReport.js b/lib/extension/deviceReport.js index 32db72d5c..cb6e02dd1 100644 --- a/lib/extension/deviceReport.js +++ b/lib/extension/deviceReport.js @@ -1,34 +1,31 @@ -const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); const logger = require('../util/logger'); -const CC2530Router = zigbeeShepherdConverters.devices.find((d) => d.model === 'CC2530.ROUTER'); +const CC2530Router = zigbeeHerdsmanConverters.devices.find((d) => d.model === 'CC2530.ROUTER'); const utils = require('../util/utils'); -const candidates = { - 'genOnOff': { - attrs: ['onOff'], - reportIntervalMin: 3, - reportIntervalMax: 300, - reportableChange: 0, - }, - 'genLevelCtrl': { - attrs: ['currentLevel'], - }, - 'lightingColorCtrl': { - attrs: ['colorTemperature', 'currentX', 'currentY'], - }, - 'closuresWindowCovering': { - attrs: ['currentPositionLiftPercentage', 'currentPositionTiltPercentage'], - reportIntervalMin: 2, - reportIntervalMax: 300, - }, +const defaultConfiguration = { + minimumReportInterval: 3, maximumReportInterval: 300, reportableChange: 0, }; -const reportInterval = { - min: 3, - max: 3600, -}; +const reportKey = 1; -const reportableChange = 1; +const clusters = { + 'genOnOff': [ + {attribute: 'onOff', ...defaultConfiguration, minimumReportInterval: 0, reportableChange: 0}, + ], + 'genLevelCtrl': [ + {attribute: 'currentLevel', ...defaultConfiguration}, + ], + 'lightingColorCtrl': [ + {attribute: 'colorTemperature', ...defaultConfiguration}, + {attribute: 'currentX', ...defaultConfiguration}, + {attribute: 'currentY', ...defaultConfiguration}, + ], + 'closuresWindowCovering': [ + {attribute: 'currentPositionLiftPercentage', ...defaultConfiguration}, + {attribute: 'currentPositionTiltPercentage', ...defaultConfiguration}, + ], +}; class DeviceReport { constructor(zigbee, mqtt, state, publishEntityState) { @@ -36,83 +33,69 @@ class DeviceReport { this.mqtt = mqtt; this.state = state; this.publishEntityState = publishEntityState; + this.configuring = new Set(); } - setupReporting(mappedDevice, device) { - let epId = null; + async setupReporting(device) { + if (this.configuring.has(device.ieeeAddr)) return; + this.configuring.add(device.ieeeAddr); - if (mappedDevice === CC2530Router) { - logger.debug('Not setting up reporting for CC2530 router'); - return; - } - - // Check if this device uses a different epId. - if (mappedDevice.hasOwnProperty('ep')) { - const eps = mappedDevice.ep(device); - epId = eps[''] || null; - } - - const endpoint = this.zigbee.getEndpoint(device.ieeeAddr, epId); - - if (!endpoint) { - logger.error(`Failed to setup reporting for ${device.ieeeAddr}, endpoint not found`); - return; - } - - logger.debug(`Setting up reporting for ${device.ieeeAddr}`); - Object.values(endpoint.clusters).filter((c) => c).forEach((c) => { - const cluster = c.attrs.cid; - if (candidates[cluster]) { - const candidate = candidates[cluster]; - let attributeNames = candidate.attrs.filter((a) => c.attrs.hasOwnProperty(a)); - - // Sometimes a cluster has no attributes, in this case setup reporting for all attributes. - attributeNames = attributeNames.length ? attributeNames : candidate.attrs; - - const attributes = attributeNames.map((attribute) => { - return { - attr: attribute, - min: candidate.hasOwnProperty('reportIntervalMin')? - candidate.reportIntervalMin:reportInterval.min, - max: candidate.hasOwnProperty('reportIntervalMax')? - candidate.reportIntervalMax:reportInterval.max, - change: candidate.hasOwnProperty('reportableChange')? - candidate.reportableChange:reportableChange, - }; - }); - - if (attributes.length > 0) { - this.zigbee.report(endpoint, cluster, attributes); + try { + for (const endpoint of device.endpoints) { + for (const [cluster, configuration] of Object.entries(clusters)) { + if (endpoint.supportsInputCluster(cluster)) { + logger.debug(`Setup reporting for '${device.ieeeAddr}' - ${endpoint.ID} - ${cluster}`); + await endpoint.bind(cluster, this.coordinatorEndpoint); + await endpoint.configureReporting(cluster, configuration); + logger.info( + `Succesfully setup reporting for '${device.ieeeAddr}' - ${endpoint.ID} - ${cluster}` + ); + } } } - }); + + // eslint-disable-next-line + device.meta.reporting = reportKey; + } catch (error) { + logger.error( + `Failed to setup reporting for '${device.ieeeAddr}' - ${error}` + ); + } + + await device.save(); + this.configuring.delete(device.ieeeAddr); } - shouldSetupReporting(device) { - return utils.isRouter(device) && !utils.isBatteryPowered(device); - } + shouldSetupReporting(mappedDevice, device, messageType) { + if (!device) return false; - onZigbeeStarted() { - this.zigbee.getAllClients() - .filter((d) => this.shouldSetupReporting(d)) - .forEach((device) => { - const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); - - if (mappedDevice) { - this.setupReporting(mappedDevice, device); - } - }); - } - - onZigbeeMessage(message, device, mappedDevice) { // Handle messages of type endDeviceAnnce and devIncoming. // This message is typically send when a device comes online after being powered off // Ikea TRADFRI tend to forget their reporting after powered off. // Re-setup reporting. // https://github.com/Koenkk/zigbee2mqtt/issues/966 - if (device && mappedDevice && ['endDeviceAnnce', 'devIncoming'].includes(message.type) && - this.shouldSetupReporting(device)) { - this.setupReporting(mappedDevice, device); + if (messageType === 'deviceAnnounce' && utils.isIkeaTradfriDevice(device)) return true; + + if (device.meta.hasOwnProperty('reporting') && device.meta.reporting === reportKey) return false; + if (!utils.isRouter(device) || utils.isBatteryPowered(device)) return false; + if (mappedDevice === CC2530Router) return false; + return true; + } + + async onZigbeeStarted() { + this.coordinatorEndpoint = (await this.zigbee.getDevice({type: 'Coordinator'})).endpoints[0]; + + for (const device of await this.zigbee.getClients()) { + const mappedDevice = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); + if (this.shouldSetupReporting(mappedDevice, device, null)) { + this.setupReporting(device); + } + } + } + + onZigbeeEvent(type, data, mappedDevice, settingsDevice) { + if (this.shouldSetupReporting(mappedDevice, data.device, type)) { + this.setupReporting(data.device); } } } diff --git a/lib/extension/entityPublish.js b/lib/extension/entityPublish.js new file mode 100644 index 000000000..2e0d084f0 --- /dev/null +++ b/lib/extension/entityPublish.js @@ -0,0 +1,201 @@ + +const settings = require('../util/settings'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); +const logger = require('../util/logger'); +const utils = require('../util/utils'); +const assert = require('assert'); + +const postfixes = utils.getPostfixes(); +const topicRegex = new RegExp(`^(.+?)(?:/(${postfixes.join('|')}))?/(get|set)(?:/(.+))?`); + +const groupConverters = [ + zigbeeHerdsmanConverters.toZigbeeConverters.light_onoff_brightness, + zigbeeHerdsmanConverters.toZigbeeConverters.light_colortemp, + zigbeeHerdsmanConverters.toZigbeeConverters.light_color, + zigbeeHerdsmanConverters.toZigbeeConverters.light_alert, + zigbeeHerdsmanConverters.toZigbeeConverters.ignore_transition, +]; + +class EntityPublish { + constructor(zigbee, mqtt, state, publishEntityState) { + this.zigbee = zigbee; + this.mqtt = mqtt; + this.state = state; + this.publishEntityState = publishEntityState; + } + + onMQTTConnected() { + // Subscribe to topics. + const baseTopic = settings.get().mqtt.base_topic; + for (let step = 1; step < 20; step++) { + const topic = `${baseTopic}/${'+/'.repeat(step)}`; + this.mqtt.subscribe(`${topic}set`); + this.mqtt.subscribe(`${topic}set/+`); + this.mqtt.subscribe(`${topic}get`); + this.mqtt.subscribe(`${topic}get/+`); + } + } + + parseTopic(topic) { + const match = topic.match(topicRegex); + if (!match) { + return null; + } + + const ID = match[1].replace(`${settings.get().mqtt.base_topic}/`, ''); + // If we didn't repalce base_topic we received something we don't care about + if (ID === match[1] || ID.match(/bridge/)) { + return null; + } + + return {ID: ID, postfix: match[2] || '', type: match[3], attribute: match[4]}; + } + + async onMQTTMessage(topic, message) { + topic = this.parseTopic(topic); + if (!topic) { + return false; + } + + const entityName = `${topic.ID}` + (topic.postfix ? `/${topic.postfix}` : ''); + const entity = await this.zigbee.resolveEntity(entityName); + + if (!entity) { + this.mqtt.log('entity_not_found', entityName); + logger.error(`Entity '${entityName}' is unknown`); + return; + } + + // Get entity details + let converters = null; + let target = null; + let options = {}; + + assert(entity.type === 'device' || entity.type === 'group'); + if (entity.type === 'device') { + // Map device to a model + if (!entity.mapped) { + logger.warn(`Device with modelID '${entity.device.modelID}' is not supported.`); + logger.warn(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html`); + return; + } + + target = entity.endpoint; + converters = entity.mapped.toZigbee; + options = entity.mapped.options || {}; + options = {...options, ...entity.settings}; + } else { + converters = groupConverters; + target = entity.group; + } + + // Convert the MQTT message to a Zigbee message. + let json = {}; + if (topic.hasOwnProperty('attribute') && topic.attribute) { + json[topic.attribute] = message; + } else { + try { + json = JSON.parse(message); + } catch (e) { + // Cannot be parsed to JSON, assume state message. + json = {state: message}; + } + } + + /** + * Home Assistant always publishes 'state', even when e.g. only setting + * the color temperature. This would lead to 2 zigbee publishes, where the first one + * (state) is probably unecessary. + */ + if (settings.get().homeassistant) { + const deviceState = this.state.get(entity.settings.ID); + const hasColorTemp = json.hasOwnProperty('color_temp'); + const hasColor = json.hasOwnProperty('color'); + const hasBrightness = json.hasOwnProperty('brightness'); + const isOn = deviceState && deviceState.state === 'ON' ? true : false; + if (isOn && (hasColorTemp || hasColor) && !hasBrightness) { + delete json.state; + logger.debug('Skipping state because of Home Assistant'); + } + } + + // Ensure that state and brightness are executed before other commands. + const entries = Object.entries(json); + entries.sort((a, b) => (['state', 'brightness'].includes(a[0]) ? -1 : 1)); + + // For each attribute call the corresponding converter + const usedConverters = []; + for (const [key, value] of entries) { + const converter = converters.find((c) => c.key.includes(key)); + + if (usedConverters.includes(converter)) { + // Use a converter only once (e.g. light_onoff_brightness converters can convert state and brightness) + continue; + } + + if (!converter) { + logger.error(`No converter available for '${key}' (${json[key]})`); + continue; + } + + // Converter didn't return a result, skip + const meta = { + endpoint_name: topic.postfix, + options, + message: json, + }; + + try { + if (topic.type === 'set' && converter && converter.convertSet) { + logger.debug(`Publishing set '${topic.type}' '${key}' to '${entity.name}'`); + const result = await converter.convertSet(target, key, value, meta); + if (result && result.hasOwnProperty('state')) { + const msg = result.state; + + if (topic.postfix) { + msg[`state_${topic.postfix}`] = msg.state; + delete msg.state; + } + + if (settings.get().advanced.last_seen !== 'disable') { + msg.last_seen = utils.formatDate(Date.now(), settings.get().advanced.last_seen); + } + + this.publishEntityState(entity.settings.ID, msg); + } + + // It's possible for devices to get out of sync when writing an attribute that's not reportable. + // So here we re-read the value after a specified timeout, this timeout could for example be the + // transition time of a color change or for forcing a state read for devices that don't + // automatically report a new state when set. + // When reporting is requested for a device (report: true in device-specific settings) we won't + // ever issue a read here, as we assume the device will properly report changes. + // Only do this when the retrieve_state option is enabled for this device. + if ( + entity.type === 'device' && result && result.hasOwnProperty('readAfterWriteTime') && + entity.settings.retrieve_state + ) { + setTimeout(() => converter.convertGet(target, key, meta), result.readAfterWriteTime); + } + } else if (topic.type === 'get' && converter && converter.convertGet) { + logger.debug(`Publishing get '${topic.type}' '${key}' to '${entity.name}'`); + await converter.convertGet(target, key, meta); + } else { + logger.error(`No converter available for '${topic.type}' '${key}' (${json[key]})`); + continue; + } + } catch (error) { + const message = + `Publish '${topic.type}' '${key}' to '${entity.name}' failed: '${error}'`; + logger.error(message); + this.mqtt.log('zigbee_publish_error', message, {entity: entity.settings.ID}); + } + + usedConverters.push(converter); + } + + return true; + } +} + +module.exports = EntityPublish; diff --git a/lib/extension/extensionTemplate.js b/lib/extension/extensionTemplate.js index 0214d38e4..5eb164459 100644 --- a/lib/extension/extensionTemplate.js +++ b/lib/extension/extensionTemplate.js @@ -1,3 +1,5 @@ +/* istanbul ignore file */ + /** * This extensions is for documentation purposes only. * It describes all methods that are called by the controller. diff --git a/lib/extension/groups.js b/lib/extension/groups.js index 8f7f6b08d..bc7af8628 100644 --- a/lib/extension/groups.js +++ b/lib/extension/groups.js @@ -1,12 +1,8 @@ const settings = require('../util/settings'); const logger = require('../util/logger'); -const data = require('../util/data'); -const utils = require('../util/utils'); -const fs = require('fs'); -const diff = require('deep-diff'); -const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/.+/(remove|add|remove_all)$`); -const topicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(remove|add|remove_all)$`); +const topicRegex = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/(.+)/(remove|add|remove_all)$`); +const topicRegexRemoveAll = new RegExp(`^${settings.get().mqtt.base_topic}/bridge/group/remove_all$`); class Groups { constructor(zigbee, mqtt, state, publishEntityState) { @@ -14,18 +10,6 @@ class Groups { this.mqtt = mqtt; this.state = state; this.publishEntityState = publishEntityState; - this.onStateChange = this.onStateChange.bind(this); - - this.groupsCacheFile = data.joinPathStorage('groups_cache.json'); - this.groupsCache = this.readGroupsCache(); - } - - readGroupsCache() { - return fs.existsSync(this.groupsCacheFile) ? JSON.parse(fs.readFileSync(this.groupsCacheFile, 'utf8')) : {}; - } - - writeGroupsCache() { - fs.writeFileSync(this.groupsCacheFile, JSON.stringify(this.groupsCache), 'utf8'); } onMQTTConnected() { @@ -35,207 +19,111 @@ class Groups { this.mqtt.subscribe(`${settings.get().mqtt.base_topic}/bridge/group/remove_all`); } - apply(from, to) { - const sortGroups = (obj) => Object.keys(obj).forEach((key) => obj[key] = obj[key].sort()); + async onZigbeeStarted() { + await this.syncGroupsWithSettings(); + } - sortGroups(from); - sortGroups(to); + async syncGroupsWithSettings() { + const settingsGroups = settings.getGroups(); + const zigbeeGroups = await this.zigbee.getGroups({}); + for (const settingGroup of settingsGroups) { + const groupID = settingGroup.ID; + const zigbeeGroup = zigbeeGroups.find((g) => g.groupID === groupID) || + (await this.zigbee.createGroup(groupID)); + const settingsEntity = (await Promise.all(settingGroup.devices.map(async (d) => { + const entity = await this.zigbee.resolveEntity(d); + if (!entity) logger.error(`Cannot find '${d}' of group '${settingGroup.friendlyName}'`); + return entity; + }))).filter((e) => e != null); - const differences = diff(from, to); - if (differences) { - differences.forEach((diff) => { - const groupID = diff.path[0]; - - if (diff.kind === 'N') { - diff.rhs.forEach((ID) => this.updateDeviceGroup(ID, 'add', groupID)); - } else if (diff.kind === 'A') { - if (diff.item.lhs) { - this.updateDeviceGroup(diff.item.lhs, 'remove', groupID); - } else { - this.updateDeviceGroup(diff.item.rhs, 'add', groupID); - } - } else if (diff.kind === 'D') { - diff.lhs.forEach((ID) => this.updateDeviceGroup(ID, 'remove', groupID)); - } else if (diff.kind === 'E') { - this.updateDeviceGroup(diff.rhs, 'add', groupID); - this.updateDeviceGroup(diff.lhs, 'remove', groupID); + // In settings but not in zigbee + for (const entity of settingsEntity) { + if (!zigbeeGroup.hasMember(entity.endpoint)) { + logger.info(`Adding '${entity.name}' to group '${settingGroup.friendlyName}'`); + await entity.endpoint.addToGroup(zigbeeGroup); } - }); + } + + // In zigbee but not in settings + for (const endpoint of zigbeeGroup.getMembers()) { + if (!settingsEntity.find((e) => e.endpoint === endpoint)) { + const deviceSettings = settings.getDevice(endpoint.deviceIeeeAddress); + logger.info(`Removing '${deviceSettings.friendlyName}' from group '${settingGroup.friendlyName}'`); + await endpoint.removeFromGroup(zigbeeGroup); + } + } + } + + // eslint-disable-next-line + for (const zigbeeGroup of zigbeeGroups) { + if (!settingsGroups.find((g) => g.ID === zigbeeGroup.groupID)) { + for (const endpoint of zigbeeGroup.getMembers()) { + const deviceSettings = settings.getDevice(endpoint.deviceIeeeAddress); + logger.info(`Removing '${deviceSettings.friendlyName}' from group ${zigbeeGroup.groupID}`); + await endpoint.removeFromGroup(zigbeeGroup); + } + } } } - parseID(ID) { - let entityID = ID; - let endpointID = null; - const postfix = utils.getPostfixes().find((p) => entityID.endsWith(`/${p}`)); - if (postfix) { - // Found a postfix, retrieve the endpoint which correspodns to the postfix - entityID = entityID.substring(0, entityID.length - (postfix.length + 1)); - const endpoint = utils.getEndpointByEntityID(this.zigbee, entityID, postfix); + async onMQTTMessage(topic, message) { + let type; + let group; + const topicMatch = topic.match(topicRegex); + if (topicMatch) { + group = await this.zigbee.resolveEntity(topicMatch[1]); + type = topicMatch[2]; - if (!endpoint) { + if (!group || group.type !== 'group') { + logger.error(`Group '${topicMatch[1]}' does not exist`); return; } - - endpointID = endpoint.epId; + } else if (topic.match(topicRegexRemoveAll)) { + type = 'remove_all'; + } else { + return; } - return {endpointID, entityID}; - } + const entity = await this.zigbee.resolveEntity(message); + if (!entity || !entity.type === 'device') { + logger.error(`Device '${message}' does not exist`); + return; + } - getGroupsOfDevice(entityID) { - return Object.keys(settings.getGroups()).filter((groupID) => { - return settings.getGroup(groupID).devices.includes(entityID); - }); - } + const keys = [ + `${entity.device.ieeeAddr}/${entity.endpoint.ID}`, + `${entity.name}/${entity.endpoint.ID}`, + ]; - onStateChange(ieeeAddr, from, to) { - const properties = ['state', 'brightness', 'color_temp', 'color']; - const payload = {}; + if (entity.endpointName) { + keys.push(`${entity.device.ieeeAddr}/${entity.endpointName}`); + keys.push(`${entity.name}/${entity.endpointName}`); + } - properties.forEach((prop) => { - if (to.hasOwnProperty(prop) && (!from || from[prop] != to[prop])) { - payload[prop] = to[prop]; + if (entity.isDefaultEndpoint) { + keys.push(entity.name); + keys.push(entity.device.ieeeAddr); + } + + if (type === 'add') { + logger.info(`Adding '${entity.name}' to '${group.name}'`); + await entity.endpoint.addToGroup(group.group); + settings.addDeviceToGroup(group.settings.ID, keys); + this.mqtt.log({type: 'add', message: {device: entity.name, group: group.name}}); + } else if (type === 'remove') { + logger.info(`Removing '${entity.name}' from '${group.name}'`); + await entity.endpoint.removeFromGroup(group.group); + settings.removeDeviceFromGroup(group.settings.ID, keys); + this.mqtt.log({type: 'remove', message: {device: entity.name, group: group.name}}); + } else { // remove_all + logger.info(`Removing '${entity.name}' from all groups`); + await entity.endpoint.removeFromAllGroups(); + for (const settingsGroup of settings.getGroups()) { + settings.removeDeviceFromGroup(settingsGroup.ID, keys); + this.mqtt.log({type: 'remove_all', message: {device: entity.name}}); } - }); - - if (Object.keys(payload)) { - const groups = this.getGroupsOfDevice(ieeeAddr); - groups.forEach((groupID) => { - this.publishEntityState(groupID, payload); - }); } } - - onZigbeeStarted() { - this.state.registerOnStateChangeListener(this.onStateChange); - - const settingsGroups = {}; - Object.keys(settings.getGroups()).forEach((groupID) => { - settingsGroups[groupID] = settings.getGroup(groupID).devices; - }); - - this.apply(this.groupsCache, settingsGroups); - } - - parseTopic(topic) { - if (!topic.match(topicRegex) && !topic.match(topicRegexRemoveAll)) { - return null; - } - - // Remove base from topic - topic = topic.replace(`${settings.get().mqtt.base_topic}/bridge/group/`, ''); - - // Parse type from topic - const type = topic.substr(topic.lastIndexOf('/') + 1, topic.length); - - // Remove type from topic - topic = topic.replace(`/${type}`, ''); - - return {friendly_name: type === 'remove_all' ? null : topic, type}; - } - - updateDeviceGroup(ID, cmd, groupID) { - let payload = null; - if (cmd === 'add') { - payload = {groupid: groupID, groupname: ''}; - cmd = 'add'; - } else if (cmd === 'remove') { - payload = {groupid: groupID}; - cmd = 'remove'; - } else if (cmd === 'remove_all') { - payload = {}; - cmd = 'removeAll'; - } - - const {entityID, endpointID} = this.parseID(ID); - const ieeeAddr = settings.resolveEntity(entityID).ID; - - const cb = (error, rsp) => { - if (error) { - logger.error(`Failed to ${cmd} ${ieeeAddr} from ${groupID}`); - } else { - logger.info(`Successfully ${cmd} ${ieeeAddr} to ${groupID}`); - - // Log to MQTT - const lookup = { - 'add': 'device_group_add', - 'remove': 'device_group_remove', - 'removeAll': 'device_group_remove_all', - }; - - this.mqtt.log({ - type: lookup[cmd], - message: { - device: settings.getDevice(ieeeAddr).friendly_name, - group: groupID, - }, - }); - - // Update group cache - if (cmd === 'add') { - if (!this.groupsCache[groupID]) { - this.groupsCache[groupID] = []; - } - - if (!this.groupsCache[groupID].includes(ieeeAddr)) { - this.groupsCache[groupID].push(ieeeAddr); - } - } else if (cmd === 'remove') { - if (this.groupsCache[groupID]) { - this.groupsCache[groupID] = this.groupsCache[groupID].filter((device) => device != ieeeAddr); - } - } else if (cmd === 'removeAll') { - Object.keys(this.groupsCache).forEach((groupID_) => { - this.groupsCache[groupID_] = this.groupsCache[groupID_].filter((device) => device != ieeeAddr); - }); - } - - this.writeGroupsCache(); - - // Update settings - if (cmd === 'add') { - settings.addDeviceToGroup(groupID, ieeeAddr); - } else if (cmd === 'remove') { - settings.removeDeviceFromGroup(groupID, ieeeAddr); - } else if (cmd === 'removeAll') { - Object.keys(settings.get().groups).forEach((groupID_) => { - settings.removeDeviceFromGroup(groupID_, ieeeAddr); - }); - } - } - }; - - this.zigbee.publish( - ieeeAddr, 'device', 'genGroups', cmd, 'functional', - payload, null, endpointID, cb, - ); - } - - onMQTTMessage(topic, message) { - topic = this.parseTopic(topic); - - if (!topic) { - return false; - } - - // Find ID of this group. - let groupID = null; - if (topic.type !== 'remove_all') { - groupID = settings.getGroupIDByFriendlyName(topic.friendly_name); - if (!groupID) { - logger.error(`Group with friendly_name '${topic.friendly_name}' doesn't exist`); - return; - } - - groupID = groupID.toString(); - } - - // Send command to the device. - this.updateDeviceGroup(message.toString(), topic.type, groupID); - - return true; - } } module.exports = Groups; diff --git a/lib/extension/homeassistant.js b/lib/extension/homeassistant.js index 42449ddbf..bef29c627 100644 --- a/lib/extension/homeassistant.js +++ b/lib/extension/homeassistant.js @@ -1,4 +1,4 @@ -const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); const settings = require('../util/settings'); const logger = require('../util/logger'); const zigbee2mqttVersion = require('../../package.json').version; @@ -908,7 +908,6 @@ class HomeAssistant { this.mqtt = mqtt; this.state = state; this.publishEntityState = publishEntityState; - this.zigbee2mqttVersion = zigbee2mqttVersion; // A map of all discoverd devices this.discovered = {}; @@ -925,16 +924,16 @@ class HomeAssistant { this.statusTopic = settings.get().advanced.homeassistant_status_topic; } - onMQTTConnected() { + async onMQTTConnected() { this.mqtt.subscribe(this.statusTopic); // MQTT discovery of all paired devices on startup. - this.zigbee.getAllClients().forEach((device) => { - const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); + for (const device of await this.zigbee.getClients()) { + const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); if (mappedModel) { this.discover(device.ieeeAddr, mappedModel, true); } - }); + } } discover(entityID, mappedModel, force=false) { @@ -944,22 +943,23 @@ class HomeAssistant { return; } - const entity = settings.resolveEntity(entityID); - if (entity.type === 'device' && (!mapping[mappedModel.model] || !settings.getDevice(entity.ID))) { - return; - } else if (entity.type === 'group' && (!settings.getGroup(entity.ID))) { + const entity = settings.getEntity(entityID); + if (!entity || (entity.type === 'device' && !mapping[mappedModel.model])) { return; } mapping[mappedModel.model].forEach((config) => { - const topic = `${config.type}/${entityID}/${config.object_id}/config`; + const topic = `${config.type}/${entity.ID}/${config.object_id}/config`; const payload = {...config.discovery_payload}; const stateTopic = `${settings.get().mqtt.base_topic}/${entity.friendlyName}`; if (!payload.hasOwnProperty('state_topic') || payload.state_topic) { payload.state_topic = stateTopic; - } else if (payload.hasOwnProperty('state_topic')) { - delete payload.state_topic; + } else { + /* istanbul ignore else */ + if (payload.hasOwnProperty('state_topic')) { + delete payload.state_topic; + } } if (payload.position_topic) { @@ -980,13 +980,13 @@ class HomeAssistant { payload.name = `${entity.friendlyName}_${config.object_id}`; // Set unique_id - payload.unique_id = `${entityID}_${config.object_id}_${settings.get().mqtt.base_topic}`; + payload.unique_id = `${entity.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`; // Attributes for device registry payload.device = { - identifiers: [`zigbee2mqtt_${entityID}`], + identifiers: [`zigbee2mqtt_${entity.ID}`], name: entity.friendlyName, - sw_version: `Zigbee2mqtt ${this.zigbee2mqttVersion}`, + sw_version: `Zigbee2mqtt ${zigbee2mqttVersion}`, model: `${mappedModel.description} (${mappedModel.model})`, manufacturer: mappedModel.vendor, }; @@ -1001,9 +1001,8 @@ class HomeAssistant { } // Add precision to value_template - const device = settings.getDevice(entityID); - if (device.hasOwnProperty(`${config.object_id}_precision`)) { - const precision = device[`${config.object_id}_precision`]; + if (entity.hasOwnProperty(`${config.object_id}_precision`)) { + const precision = entity[`${config.object_id}_precision`]; let template = payload.value_template; template = template.replace('{{ ', '').replace(' }}', ''); template = `{{ (${template} | float) | round(${precision}) }}`; @@ -1065,12 +1064,12 @@ class HomeAssistant { } // Override configuration with user settings. - if (device.hasOwnProperty('homeassistant')) { + if (entity.hasOwnProperty('homeassistant')) { const add = (obj) => { Object.keys(obj).forEach((key) => { if (['number', 'string', 'boolean'].includes(typeof obj[key])) { payload[key] = obj[key]; - } else if (typeof obj[key] === 'object' && obj[key] === null) { + } else if (obj[key] === null) { delete payload[key]; } else if (key === 'device' && typeof obj[key] === 'object') { Object.keys(obj['device']).forEach((key) => { @@ -1080,43 +1079,42 @@ class HomeAssistant { }); }; - add(device.homeassistant); + add(entity.homeassistant); - if (device.homeassistant.hasOwnProperty(config.object_id)) { - add(device.homeassistant[config.object_id]); + if (entity.homeassistant.hasOwnProperty(config.object_id)) { + add(entity.homeassistant[config.object_id]); } } - this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, null, this.discoveryTopic); + this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 0}, this.discoveryTopic); }); this.discovered[entityID] = true; } onMQTTMessage(topic, message) { - if (!topic === this.statusTopic) { + if (topic !== this.statusTopic) { return false; } - if (message.toString().toLowerCase() === 'online') { - const timer = setTimeout(() => { + if (message.toLowerCase() === 'online') { + const timer = setTimeout(async () => { // Publish all device states. - this.zigbee.getAllClients().forEach((device) => { + for (const device of await this.zigbee.getClients()) { if (this.state.exists(device.ieeeAddr)) { this.publishEntityState(device.ieeeAddr, this.state.get(device.ieeeAddr)); } - }); + } clearTimeout(timer); }, 20000); } - - return true; } - onZigbeeMessage(message, device, mappedModel) { - if (device && mappedModel) { - this.discover(device.ieeeAddr, mappedModel); + onZigbeeEvent(type, data, mappedDevice, settingsDevice) { + const device = data.device; + if (device && mappedDevice) { + this.discover(device.ieeeAddr, mappedDevice); } } diff --git a/lib/extension/livolo.js b/lib/extension/livolo.js index deb36f11e..f927bb78a 100644 --- a/lib/extension/livolo.js +++ b/lib/extension/livolo.js @@ -1,3 +1,5 @@ +/* istanbul ignore file */ +// todo const utils = require('../util/utils'); const interval = utils.secondsToMilliseconds(1); const logger = require('../util/logger'); @@ -26,18 +28,17 @@ class Livolo { }; } - onZigbeeMessage(message, device, mappedDevice) { - if (!device) { + onZigbeeEvent(type, data, mappedDevice, settingsDevice) { + if (!data.device) { return; } - if ((message.type == 'devInterview') || - (message.type == 'devIncoming') || - (message.type == 'endDeviceAnnce') || - (message.type == 'devStatus')) { - if (this.configured.hasOwnProperty(device.ieeeAddr)) { - logger.info(`LIVOLO ${device.ieeeAddr}. (Re)joins in the network (after power off?)`); - this._resetDeviceState(device.ieeeAddr); + if ((type == 'deviceInterview') || + (type == 'deviceJoined') || + (type == 'deviceAnnounce')) { + if (this.configured.hasOwnProperty(data.device.ieeeAddr)) { + logger.info(`LIVOLO ${data.device.ieeeAddr}. (Re)joins in the network (after power off?)`); + this._resetDeviceState(data.device.ieeeAddr); } return; } @@ -134,9 +135,9 @@ class Livolo { }); } - handleInterval() { - this.zigbee.getAllClients() - .filter((d) => d.manufName && d.manufName.startsWith('LIVOLO')) // LIVOLO + async handleInterval() { + (await this.zigbee.getAllClients()) + .filter((d) => d.manufacturerName && d.manufacturerName.startsWith('LIVOLO')) // LIVOLO .filter((d) => d.type === 'EndDevice') // Filter end devices .filter((d) => d.powerSource && d.powerSource !== 'Battery') // Remove battery powered devices .forEach((d) => { diff --git a/lib/extension/networkMap.js b/lib/extension/networkMap.js index d3fdfd124..1645cd4ad 100644 --- a/lib/extension/networkMap.js +++ b/lib/extension/networkMap.js @@ -1,13 +1,14 @@ const settings = require('../util/settings'); const utils = require('../util/utils'); -const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); +const logger = require('../util/logger'); class NetworkMap { constructor(zigbee, mqtt, state, publishEntityState) { this.zigbee = zigbee; this.mqtt = mqtt; this.state = state; - this.lastSeenMap = {}; + this.lastSeen = new Map(); // Subscribe to topic. this.topic = `${settings.get().mqtt.base_topic}/bridge/networkmap`; @@ -24,9 +25,9 @@ class NetworkMap { }; } - onZigbeeMessage(message, device, mappedDevice) { - if (device) { - this.lastSeenMap[device.ieeeAddr] = Date.now(); + onZigbeeEvent(type, data, mappedDevice, settingsDevice) { + if (data.device) { + this.lastSeen.set(data.device.ieeeAddr, Date.now()); } } @@ -35,31 +36,24 @@ class NetworkMap { this.mqtt.subscribe(this.topicRoutes); } - onMQTTMessage(topic, message) { - message = message.toString(); - const includeRoutes = (topic === this.topicRoutes) ? 1: 0; - if ((topic === this.topic || topic === this.topicRoutes) && - this.supportedFormats.hasOwnProperty(message)) { - this.zigbee.networkScan(includeRoutes, (result)=> { - const converted = this.supportedFormats[message](this.zigbee, result); - this.mqtt.publish(`bridge/networkmap/${message}`, converted, {}); - }); - - return true; + async onMQTTMessage(topic, message) { + if ((topic === this.topic || topic === this.topicRoutes) && this.supportedFormats.hasOwnProperty(message)) { + const includeRoutes = topic === this.topicRoutes; + const topology = await this.networkScan(includeRoutes); + const converted = this.supportedFormats[message](topology); + this.mqtt.publish(`bridge/networkmap/${message}`, converted, {}); } - - return false; } - raw(zigbee, topology) { + raw(topology) { return JSON.stringify(topology); } - graphviz(zigbee, topology) { + graphviz(topology) { const colors = settings.get().map_options.graphviz.colors; let text = 'digraph G {\nnode[shape=record];\n'; - let devStyle = ''; + let style = ''; topology.nodes.forEach((device) => { const labels = []; @@ -67,79 +61,141 @@ class NetworkMap { // Add friendly name labels.push(`${device.friendlyName}`); - // Add the device short network address and scan note (if any) - let scanNote = ''; - if (device.scanfailed.includes('lqi')) { - scanNote += ' no lqi'; - } - if (device.scanfailed.includes('rtg')) { - scanNote += ' no routes'; - } - labels.push(`0x${device.nwkAddr.toString(16)} ${scanNote}`); + // Add the device short network address, ieeaddr and scan note (if any) + labels.push( + `${device.ieeeAddr} (${device.networkAddress})` + + ((device.failed && device.failed.length) ? `failed: ${device.failed.join(',')}` : '') + ); // Add the device model - const mappedModel = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); - if (mappedModel) { - labels.push(`${mappedModel.vendor} ${mappedModel.description} (${mappedModel.model})`); - } else { - // This model is not supported by zigbee-shepherd-converters, add zigbee model information, if available - const zigbeeModel = [device.manufName, device.modelId].filter((a) => a).join(' '); - labels.push(zigbeeModel ? zigbeeModel : 'No model information available'); - } - - // Add the device status (online/offline) and last_seen timestamp - let lastSeen = 'unknown'; - if (device.type == 'Coordinator') { - lastSeen = utils.formatDate(Date.now(), settings.get().advanced.last_seen, - utils.toLocalISOString(new Date(Date.now()))); - } else { - if (this.lastSeenMap[device.ieeeAddr]) { - const lastSeenAgo = - `${new Date(Date.now() - this.lastSeenMap[device.ieeeAddr]).toISOString().substr(11, 8)}s ago`; - lastSeen = utils.formatDate(this.lastSeenMap[device.ieeeAddr], - settings.get().advanced.last_seen, lastSeenAgo); + if (device.type !== 'Coordinator') { + const mappedModel = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); + if (mappedModel) { + labels.push(`${mappedModel.vendor} ${mappedModel.description} (${mappedModel.model})`); + } else { + // This model is not supported by zigbee-shepherd-converters, add zigbee model information + labels.push(`${device.manufacturerName} ${device.modelID}`); } } - labels.push(`${device.status} (${lastSeen})`); + + // Add the device last_seen timestamp + let lastSeen = 'unknown'; + const date = device.type === 'Coordinator' ? Date.now() : this.lastSeen.get(device.ieeeAddr); + if (date) { + const lastSeenAgo = `${new Date(Date.now() - date).toISOString().substr(11, 8)}s ago`; + lastSeen = utils.formatDate(date, settings.get().advanced.last_seen, lastSeenAgo); + } + + labels.push(`Last seen: ${lastSeen}`); // Shape the record according to device type if (device.type == 'Coordinator') { - devStyle = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` + + style = `style="bold, filled", fillcolor="${colors.fill.coordinator}", ` + `fontcolor="${colors.font.coordinator}"`; } else if (device.type == 'Router') { - devStyle = `style="rounded, filled", fillcolor="${colors.fill.router}", ` + + style = `style="rounded, filled", fillcolor="${colors.fill.router}", ` + `fontcolor="${colors.font.router}"`; } else { - devStyle = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", ` + + style = `style="rounded, dashed, filled", fillcolor="${colors.fill.enddevice}", ` + `fontcolor="${colors.font.enddevice}"`; } // Add the device with its labels to the graph as a node. - text += ` "${device.ieeeAddr}" [`+devStyle+`, label="{${labels.join('|')}}"];\n`; + text += ` "${device.ieeeAddr}" [`+style+`, label="{${labels.join('|')}}"];\n`; /** * Add an edge between the device and its child to the graph * NOTE: There are situations where a device is NOT in the topology, this can be e.g. * due to not responded to the lqi scan. In that case we do not add an edge for this device. */ - topology.links.filter((e) => (e.sourceIeeeAddr === device.ieeeAddr) || (e.SourceNwkAddr === device.nwkAddr)) - .forEach((e) => { - const lineStyle = (device.type=='EndDevice') ? 'penwidth=1, ' : (!e.routes.length) ? - 'penwidth=0.5, ' : 'penwidth=2, '; - const lineWeight = (!e.routes.length) ? `weight=0, color="${colors.line.inactive}", ` : - `weight=1, color="${colors.line.active}", `; - const textRoutes = e.routes.map((r) => `0x${r.toString(16)}`); - const lineLabels = (!e.routes.length) ? `label="${e.lqi}"` : - `label="${e.lqi}\\n[${textRoutes.join(']\\n[')}]"`; - text += ` "${device.ieeeAddr}" -> "${e.targetIeeeAddr}"`; - text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`; - }); + topology.links.filter((e) => (e.source.ieeeAddr === device.ieeeAddr)).forEach((e) => { + const lineStyle = (device.type=='EndDevice') ? 'penwidth=1, ' : + (!e.routes.length) ? 'penwidth=0.5, ' : 'penwidth=2, '; + const lineWeight = (!e.routes.length) ? `weight=0, color="${colors.line.inactive}", ` : + `weight=1, color="${colors.line.active}", `; + const textRoutes = e.routes.map((r) => r.destinationAddress); + const lineLabels = (!e.routes.length) ? `label="${e.linkquality}"` : + `label="${e.linkquality} (routes: ${textRoutes.join(',')})"`; + text += ` "${device.ieeeAddr}" -> "${e.target.ieeeAddr}"`; + text += ` [${lineStyle}${lineWeight}${lineLabels}]\n`; + }); }); text += '}'; return text.replace(/\0/g, ''); } + + async networkScan(includeRoutes) { + logger.info(`Starting network scan (includeRoutes '${includeRoutes}')`); + const devices = await this.zigbee.getDevices({}); + const lqis = new Map(); + const routingTables = new Map(); + const failed = new Map(); + + for (const device of devices.filter((d) => d.type != 'EndDevice')) { + failed.set(device, []); + const resolved = await this.zigbee.resolveEntity(device); + try { + const result = await device.lqi(); + lqis.set(device, result); + logger.debug(`LQI succeeded for '${resolved.name}'`); + } catch (error) { + failed.get(device).push('lqi'); + logger.error(`Failed to execute LQI for '${resolved.name}'`); + } + + if (includeRoutes) { + try { + const result = await device.routingTable(); + routingTables.set(device, result); + logger.debug(`Routing table succeeded for '${resolved.name}'`); + } catch (error) { + failed.get(device).push('routingTable'); + logger.error(`Failed to execute routing table for '${resolved.name}'`); + } + } + } + + logger.info(`Network scan finished`); + + const networkMap = {nodes: [], links: []}; + // Add nodes + for (const device of devices) { + const resolved = await this.zigbee.resolveEntity(device); + networkMap.nodes.push({ + ieeeAddr: device.ieeeAddr, friendlyName: resolved.name, type: device.type, + networkAddress: device.networkAddress, manufacturerName: device.manufacturerName, + modelID: device.modelID, failed: failed.get(device), + }); + } + + // Add links + lqis.forEach((lqi, device) => { + for (const neighbor of lqi.neighbors) { + if (neighbor.relationship > 3) { + // Relationship is not active, skip it + continue; + } + + const link = { + source: {ieeeAddr: neighbor.ieeeAddr, networkAddress: neighbor.networkAddress}, + target: {ieeeAddr: device.ieeeAddr, networkAddress: device.networkAddress}, + linkquality: neighbor.linkquality, depth: neighbor.depth, routes: [], + }; + + const routingTable = routingTables.get(device); + if (routingTable) { + link.routes = routingTable.table + .filter((t) => t.status === 'ACTIVE' && t.nextHop === neighbor.networkAddress); + } + + networkMap.links.push(link); + } + }); + + return networkMap; + } } module.exports = NetworkMap; diff --git a/lib/extension/responder.js b/lib/extension/responder.js deleted file mode 100644 index 7849fc25d..000000000 --- a/lib/extension/responder.js +++ /dev/null @@ -1,75 +0,0 @@ -const Zcl = require('zigbee-herdsman/dist/zcl'); - -const OneJanuary2000 = new Date('January 01, 2000 00:00:00').getTime(); - -/** - * This extensions is reponsible for respondig to device requests. - */ -class ExtensionResponder { - constructor(zigbee, mqtt, state, publishEntityState) { - this.zigbee = zigbee; - this.mqtt = mqtt; - this.state = state; - this.publishEntityState = publishEntityState; - - this.onZclFoundation = this.onZclFoundation.bind(this); - this.configured = []; - } - - setupOnZclFoundation(device) { - if (device && device.epList && !this.configured.includes(device.ieeeAddr)) { - this.configured.push(device.ieeeAddr); - - device.epList.forEach((epID) => { - const ep = this.zigbee.getEndpoint(device.ieeeAddr, epID); - ep.onZclFoundation = this.onZclFoundation; - }); - } - } - - onZigbeeStarted() { - this.zigbee.getAllClients().forEach((device) => this.setupOnZclFoundation(device)); - } - - onZigbeeMessage(message, device, mappedDevice) { - this.setupOnZclFoundation(device); - } - - readResponse(message, endpoint) { - const clusterID = message.clusterid; - const cluster = Zcl.getClusterLegacy(clusterID).key; - const attributes = message.zclMsg.payload.map((p) => Zcl.getAttributeLegacy(message.clusterid, p.attrId)); - const response = []; - - attributes.forEach((attribute) => { - if (cluster === 'genTime' && attribute.key === 'time') { - const time = Math.round(((new Date()).getTime() - OneJanuary2000) / 1000); - response.push(this.createReadResponseRec(clusterID, attribute.value, time)); - } - }); - - this.zigbee.publish( - endpoint.device.ieeeAddr, 'device', cluster, 'readRsp', 'foundation', response, - {direction: 1, seqNum: message.zclMsg.seqNum, disDefaultRsp: 1}, endpoint.epId, - ); - } - - createReadResponseRec(cId, attrId, value) { - return { - attrId: attrId, - status: 0, - attrData: value, - dataType: Zcl.getAttributeTypeLegacy(cId, attrId).value, - }; - } - - onZclFoundation(message, endpoint) { - const cmd = message.zclMsg.cmdId; - - if (cmd === 'read') { - this.readResponse(message, endpoint); - } - } -} - -module.exports = ExtensionResponder; diff --git a/lib/extension/softReset.js b/lib/extension/softReset.js index 6ed78af92..d2ef03caa 100644 --- a/lib/extension/softReset.js +++ b/lib/extension/softReset.js @@ -1,3 +1,5 @@ +/* istanbul ignore file */ +// deprecated const settings = require('../util/settings'); const logger = require('../util/logger'); const utils = require('../util/utils'); @@ -33,29 +35,29 @@ class SoftReset { this.timer = setTimeout(() => this.handleTimeout(), this.timeout); } - handleTimeout() { + async handleTimeout() { logger.warn('Soft reset timeout triggered'); - this.zigbee.softReset((error) => { - if (error) { - logger.warn('Soft reset failed, trying stop/start'); - this.zigbee.stop((error) => { - logger.warn('Zigbee stopped'); - this.zigbee.start((error) => { - if (error) { - logger.error('Failed to restart!'); - } - }); - }); - } else { - logger.warn('Soft resetted ZNP due to timeout'); - } + try { + await this.zigbee.softReset(); + logger.warn('Soft resetted ZNP due to timeout'); + } catch (error) { + logger.warn('Soft reset failed, trying stop/start'); - this.resetTimer(); - }); + await this.zigbee.stop(); + logger.warn('Zigbee stopped'); + + try { + await this.zigbee.start(); + } catch (error) { + logger.error('Failed to restart!'); + } + } + + this.resetTimer(); } - onZigbeeMessage(message, device, mappedDevice) { + onZigbeeEvent() { this.resetTimer(); } } diff --git a/lib/extension/xiaomi.js b/lib/extension/xiaomi.js deleted file mode 100644 index 539cb7201..000000000 --- a/lib/extension/xiaomi.js +++ /dev/null @@ -1,70 +0,0 @@ -const utils = require('../util/utils'); -const interval = utils.secondsToMilliseconds(60); -const logger = require('../util/logger'); -const settings = require('../util/settings'); - -/** - * This extensions handles Xiaomi devices. - * - Marks Xiaomi devices as online. - * - Polls Xiaomi routers to keep them awake. - */ -class Xiaomi { - constructor(zigbee, mqtt, state, publishEntityState) { - this.zigbee = zigbee; - this.timer = null; - } - - onZigbeeStarted() { - // Set all Xiaomi devices to be online, so shepherd won't try - // to query info from devices (which would fail because they go to sleep). - const devices = this.zigbee.getAllClients(); - devices.forEach((d) => { - if (utils.isXiaomiDevice(d)) { - const device = this.zigbee.shepherd.find(d.ieeeAddr, 1); - if (device) { - device.getDevice().update({ - status: 'online', - joinTime: Math.floor(Date.now() / 1000), - }); - } - } - }); - - /** - * Device availability already pings Xiaomi devices. - */ - if (!settings.get().advanced.availability_timeout) { - this.startTimer(); - } else { - logger.debug('Disabled pinging of Xiaomi devices in Xiaomi extension because availability is enabled'); - } - } - - startTimer() { - this.clearTimer(); - this.timer = setInterval(() => this.handleInterval(), interval); - } - - clearTimer() { - if (this.timer) { - clearTimeout(this.timer); - this.timer = null; - } - } - - stop() { - this.clearTimer(); - } - - ping(ieeeAddr) { - this.zigbee.ping(ieeeAddr, 'error', null, 'basic'); - } - - handleInterval() { - this.zigbee.getAllClients() - .filter((d) => utils.isXiaomiDevice(d) && utils.isRouter(d) && !utils.isBatteryPowered(d)) - .forEach((d) => this.ping(d.ieeeAddr)); - } -} - -module.exports = Xiaomi; diff --git a/lib/mqtt.js b/lib/mqtt.js index 910c7031c..5765b47e6 100644 --- a/lib/mqtt.js +++ b/lib/mqtt.js @@ -2,15 +2,15 @@ const mqtt = require('mqtt'); const logger = require('./util/logger'); const settings = require('./util/settings'); const fs = require('fs'); +const events = require('events'); -class MQTT { +class MQTT extends events.EventEmitter { constructor() { - this.onConnect = this.onConnect.bind(this); + super(); this.onMessage = this.onMessage.bind(this); - this.messageHandler = null; } - connect(messageHandler, callback) { + async connect() { const mqttSettings = settings.get().mqtt; logger.info(`Connecting to MQTT server at ${mqttSettings.server}`); @@ -24,28 +24,14 @@ class MQTT { if (mqttSettings.ca) { logger.debug(`MQTT SSL/TLS: Path to CA certificate = ${mqttSettings.ca}`); - const ca = fs.readFileSync(mqttSettings.ca); - - if (ca) { - options.ca = ca; - } else { - logger.error(`Error loading CA certificate for MQTT SSL/TLS configuration.`); - } + options.ca = fs.readFileSync(mqttSettings.ca); } if (mqttSettings.key && mqttSettings.cert) { logger.debug(`MQTT SSL/TLS: Path to client key = ${mqttSettings.key}`); logger.debug(`MQTT SSL/TLS: Path to client certificate = ${mqttSettings.cert}`); - - const key = fs.readFileSync(mqttSettings.key); - const cert = fs.readFileSync(mqttSettings.cert); - - if (key && cert) { - options.key = key; - options.cert = cert; - } else { - logger.error(`Error loading key and/or certificate for MQTT SSL/TLS client authentication.`); - } + options.key = fs.readFileSync(mqttSettings.key); + options.cert = fs.readFileSync(mqttSettings.cert); } if (mqttSettings.user && mqttSettings.password) { @@ -63,16 +49,6 @@ class MQTT { options.rejectUnauthorized = false; } - this.client = mqtt.connect(mqttSettings.server, options); - - // Register callbacks. - this.client.on('connect', () => { - this.onConnect(); - callback(); - }); - - this.client.on('message', this.onMessage); - // Set timer at interval to check if connected to MQTT server. const interval = 10 * 1000; // seconds * 1000. this.connectionTimer = setInterval(() => { @@ -81,22 +57,25 @@ class MQTT { } }, interval); - this.messageHandler = messageHandler; - } + return new Promise((resolve) => { + this.client = mqtt.connect(mqttSettings.server, options); - disconnect() { - clearTimeout(this.connectionTimer); - this.connectionTimer = null; + this.client.on('connect', () => { + logger.info('Connected to MQTT server'); + this.publish('bridge/state', 'online', {retain: true, qos: 0}); + resolve(); + }); - this.publish('bridge/state', 'offline', {retain: true, qos: 0}, () => { - logger.info('Disconnecting from MQTT server'); - this.client.end(); + this.client.on('message', this.onMessage); }); } - onConnect() { - logger.info('Connected to MQTT server'); - this.publish('bridge/state', 'online', {retain: true, qos: 0}); + async disconnect() { + clearTimeout(this.connectionTimer); + this.connectionTimer = null; + await this.publish('bridge/state', 'offline', {retain: true, qos: 0}); + logger.info('Disconnecting from MQTT server'); + this.client.end(); } subscribe(topic) { @@ -104,13 +83,12 @@ class MQTT { } onMessage(topic, message) { - if (this.messageHandler) { - this.messageHandler(topic, message); - } + this.emit('message', {topic, message: message.toString()}); } - publish(topic, payload, options, callback, base=settings.get().mqtt.base_topic) { + async publish(topic, payload, options, base=settings.get().mqtt.base_topic) { topic = `${base}/${topic}`; + options = {qos: 0, retain: false, ...options}; if (!this.client || this.client.reconnecting) { logger.error(`Not connected to MQTT server!`); @@ -119,7 +97,10 @@ class MQTT { } logger.info(`MQTT publish: topic '${topic}', payload '${payload}'`); - this.client.publish(topic, payload, options, callback); + + return new Promise((resolve) => { + this.client.publish(topic, payload, options, () => resolve()); + }); } log(type, message, meta=null) { diff --git a/lib/state.js b/lib/state.js index 9ad4fa4c9..5d8b0822c 100644 --- a/lib/state.js +++ b/lib/state.js @@ -18,7 +18,6 @@ class State { this.state = {}; this.file = data.joinPath('state.json'); this.timer = null; - this.stateChangeListeners = []; this.handleSettingsChanged = this.handleSettingsChanged.bind(this); } @@ -40,10 +39,6 @@ class State { this.checkLastSeen(); } - registerOnStateChangeListener(listener) { - this.stateChangeListeners.push(listener); - } - checkLastSeen() { if (settings.get().advanced.last_seen === 'disable') { Object.values(this.state).forEach((s) => { @@ -103,9 +98,6 @@ class State { } }); - const fromState = this.state[ieeeAddr]; - this.stateChangeListeners.forEach((listener) => listener(ieeeAddr, fromState, toState)); - this.state[ieeeAddr] = toState; } diff --git a/lib/util/data.js b/lib/util/data.js index b87fb95ed..7b5f9f94f 100644 --- a/lib/util/data.js +++ b/lib/util/data.js @@ -1,5 +1,4 @@ const path = require('path'); -const fs = require('fs'); let dataPath = null; @@ -14,18 +13,8 @@ function load() { load(); -function joinPathStorage(file) { - const storagePath = path.join(dataPath, '.storage'); - if (!fs.existsSync(storagePath)) { - fs.mkdirSync(storagePath); - } - - return path.join(storagePath, file); -} - module.exports = { joinPath: (file) => path.join(dataPath, file), - joinPathStorage: (file) => joinPathStorage(file), getPath: () => dataPath, // For test only. diff --git a/lib/util/fs.js b/lib/util/fs.js deleted file mode 100644 index d12e7da6e..000000000 --- a/lib/util/fs.js +++ /dev/null @@ -1,16 +0,0 @@ -const yaml = require('js-yaml'); -const fs = require('fs'); - -function readYaml(file) { - return yaml.safeLoad(fs.readFileSync(file, 'utf8')); -} - -function readYamlIfExists(file) { - return fs.existsSync(file) ? readYaml(file) : null; -} - -function writeYaml(file, content) { - fs.writeFileSync(file, yaml.safeDump(content)); -} - -module.exports = {readYaml, readYamlIfExists, writeYaml}; diff --git a/lib/util/logger.js b/lib/util/logger.js index cd59e0a33..b3c3c2b0b 100644 --- a/lib/util/logger.js +++ b/lib/util/logger.js @@ -34,6 +34,18 @@ const levels = { }, }; +/* istanbul ignore next */ +function timestampFormatter() { + return new Date().toLocaleString(); +} + +/* istanbul ignore next */ +function formatter(options) { + return winston.config.colorize(options.level, ' zigbee2mqtt:' + options.level.toLowerCase()) + ' ' + + options.timestamp() + ' ' + (options.message ? options.message : '') + + (options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta) : '' ); +} + // Create logger const logger = new winston.Logger({ levels: levels.levels, @@ -44,14 +56,11 @@ const logger = new winston.Logger({ level, maxFiles: 3, // Keep last 3 files maxsize: 10000000, // 10MB - timestamp: () => new Date().toLocaleString(), + timestamp: timestampFormatter, }), new winston.transports.Console({ - timestamp: () => new Date().toLocaleString(), - formatter: (options) => - winston.config.colorize(options.level, ' zigbee2mqtt:' + options.level.toLowerCase()) + ' ' + - options.timestamp() + ' ' + (options.message ? options.message : '') + - (options.meta && Object.keys(options.meta).length ? '\n\t'+ JSON.stringify(options.meta) : '' ), + timestamp: timestampFormatter, + formatter, }), ], }); diff --git a/lib/util/settings.js b/lib/util/settings.js index 308ff2493..222845a44 100644 --- a/lib/util/settings.js +++ b/lib/util/settings.js @@ -2,7 +2,7 @@ const data = require('./data'); const file = data.joinPath('configuration.yaml'); const objectAssignDeep = require(`object-assign-deep`); const path = require('path'); -const fs = require('./fs'); +const yaml = require('./yaml'); const onChangeHandlers = []; const defaults = { @@ -12,6 +12,10 @@ const defaults = { mqtt: { include_device_information: false, }, + serial: { + disable_led: false, + }, + devices: {}, groups: {}, device_options: {}, map_options: { @@ -34,10 +38,6 @@ const defaults = { }, }, }, - queue: { - delay: 250, - simultaneously: 5, - }, experimental: { livolo: false, // json or attribute @@ -45,7 +45,7 @@ const defaults = { }, advanced: { log_directory: path.join(data.getPath(), 'log', '%TIMESTAMP%'), - log_level: process.env.DEBUG ? 'debug' : 'info', + log_level: /* istanbul ignore next */ process.env.DEBUG ? 'debug' : 'info', soft_reset_timeout: 0, pan_id: 0x1a62, ext_pan_id: [0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD, 0xDD], @@ -105,149 +105,207 @@ const defaults = { }; let _settings; - -function getSettings() { - if (!_settings) { - _settings = read(); - } - - if (_settings.hasOwnProperty('advanced') && !_settings.advanced) { - delete _settings.advanced; - } - - return _settings; -} - -function get() { - const _settings = objectAssignDeep.noMutate(defaults, getSettings()); - - if (!_settings.whitelist) { - _settings.whitelist = []; - } - - return _settings; -} - -function writeRead() { - write(); - _settings = read(); - onChangeHandlers.forEach((handler) => handler()); -} +let _settingsWithDefaults; function write() { - const settings = getSettings(); + const settings = get(); const toWrite = objectAssignDeep.noMutate(settings); // Read settings to check if we have to split devices/groups into separate file. - const actual = fs.readYaml(file); + const actual = yaml.read(file); if (typeof actual.devices === 'string') { - fs.writeYaml(data.joinPath(actual.devices), settings.devices); + yaml.write(data.joinPath(actual.devices), settings.devices); toWrite.devices = actual.devices; } if (typeof actual.groups === 'string') { - fs.writeYaml(data.joinPath(actual.groups), settings.groups); + yaml.write(data.joinPath(actual.groups), settings.groups); toWrite.groups = actual.groups; } - fs.writeYaml(file, toWrite); + yaml.write(file, toWrite); + + _settings = read(); + _settingsWithDefaults = objectAssignDeep.noMutate(defaults, get()); + onChangeHandlers.forEach((handler) => handler()); } function read() { - const s = fs.readYaml(file); + const s = yaml.read(file); // Read devices/groups configuration from separate file. if (typeof s.devices === 'string') { const file = data.joinPath(s.devices); - s.devices = fs.readYamlIfExists(file); + s.devices = yaml.readIfExists(file) || {}; } if (typeof s.groups === 'string') { const file = data.joinPath(s.groups); - s.groups = fs.readYamlIfExists(file); + s.groups = yaml.readIfExists(file) || {}; } return s; } +function get() { + if (!_settings) { + _settings = read(); + } + + return _settings; +} + +function getWithDefaults() { + if (!_settingsWithDefaults) { + _settingsWithDefaults = objectAssignDeep.noMutate(defaults, get()); + } + + return _settingsWithDefaults; +} + function set(path, value) { - let obj = getSettings(); + let settings = get(); for (let i = 0; i < path.length; i++) { const key = path[i]; if (i === path.length - 1) { - obj[key] = value; + settings[key] = value; } else { - if (!obj[key]) { - obj[key] = {}; + if (!settings[key]) { + settings[key] = {}; } - obj = obj[key]; + settings = settings[key]; } } - writeRead(); + write(); } -const getDevices = () => getSettings().devices || []; +function getGroup(IDorName) { + const settings = getWithDefaults(); + const byID = settings.groups[IDorName]; + if (byID) { + return {devices: [], ...byID, ID: Number(IDorName), friendlyName: byID.friendly_name}; + } -const getDevice = (ieeeAddr) => getDevices()[ieeeAddr]; + for (const [ID, group] of Object.entries(settings.groups)) { + if (group.friendly_name === IDorName) { + return {devices: [], ...group, ID: Number(ID), friendlyName: group.friendly_name}; + } + } -const getGroups = () => getSettings().groups || {}; + return null; +} -const getGroup = (ID) => { - const group = getGroups()[ID]; +function getGroups() { + const settings = getWithDefaults(); + return Object.entries(settings.groups).map(([ID, group]) => { + return {devices: [], ...group, ID: Number(ID), friendlyName: group.friendly_name}; + }); +} - if (group && !group.hasOwnProperty('devices')) { - group.devices = []; +function getGroupThrowIfNotExists(IDorName) { + const group = getGroup(IDorName); + if (!group) { + throw new Error(`Group '${IDorName}' does not exist`); } return group; -}; +} +function getDevice(IDorName) { + const settings = getWithDefaults(); + const byID = settings.devices[IDorName]; + if (byID) { + return {...byID, ID: IDorName, friendlyName: byID.friendly_name}; + } + + for (const [ID, device] of Object.entries(settings.devices)) { + if (device.friendly_name === IDorName) { + return {...device, ID, friendlyName: device.friendly_name}; + } + } + + return null; +} + +function getDeviceThrowIfNotExists(IDorName) { + const device = getDevice(IDorName); + if (!device) { + throw new Error(`Device '${IDorName}' does not exist`); + } + + return device; +} + +function getEntity(IDorName) { + const device = getDevice(IDorName); + if (device) { + return {...device, type: 'device'}; + } + + const group = getGroup(IDorName); + if (group) { + return {...group, type: 'group'}; + } + + return null; +} + +function addDevice(ID) { + if (getDevice(ID)) { + throw new Error(`Device '${ID}' already exists`); + } + + const settings = get(); -function addDevice(ieeeAddr) { - const settings = getSettings(); if (!settings.devices) { settings.devices = {}; } - settings.devices[ieeeAddr] = {friendly_name: ieeeAddr, retain: false}; - writeRead(); + settings.devices[ID] = {friendly_name: ID, retain: false}; + write(); + return getDevice(ID); } -function whitelistDevice(ieeeAddr) { - const settings = getSettings(); +function whitelistDevice(ID) { + const settings = get(); if (!settings.whitelist) { settings.whitelist = []; } - settings.whitelist.push(ieeeAddr); + if (settings.whitelist.includes(ID)) { + throw new Error(`Device '${ID}' already whitelisted`); + } - writeRead(); + settings.whitelist.push(ID); + write(); } -function banDevice(ieeeAddr) { - const settings = getSettings(); +function banDevice(ID) { + const settings = get(); if (!settings.ban) { settings.ban = []; } - settings.ban.push(ieeeAddr); - - writeRead(); + settings.ban.push(ID); + write(); } -function removeDevice(ieeeAddr) { - const settings = getSettings(); - if (!settings.devices || !settings.devices[ieeeAddr]) return; - - delete settings.devices[ieeeAddr]; - writeRead(); +function removeDevice(IDorName) { + const device = getDeviceThrowIfNotExists(IDorName); + const settings = get(); + delete settings.devices[device.ID]; + write(); } -function addGroup(groupName) { - const settings = getSettings(); +function addGroup(name) { + if (getGroup(name)) { + throw new Error(`Group '${name}' already exists`); + } + + const settings = get(); if (!settings.groups) { settings.groups = {}; } @@ -257,83 +315,61 @@ function addGroup(groupName) { ID = (Number.parseInt(ID) + 1).toString(); } - if (Object.values(settings.groups).find((g) => g.friendly_name === groupName)) { - return false; // Duplicate group names are not allowed - } + settings.groups[ID] = {friendly_name: name}; + write(); - settings.groups[ID] = {friendly_name: groupName}; - writeRead(); - - return true; + return getGroup(ID); } -function addDeviceToGroup(ID, ieeeAddr) { - const settings = getSettings(); - const group = settings.groups[ID]; +function groupHasDevice(group, keys) { + for (const device of group.devices) { + const index = keys.indexOf(device); + if (index != -1) { + return keys[index]; + } + } + + return false; +} + +function addDeviceToGroup(groupIDorName, keys) { + const groupID = getGroupThrowIfNotExists(groupIDorName).ID; + const settings = get(); + + const group = settings.groups[groupID]; if (!group.devices) { group.devices = []; } - if (!group.devices.includes(ieeeAddr)) { - group.devices.push(ieeeAddr); - writeRead(); - return true; - } else { - return false; + if (!groupHasDevice(group, keys)) { + group.devices.push(keys[0]); + write(); } } -function removeDeviceFromGroup(ID, ieeeAddr) { - const settings = getSettings(); - const group = settings.groups[ID]; +function removeDeviceFromGroup(groupIDorName, keys) { + const groupID = getGroupThrowIfNotExists(groupIDorName).ID; + const settings = get(); + const group = settings.groups[groupID]; - if (group.devices && group.devices.includes(ieeeAddr)) { - group.devices = group.devices.filter((d) => d != ieeeAddr); - writeRead(); - return true; - } else { - return false; + const key = groupHasDevice(group, keys); + if (key) { + group.devices = group.devices.filter((d) => d != key); + write(); } } -function removeGroup(name) { - const settings = getSettings(); - if (!settings.groups) return; - - const ID = Object.keys(settings.groups).find((key) => { - return settings.groups[key].friendly_name === name; - }); - - if (ID) { - delete settings.groups[ID]; - writeRead(); - return true; - } else { - return false; - } +function removeGroup(groupIDorName) { + const groupID = getGroupThrowIfNotExists(groupIDorName).ID; + const settings = get(); + delete settings.groups[groupID]; + write(); } -function getIeeeAddrByFriendlyName(friendlyName) { - const entry = Object.entries(getDevices()).find(([ieeeAddr, device]) => - device.friendly_name === friendlyName - ); - return entry && entry[0]; -} - -function getGroupIDByFriendlyName(friendlyName) { - const entry = Object.entries(getGroups()).find(([ID, group]) => - group.friendly_name === friendlyName - ); - return entry && entry[0]; -} - -function changeDeviceOptions(ieeeAddr, newOptions) { - const settings = getSettings(); - const currentOptions = settings.devices[ieeeAddr]; - - if (!currentOptions) { - return; - } +function changeDeviceOptions(IDorName, newOptions) { + const device = getDeviceThrowIfNotExists(IDorName); + const settings = get(); + const currentOptions = settings.devices[device.ID]; Object.keys(currentOptions).forEach((key) => { if (newOptions[key]) { @@ -341,80 +377,44 @@ function changeDeviceOptions(ieeeAddr, newOptions) { } }); - writeRead(); + write(); } -function changeFriendlyName(old, new_) { - const settings = getSettings(); - const ieeeAddr = getIeeeAddrByFriendlyName(old); - - if (!ieeeAddr) { - return false; - } - - settings.devices[ieeeAddr].friendly_name = new_; - writeRead(); - return true; +function changeFriendlyName(IDorName, newName) { + const device = getDeviceThrowIfNotExists(IDorName); + const settings = get(); + settings.devices[device.ID].friendly_name = newName; + write(); } -// An entity can be either a group or a device. -function resolveEntity(ID) { - let type = null; - let friendlyName = null; - - if (module.exports.getIeeeAddrByFriendlyName(ID)) { - // Check if the ID is a friendly_name of a device. - friendlyName = ID; - ID = module.exports.getIeeeAddrByFriendlyName(ID); - type = 'device'; - } else if (module.exports.getGroupIDByFriendlyName(ID)) { - // Check if the ID is a friendly_name of a group. - friendlyName = ID; - ID = Number(module.exports.getGroupIDByFriendlyName(ID)); - type = 'group'; - } else if (module.exports.getGroup(ID)) { - friendlyName = module.exports.getGroup(ID).friendly_name; - ID = Number(ID); - type = 'group'; - } else { - // By default it is a device with ID as ID. - type = 'device'; - const device = module.exports.getDevice(ID); - friendlyName = device ? device.friendly_name : ID; - } - - return {ID, type, friendlyName}; +function addOnChangeHandler(handler) { + onChangeHandlers.push(handler); } + module.exports = { - get: () => get(), - write: () => write(), - set: (path, value) => set(path, value), - + get: getWithDefaults, + set, getDevice, getGroup, getGroups, - getDevices, - whitelistDevice: (ieeeAddr) => whitelistDevice(ieeeAddr), - banDevice: (ieeeAddr) => banDevice(ieeeAddr), - addDevice: (ieeeAddr) => addDevice(ieeeAddr), - removeDevice: (ieeeAddr) => removeDevice(ieeeAddr), - addGroup: (name) => addGroup(name), - removeGroup: (name) => removeGroup(name), - addDeviceToGroup: (ID, ieeeAddr) => addDeviceToGroup(ID, ieeeAddr), - removeDeviceFromGroup: (ID, ieeeAddr) => removeDeviceFromGroup(ID, ieeeAddr), + getEntity, + whitelistDevice, + banDevice, + addDevice, + removeDevice, + addGroup, + removeGroup, + addDeviceToGroup, + removeDeviceFromGroup, + changeDeviceOptions, + addOnChangeHandler, + changeFriendlyName, - getIeeeAddrByFriendlyName: (friendlyName) => getIeeeAddrByFriendlyName(friendlyName), - getGroupIDByFriendlyName: (friendlyName) => getGroupIDByFriendlyName(friendlyName), - changeFriendlyName: (old, new_) => changeFriendlyName(old, new_), - changeDeviceOptions: (ieeeAddr, options) => changeDeviceOptions(ieeeAddr, options), - resolveEntity, - - addOnChangeHandler: (handler) => onChangeHandlers.push(handler), - - // For test - _getDefaults: () => { - return objectAssignDeep.noMutate(defaults); + // For tests only + _reRead: () => { + _settings = read(); + _settingsWithDefaults = objectAssignDeep.noMutate(defaults, get()); }, - _clear: () => _settings = undefined, + _getDefaults: () => defaults, }; diff --git a/lib/util/utils.js b/lib/util/utils.js index 60eac315a..4900a7319 100644 --- a/lib/util/utils.js +++ b/lib/util/utils.js @@ -1,5 +1,4 @@ -const zigbeeShepherdConverters = require('zigbee-shepherd-converters'); -const logger = require('../util/logger'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); // Xiaomi uses 4151 and 4447 (lumi.plug) as manufacturer ID. const xiaomiManufacturerID = [4151, 4447]; @@ -28,8 +27,9 @@ function toLocalISOString(dDate) { } const postfixes = [ - 'left', 'right', 'center', 'bottom_left', 'bottom_right', 'l1', 'l2', + 'left', 'right', 'center', 'bottom_left', 'bottom_right', 'l1', 'l2', 'default', 'top_left', 'top_right', 'white', 'rgb', 'system', 'top', 'bottom', 'center_left', 'center_right', + 'ep1', 'ep2', ]; function flatten(arr) { @@ -39,67 +39,31 @@ function flatten(arr) { } const forceEndDevice = flatten( - ['QBKG03LM', 'QBKG04LM', 'ZNMS13LM'] - .map((model) => zigbeeShepherdConverters.devices.find((d) => d.model === model)) + ['QBKG03LM', 'QBKG04LM', 'ZNMS13LM', 'ZNMS12LM'] + .map((model) => zigbeeHerdsmanConverters.devices.find((d) => d.model === model)) .map((mappedModel) => mappedModel.zigbeeModel)); -function getEndpointByEntityID(zigbee, entityID, epName) { - const device = zigbee.getDevice(entityID); - if (!device) { - logger.error(`Failed to find device with entity ID '${entityID}'`); - return; - } +async function getZigbee2mqttVersion() { + return new Promise((resolve, reject) => { + const git = require('git-last-commit'); + const packageJSON = require('../../package.json'); + const version = packageJSON.version; - const mappedDevice = zigbeeShepherdConverters.findByZigbeeModel(device.modelId); - if (!mappedDevice) { - logger.error(`Device with model ID ${device.modelId} is not supported`); - return; - } + git.getLastCommit((err, commit) => { + let commitHash = null; - let epID = null; - if (epName) { - if (!mappedDevice.ep) { - logger.error(`Device ${mappedDevice.model} doesn't define eps`); - return; - } - - epID = mappedDevice.ep(device)[epName]; - if (!epID) { - logger.error(`Device ${mappedDevice.model} doesn't have ep named '${epName}'`); - return; - } - } else if (mappedDevice.hasOwnProperty('ep')) { - const eps = mappedDevice.ep(device); - epID = eps[''] || null; - } - - const endpoint = zigbee.getEndpoint(entityID, epID); - if (!endpoint) { - logger.error(`Failed to retrieve for entity ID ${entityID} and endpoint ID ${epID}`); - } - - return endpoint; -} - -function getZigbee2mqttVersion(callback) { - const git = require('git-last-commit'); - const packageJSON = require('../../package.json'); - const version = packageJSON.version; - - git.getLastCommit((err, commit) => { - let commitHash = null; - - if (err) { - try { - commitHash = require('../../.hash.json').hash; - } catch (error) { - commitHash = 'unknown'; + if (err) { + try { + commitHash = require('../../.hash.json').hash; + } catch (error) { + commitHash = 'unknown'; + } + } else { + commitHash = commit.shortHash; } - } else { - commitHash = commit.shortHash; - } - callback({commitHash, version}); + resolve({commitHash, version}); + }); }); } @@ -124,29 +88,28 @@ function formatDate(date, type, _default) { return result; } +function objectHasProperties(object, properties) { + for (const property of properties) { + if (!object.hasOwnProperty(property)) { + return false; + } + } + + return true; +} + module.exports = { millisecondsToSeconds: (milliseconds) => milliseconds / 1000, secondsToMilliseconds: (seconds) => seconds * 1000, - isXiaomiDevice: (device) => { - return device.modelId !== 'lumi.router' && xiaomiManufacturerID.includes(device.manufId) && - (!device.manufName || !device.manufName.startsWith('Trust')); - }, - isIkeaTradfriDevice: (device) => ikeaTradfriManufacturerID.includes(device.manufId), - isRouter: (device) => device.type === 'Router' && !forceEndDevice.includes(device.modelId), - isBatteryPowered: (device) => device.powerSource && device.powerSource === 'Battery', - isNumeric: (string) => /^\d+$/.test(string), - toLocalISOString: (dDate) => toLocalISOString(dDate), - getPostfixes: () => postfixes, - formatDate: (date, type, _default=null) => formatDate(date, type, _default), - correctDeviceType: (device) => { - if (device) { - if (forceEndDevice.includes(device.modelId)) { - return 'EndDevice'; - } - } - - return device.type; - }, - getEndpointByEntityID, getZigbee2mqttVersion, + objectHasProperties, + getPostfixes: () => postfixes, + isXiaomiDevice: (device) => { + return device.modelID !== 'lumi.router' && xiaomiManufacturerID.includes(device.manufacturerID) && + (!device.manufacturerName || !device.manufacturerName.startsWith('Trust')); + }, + isIkeaTradfriDevice: (device) => ikeaTradfriManufacturerID.includes(device.manufacturerID), + isRouter: (device) => device.type === 'Router' && !forceEndDevice.includes(device.modelID), + isBatteryPowered: (device) => device.powerSource && device.powerSource === 'Battery', + formatDate: (date, type, _default=null) => formatDate(date, type, _default), }; diff --git a/lib/util/yaml.js b/lib/util/yaml.js new file mode 100644 index 000000000..b493e6d92 --- /dev/null +++ b/lib/util/yaml.js @@ -0,0 +1,16 @@ +const yaml = require('js-yaml'); +const fs = require('fs'); + +function read(file) { + return yaml.safeLoad(fs.readFileSync(file, 'utf8')); +} + +function readIfExists(file) { + return fs.existsSync(file) ? read(file) : null; +} + +function write(file, content) { + fs.writeFileSync(file, yaml.safeDump(content)); +} + +module.exports = {read, readIfExists, write}; diff --git a/lib/util/zigbeeQueue.js b/lib/util/zigbeeQueue.js deleted file mode 100644 index a3dbe58b6..000000000 --- a/lib/util/zigbeeQueue.js +++ /dev/null @@ -1,97 +0,0 @@ -const settings = require('./settings'); - -const maxSimultaneouslyRunning = settings.get().queue.simultaneously; -const delay = settings.get().queue.delay; -const error17Retries = 3; -const logger = require('./logger'); - -class ZigbeeQueue { - constructor() { - this.queue = []; - this.active = []; - this.timer = null; - this.running = false; - this.ID = 0; - } - - start() { - this.running = true; - this.resetTimer(); - } - - push(entityID, func) { - this.ID++; - this.queue.push({entityID, func, attempts: 0, ID: this.ID}); - this.log(`Added new job with ID ${this.ID} for ${entityID}`); - } - - log(message) { - logger.trace(`zigbeeQueue: ${message}`); - } - - stopTimer() { - if (this.timer) { - clearInterval(this.timer); - this.timer = null; - } - } - - resetTimer() { - this.stopTimer(); - - this.timer = setInterval(() => { - this.executeNext(); - }, delay); - } - - stop() { - this.stopTimer(); - this.running = false; - } - - handleJobComplete(job, error) { - this.log(`Completed job with ID ${job.ID} for ${job.entityID}${error ? ` with error ${error}` : ''}`); - - if (error && error.message === 'rsp error: 17' && job.attempts < error17Retries) { - // Error 17 means that the buffer of the ZNP was full, - // retry this for a maximum of 3 times. - job.attempts++; - this.active.splice(this.active.indexOf(job), 1); - } else { - this.active.splice(this.active.indexOf(job), 1); - this.queue.splice(this.queue.indexOf(job), 1); - } - } - - executeNext() { - if (!this.running) { - return; - } - - const next = this.getNext(); - - if (next) { - this.active.push(next); - this.log(`Executing job with ID ${next.ID} for ${next.entityID}`); - next.func((error) => this.handleJobComplete(next, error)); - } - } - - getNext() { - if (this.active.length > (maxSimultaneouslyRunning - 1)) { - return null; - } - - for (let i = 0; i < this.queue.length; i++) { - const job = this.queue[i]; - const activeDeviceJob = this.active.find((j) => j.entityID === job.entityID); - if (!activeDeviceJob) { - return job; - } - } - - return null; - } -} - -module.exports = ZigbeeQueue; diff --git a/lib/zapp/cie.js b/lib/zapp/cie.js deleted file mode 100644 index 52f919ec3..000000000 --- a/lib/zapp/cie.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * This is a fake cie app which makes it possible for some iAS devices to join the network. - * Based on: https://github.com/zigbeer/zapp-cie - */ - -const Ziee = require('zigbee-herdsman/dist/deprecated/ziee'); -const Zive = require('zigbee-herdsman/dist/deprecated/zive'); -const cieClusters = new Ziee(); -// Direction-Value 1: input, 2: output, 3: input/output - -// IasZone Cluster-Init Direction -cieClusters.init('ssIasZone', 'dir', {value: 2}); // Client Side(Output) - -// Init Command Response Handler -cieClusters.init('ssIasZone', 'cmdRsps', { - enrollReq: (zapp, argObj, cb) => {}, - statusChangeNotification: (zapp, argObj, cb) => {}, -}); - -// Create Zive with profId: 0x0104 (Decimal: 260 / HA), devId: 0x0400 (Decimal: 1024 / iasControlIndicatingEquipment) -// Check the Values by https://github.com/zigbeer/zcl-id/wiki#5-table-of-identifiers -const cieApp = new Zive({profId: 0x0104, devId: 0x0400, discCmds: []}, cieClusters); -module.exports = cieApp; diff --git a/lib/zigbee.js b/lib/zigbee.js index 76af696cf..d1b307173 100644 --- a/lib/zigbee.js +++ b/lib/zigbee.js @@ -1,663 +1,228 @@ -const ZShepherd = require('zigbee-herdsman'); +const ZigbeeHerdsman = require('zigbee-herdsman'); const logger = require('./util/logger'); const settings = require('./util/settings'); const data = require('./util/data'); +const assert = require('assert'); const utils = require('./util/utils'); -const ZigbeeQueue = require('./util/zigbeeQueue'); -const cieApp = require('./zapp/cie'); +const events = require('events'); const objectAssignDeep = require('object-assign-deep'); -const Zcl = require('zigbee-herdsman/dist/zcl'); +const zigbeeHerdsmanConverters = require('zigbee-herdsman-converters'); -const advancedSettings = settings.get().advanced; +const postfixes = utils.getPostfixes(); +const keyEndpointByNumber = new RegExp(`.*/([0-9].*)$`); -if (advancedSettings.channel < 11 || advancedSettings.channel > 26) { - throw new Error(`'${advancedSettings.channel}' is an invalid channel, use a channel between 11 - 26.`); -} - -const shepherdSettings = { - net: { - panId: advancedSettings.pan_id, - extPanId: advancedSettings.ext_pan_id, - channelList: [advancedSettings.channel], - precfgkey: settings.get().advanced.network_key, +const herdsmanSettings = { + network: { + panID: settings.get().advanced.pan_id, + extenedPanID: settings.get().advanced.ext_pan_id, + channelList: [settings.get().advanced.channel], + networkKey: settings.get().advanced.network_key, }, - dbPath: data.joinPath('database.db'), - coordBackupPath: data.joinPath('coordinator_backup.json'), - sp: { - baudRate: advancedSettings.baudrate, - rtscts: advancedSettings.rtscts, + databasePath: data.joinPath('database.db'), + backupPath: data.joinPath('coordinator_backup.json'), + serialPort: { + baudRate: settings.get().advanced.baudrate, + rtscts: settings.get().advanced.rtscts, + path: settings.get().serial.port, }, }; -const defaultCfg = { - manufSpec: 0, - disDefaultRsp: 0, -}; - -// Don't print network key. -const shepherdSettingsLog = objectAssignDeep.noMutate(shepherdSettings); -shepherdSettingsLog.net.precfgkey = 'HIDDEN'; - -class Zigbee { +class Zigbee extends events.EventEmitter { constructor() { - this.onReady = this.onReady.bind(this); - this.onMessage = this.onMessage.bind(this); - this.onError = this.onError.bind(this); - this.messageHandler = null; - this.permitJoinTimer = null; - - this.queue = new ZigbeeQueue(); + super(); } - start(messageHandler, callback) { - logger.info(`Starting zigbee-shepherd`); - logger.debug(`Using zigbee-shepherd with settings: '${JSON.stringify(shepherdSettingsLog)}'`); + async start() { + logger.info(`Starting zigbee-herdsman...`); + const herdsmanSettingsLog = objectAssignDeep.noMutate(herdsmanSettings); + herdsmanSettingsLog.network.networkKey = 'HIDDEN'; + logger.debug(`Using zigbee-herdsman with settings: '${JSON.stringify(herdsmanSettingsLog)}'`); - this.messageHandler = messageHandler; - this.shepherd = new ZShepherd(settings.get().serial.port, shepherdSettings); + try { + this.herdsman = new ZigbeeHerdsman.Controller(herdsmanSettings); + await this.herdsman.start(); + } catch (error) { + logger.error(`Error while starting zigbee-herdsman`); + logger.error(error.stack); + throw error; + } - this.shepherd.start((error) => { - if (error) { - logger.info(`Error while starting zigbee-shepherd, attempting to fix... (takes 60 seconds) (${error})`); - this.shepherd.controller._znp.close((() => null)); + this.herdsman.on('adapterDisconnected', () => this.emit('adapterDisconnected')); + this.herdsman.on('deviceAnnounce', (data) => this.emit('event', 'deviceAnnounce', data)); + this.herdsman.on('deviceInterview', (data) => this.emit('event', 'deviceInterview', data)); + this.herdsman.on('deviceJoined', (data) => this.emit('event', 'deviceJoined', data)); + this.herdsman.on('deviceLeave', (data) => this.emit('event', 'deviceLeave', data)); + this.herdsman.on('message', (data) => this.emit('event', 'message', data)); - setTimeout(() => { - logger.info(`Starting zigbee-shepherd`); - this.shepherd.start((error) => { - if (error) { - logger.error(`Error while starting zigbee-shepherd! (${error})`); - logger.error( - 'Press the reset button on the stick (the one closest to the USB) and start again' - ); - callback(error); - } else { - this._handleStarted(); - callback(null); - } - }); - }, utils.secondsToMilliseconds(60)); - } else { - this._handleStarted(); - callback(null); - } - }); + logger.info('zigbee-herdsman started'); + logger.info(`Coordinator firmware version: '${JSON.stringify(await this.getCoordinatorVersion())}'`); + logger.debug(`Zigbee network parameters: ${JSON.stringify(await this.herdsman.getNetworkParameters())}`); - // Register callbacks. - this.shepherd.on('ready', this.onReady); - this.shepherd.on('ind', this.onMessage); - this.shepherd.on('error', this.onError); - this._acceptDevIncoming = this._acceptDevIncoming.bind(this); - this.shepherd.acceptDevIncoming = this._acceptDevIncoming; - } - - _handleStarted() { - this.logStartupInfo(); - - this.getAllClients().forEach((device) => { - // If set whitelist devices, all other device will be ban or reject to join the network - if (settings.get().whitelist.size>0) { + for (const device of await this.getClients()) { + // If a whitelist is used, all other device will be removed from the network. + if (settings.get().whitelist.length > 0) { if (!settings.get().whitelist.includes(device.ieeeAddr)) { - logger.warn(`Blacklist device is connected (${device.ieeeAddr}), removing...`); - this.removeDevice(device.ieeeAddr, true, () => {}); + logger.warn(`Blacklisted device is connected (${device.ieeeAddr}), removing...`); + device.removeFromNetwork(); } - } else { - if (settings.get().ban.includes(device.ieeeAddr)) { - logger.warn(`Banned device is connected (${device.ieeeAddr}), removing...`); - this.removeDevice(device.ieeeAddr, false, () => {}); - } - } - }); - - this.shepherd.backupCoordinator(() => {}); - } - - _acceptDevIncoming(devInfo, callback) { - logger.debug( - `Accept device incoming with ieeeAddr '${devInfo.ieeeAddr}' permit join is '${this.getPermitJoin()}'` - ); - // If set whitelist devices, all other device will be ban or reject to join the network - if (settings.get().whitelist.size>0) { - if (settings.get().whitelist.includes(devInfo.ieeeAddr)) { - logger.info(`whitelist device tried to connect (${devInfo.ieeeAddr})`); - callback(null, true); - } else { - logger.debug(`Not allowing device '${devInfo.ieeeAddr}' to join`); - callback(null, false); - } - } else { - if (settings.get().ban.includes(devInfo.ieeeAddr)) { - logger.info(`Banned device tried to connect (${devInfo.ieeeAddr})`); - callback(null, false); - } else { - logger.debug(`Allowing device '${devInfo.ieeeAddr}' to join`); - callback(null, true); + } else if (settings.get().ban.includes(device.ieeeAddr)) { + logger.warn(`Banned device is connected (${device.ieeeAddr}), removing...`); + device.removeFromNetwork(); } } - } - - logStartupInfo() { - logger.info('zigbee-shepherd started'); - logger.info(`Coordinator firmware version: '${this.getFirmwareVersion()}'`); - logger.debug(`zigbee-shepherd info: ${JSON.stringify(this.shepherd.info())}`); - } - - getFirmwareVersion() { - return this.shepherd.info().firmware.revision; - } - - softReset(callback) { - this.shepherd.reset('soft', callback); - } - - stop(callback) { - if (this.permitJoinTimer) { - clearInterval(this.permitJoinTimer); - } - - this.queue.stop(); - - // Backup coordinator - this.shepherd.backupCoordinator(() => { - this.shepherd.stop((error) => { - logger.info('zigbee-shepherd stopped'); - callback(error); - }); - }); - } - - onReady() { - // Mount cieApp - this.shepherd.mount(cieApp, (err, epId) => { - if (!err) { - logger.debug(`Mounted the cieApp (epId ${epId})`); - } else { - logger.error(`Failed to mount the cieApp`); - } - }); // Check if we have to turn off the led if (settings.get().serial.disable_led) { - this.shepherd.controller.request('UTIL', 'ledControl', {ledid: 3, mode: 0}); + this.herdsman.disableLED(); } - - // Wait some time before we start the queue, many calls skip this queue which hangs the stick - setTimeout(() => { - this.queue.start(); - }, 2000); - - logger.info('zigbee-shepherd ready'); } - onError(message) { - // This event may appear if zigbee-shepherd cannot decode bad packets (invalid checksum). - logger.error(message); + async getCoordinatorVersion() { + return this.herdsman.getCoordinatorVersion(); } - permitJoin(permit, callback) { - if (permit) { - logger.info('Zigbee: allowing new devices to join.'); - } else { + async softReset() { + await this.herdsman.softReset(); + } + + async stop() { + await this.herdsman.stop(); + logger.info('zigbee-herdsman stopped'); + } + + async permitJoin(permit) { + permit ? + logger.info('Zigbee: allowing new devices to join.') : logger.info('Zigbee: disabling joining new devices.'); - } - // In zigbee 3.0 a network automatically closes after 254 seconds. - // As a workaround, we enable joining again. - if (this.permitJoinTimer) { - clearInterval(this.permitJoinTimer); - } + await this.herdsman.permitJoin(permit); + } - if (permit) { - this.permitJoinTimer = setInterval(() => { - this.shepherd.permitJoin(255, (error) => { - if (error) { - logger.error('Failed to reenable joining'); + async getPermitJoin() { + return this.herdsman.getPermitJoin(); + } + + async getClients() { + const devices = await this.herdsman.getDevices({}); + return devices.filter((device) => device.type !== 'Coordinator'); + } + + async getDevices(query) { + return this.herdsman.getDevices(query); + } + + async getDevice(query) { + return this.herdsman.getDevice(query); + } + + async resolveEntity(key) { + assert(typeof key === 'string' || key.constructor.name === 'Device', `Wrong type '${typeof key}'`); + + if (typeof key === 'string') { + if (key === 'coordinator') { + const coordinator = await this.getDevice({type: 'Coordinator'}); + return { + type: 'device', + device: coordinator, + endpoint: coordinator.getEndpoint(1), + settings: {friendlyName: 'Coordinator'}, + name: 'Coordinator', + }; + } + + let postfix = postfixes.find((p) => key.endsWith(`/${p}`)); + const postfixByNumber = key.match(keyEndpointByNumber); + if (!postfix && postfixByNumber) { + postfix = Number(postfixByNumber[1]); + } + if (postfix) { + key = key.replace(`/${postfix}`, ''); + } + + const entity = settings.getEntity(key); + if (!entity) { + return null; + } else if (entity.type === 'device') { + const device = await this.getDevice({ieeeAddr: entity.ID}); + const mapped = zigbeeHerdsmanConverters.findByZigbeeModel(device.modelID); + const endpoints = mapped && mapped.endpoint ? mapped.endpoint(device) : null; + let isDefaultEndpoint = true; + let endpoint; + if (postfix) { + isDefaultEndpoint = false; + if (postfixByNumber) { + endpoint = device.getEndpoint(postfix); } else { - logger.info('Successfully reenabled joining'); + assert(mapped != null, `Postfix '${postfix}' is given but device is unsupported`); + assert(endpoints != null, `Postfix '${postfix}' is given but device defines no endpoints`); + const endpointID = endpoints[postfix]; + assert(endpointID, `Postfix '${postfix}' is given but device has no such endpoint`); + endpoint = device.getEndpoint(endpointID); } - }); - }, utils.secondsToMilliseconds(160)); - } + } else if (endpoints && endpoints['default']) { + endpoint = device.getEndpoint(endpoints['default']); + } else { + endpoint = device.endpoints[0]; + } - this.shepherd.permitJoin(permit ? 255 : 0, (error) => { - if (error) { - logger.info(error); - } - - if (callback) { - callback(); - } - }); - } - - getPermitJoin() { - return this.shepherd.controller._permitJoinTime === 255; - } - - getAllClients() { - return this.getDevices().filter((device) => device.type !== 'Coordinator'); - } - - removeDevice(deviceID, ban, callback) { - if (ban) { - settings.banDevice(deviceID); - } - - const friendlyName = this.getDeviceFriendlyName(deviceID); - - this.shepherd.remove(deviceID, {reJoin: true}, (error) => { - if (error) { - logger.warn(`Failed to remove '${friendlyName}', trying force remove...`); - this.forceRemove(deviceID, callback); + const endpointName = endpoints ? Object.entries(endpoints).find((e) => e[1] === endpoint.ID)[0] : null; + return { + type: 'device', device, settings: entity, mapped, endpoint, name: entity.friendlyName, + isDefaultEndpoint, endpointName, + }; } else { - logger.info(`Removed ${friendlyName}`); - callback(null); + let group = await this.getGroup({groupID: entity.ID}); + if (!group) group = await this.createGroup(entity.ID); + return {type: 'group', group, settings: entity, name: entity.friendlyName}; } - }); - } - - forceRemove(deviceID, callback) { - const device = this.shepherd._findDevByAddr(deviceID); - - if (device) { - const friendlyName = this.getDeviceFriendlyName(deviceID); - return this.shepherd._unregisterDev(device, (error) => { - logger.info(`Force removed ${friendlyName}`); - callback(error); - }); } else { - logger.warn(`Could not find ${deviceID} for force removal`); - callback(true); - } - } - - onMessage(message) { - if (this.messageHandler) { - this.messageHandler(message); - } - } - - getDevices() { - return this.shepherd.list(); - } - - getDevice(ieeeAddr) { - return this.getDevices().find((d) => d.ieeeAddr === ieeeAddr); - } - - getDeviceFriendlyName(ieeeAddr) { - const device = settings.getDevice(ieeeAddr); - return device ? device.friendly_name || ieeeAddr : ieeeAddr; - } - - getCoordinator() { - const device = this.getDevices().find((d) => d.type === 'Coordinator'); - return this.shepherd.find(device.ieeeAddr, 1); - } - - getScanable() { - return this.getDevices().filter((d) => d.type != 'EndDevice'); - } - - getGroup(ID) { - return this.shepherd.getGroup(ID); - } - - getGroupFriendlyName(ID) { - let friendlyName = null; - friendlyName = settings.getGroup(ID).friendly_name; - return (friendlyName ? friendlyName : ID); - } - - networkScan(includeRoutes, callback) { - logger.info(`Starting network scan includeRoutes='${includeRoutes}'...`); - const lqiScanList = new Set(); - const rtgScanList = new Set(); - const linkMap = []; - const routeMap = []; - - // Gather the lqi and the route info into separate lists and only collate them when done. - const collateMap = () => { - linkMap.sort(); - logger.debug(`Link map: %j`, linkMap); - logger.debug(`Route map: %j`, routeMap); - // Merge the routes into the linkMap by matching on 'source|target' link short addresses - linkMap.forEach((link) => { - routeMap.filter((e) => e.key === link.key).forEach((e) => { - link.routes.push(e.destAddr); - }); - link.routes.sort(function(a, b) { - return a-b; - }); - delete link.key; - }); - const networkMap = {nodes: [], links: linkMap}; - this.getDevices().forEach((device) => { - const friendlyDevice = settings.getDevice(device.ieeeAddr); - const friendlyName = friendlyDevice ? friendlyDevice.friendly_name : device.ieeeAddr; - const deviceType = utils.correctDeviceType(device); - const scanfailed = []; - if (lqiScanList.has(device.ieeeAddr)) { - scanfailed.push('lqi'); - } - if (rtgScanList.has(device.ieeeAddr)) { - scanfailed.push('rtg'); - } - networkMap.nodes.push({ieeeAddr: device.ieeeAddr, friendlyName: friendlyName, type: deviceType, - nwkAddr: device.nwkAddr, manufName: device.manufName, modelId: device.modelId, - status: device.status, scanfailed: scanfailed}); - }); - // Clear remaining devices so they don't process when/if they eventually complete - lqiScanList.clear(); - rtgScanList.clear(); - logger.debug(`Merged map: %j`, networkMap); - callback(networkMap); - }; - - const processLqiResponse = (error, rsp, targetIeeeAddr, targetNwkAddr) => { - if (error) { - logger.warn(`Failed network lqi scan for device: '${targetIeeeAddr}' with error: '${error}'`); - } else { - if (lqiScanList.has(targetIeeeAddr)) { - // Haven't processed this one yet - if (rsp && rsp.status === 0 && rsp.neighborlqilist) { - logger.debug(`lqi scan: '${targetIeeeAddr}' with '${rsp.neighborlqilistcount}' neighbors`); - rsp.neighborlqilist.forEach(function(neighbor) { - // only include active relationships - if (neighbor.relationship <= 3) { - // lqi is measured at receiver so link is from neighbor (source) to scanned router - const key = neighbor.nwkAddr + '|' + targetNwkAddr; - linkMap.push({ - key: key, sourceIeeeAddr: neighbor.extAddr, targetIeeeAddr: targetIeeeAddr, - sourceNwkAddr: neighbor.nwkAddr, lqi: neighbor.lqi, depth: neighbor.depth, - relationship: neighbor.relationship, routes: []}); - } - }); - // Remove from scan list and if both lists are done return the completed network map - lqiScanList.delete(targetIeeeAddr); - if (lqiScanList.size === 0 && rtgScanList.size === 0) { - logger.info('Network scan completed'); - collateMap(); - } else { - logger.debug(`Outstanding network lqi scans for devices: '${[...lqiScanList].join(' ')}'`); - logger.debug(`Outstanding network rtg scans for devices: '${[...rtgScanList].join(' ')}'`); - } - } else { - logger.warn(`Empty network lqi scan result for: '${targetIeeeAddr}'`); - } - } else { - // This target has already had timeout so don't add to result network map - logger.warn(`Ignoring late network lqi scan result for: '${targetIeeeAddr}'`); - } - } - }; - - const processRtgResponse = (error, rsp, sourceIeeeAddr, sourceNwkAddr) => { - if (error) { - logger.warn(`Failed network rtg scan for device: '${sourceIeeeAddr}' with error: '${error}'`); - } else { - if (rtgScanList.has(sourceIeeeAddr)) { - // Haven't processed this one yet - if (rsp && rsp.status === 0 && rsp.routingtablelist) { - logger.debug(`rtg scan: '${sourceIeeeAddr}' with '${rsp.routingtablelistcount}' entries`); - rsp.routingtablelist.forEach(function(route) { - if (route.routeStatus === 0) { - const key = sourceNwkAddr + '|' + route.nextHopNwkAddr; - routeMap.push({ - key: key, destAddr: route.destNwkAddr}); - } - }); - // Remove from scan list and if both lists are done return the completed network map - rtgScanList.delete(sourceIeeeAddr); - if (lqiScanList.size === 0 && rtgScanList.size === 0) { - logger.info('Network scan completed'); - collateMap(); - } else { - logger.debug(`Outstanding network lqi scans for devices: '${[...lqiScanList].join(' ')}'`); - logger.debug(`Outstanding network rtg scans for devices: '${[...rtgScanList].join(' ')}'`); - } - } else { - logger.warn(`Empty network rtg scan result for: '${sourceIeeeAddr}'`); - } - } else { - // This source has already had timeout so don't add to result network map - logger.warn(`Ignoring late network rtg scan result for: '${sourceIeeeAddr}'`); - } - } - }; - - // Queue up an lqi scan and an rtg scan for coordinator and each router - this.getScanable().forEach((dev) => { - logger.debug(`Queing network scans for device: '${dev.ieeeAddr}'`); - lqiScanList.add(dev.ieeeAddr); - this.queue.push(dev.ieeeAddr, (queueCallback) => { - this.shepherd.controller.request('ZDO', 'mgmtLqiReq', {dstaddr: dev.nwkAddr, startindex: 0}, - (error, rsp, ieeeAddr, nwkAddr) => { - processLqiResponse(error, rsp, dev.ieeeAddr, dev.nwkAddr); - queueCallback(error); - }); - }); - if (includeRoutes) { - rtgScanList.add(dev.ieeeAddr); - this.queue.push(dev.ieeeAddr, (queueCallback) => { - this.shepherd.controller.request('ZDO', 'mgmtRtgReq', {dstaddr: dev.nwkAddr, startindex: 0}, - (error, rsp, ieeeAddr, nwkAddr) => { - processRtgResponse(error, rsp, dev.ieeeAddr, dev.nwkAddr); - queueCallback(error); - }); - }); - } - }); - - // Wait for all device scans before forcing map with whatever results are already in - setTimeout(() => { - if (lqiScanList.size === 0 && rtgScanList.size === 0) { - logger.info('Network scan timeout no outstanding requests'); - } else { - logger.warn(`Network scan timeout, skipping outstanding lqi scans for '${[...lqiScanList].join(' ')}'`); - logger.warn(`Network scan timeout, skipping outstanding rtg scans for '${[...rtgScanList].join(' ')}'`); - collateMap(); - } - }, lqiScanList.size * 1000); - } - - getEndpoint(ieeeAddr, ep) { - // If no ep is given, the first endpoint will be returned - // Find device in zigbee-shepherd - const device = this.getDevice(ieeeAddr); - if (!device || !device.epList || !device.epList.length) { - logger.error(`Zigbee cannot determine endpoint for '${ieeeAddr}'`); - return null; - } - - ep = ep ? ep : device.epList[0]; - const endpoint = this.shepherd.find(ieeeAddr, ep); - return endpoint; - } - - publish(entityID, entityType, cid, cmd, cmdType, zclData, cfg=defaultCfg, ep, callback) { - let entity = null; - let friendlyName = null; - if (entityType === 'device') { - entity = this.getEndpoint(entityID, ep); - friendlyName = this.getDeviceFriendlyName(entityID); - } else if (entityType === 'group') { - entity = this.getGroup(entityID); - friendlyName = this.getGroupFriendlyName(entityID); - } - - if (!entity) { - logger.error( - `Cannot publish message to ${entityType} because '${entityID}' is not known by zigbee-shepherd` - ); - return; - } - - this.queue.push(entityID, (queueCallback) => { - logger.info( - `Zigbee publish to ${entityType} '${friendlyName}', ${cid} - ${cmd} - ` + - `${JSON.stringify(zclData)} - ${JSON.stringify(cfg)} - ${entity.epId}` - ); - - const callback_ = (error, rsp) => { - if (error) { - logger.error( - `Zigbee publish to ${entityType} '${friendlyName}', ${cid} ` + - `- ${cmd} - ${JSON.stringify(zclData)} ` + - `- ${JSON.stringify(cfg)} - ${ep} ` + - `failed with error ${error}`); - } - - if (callback) { - callback(error, rsp); - } - - queueCallback(error); - }; - - if (cmdType === 'functional' && entity.functional) { - entity.functional(cid, cmd, zclData, cfg, callback_); - } else if (cmdType === 'foundation' && entity.foundation) { - entity.foundation(cid, cmd, zclData, cfg, callback_); - } else { - logger.error(`Unknown zigbee publish cmdType ${cmdType}`); - } - }); - } - - ping(ieeeAddr, errorLogLevel='error', cb, mechanism='default') { - const friendlyName = this.getDeviceFriendlyName(ieeeAddr); - const callback = (error) => { - if (error) { - logger[errorLogLevel](`Failed to ping '${friendlyName}'`); - } else { - logger.debug(`Successfully pinged '${friendlyName}'`); - } - - if (cb) { - cb(error); - } - }; - - if (mechanism === 'default') { - const device = this.shepherd._findDevByAddr(ieeeAddr); - if (device) { - logger.debug(`Ping ${ieeeAddr} (default)`); - this.queue.push(ieeeAddr, (queueCallback) => { - this.shepherd.controller.checkOnline(device, (error) => { - callback(error); - queueCallback(error); - }); - }); - } - } else if (mechanism === 'basic') { - const endpoint = this.getEndpoint(ieeeAddr, null); - if (endpoint) { - logger.debug(`Ping ${ieeeAddr} (basic)`); - this.queue.push(ieeeAddr, (queueCallback) => { - endpoint.foundation('genBasic', 'read', [{attrId: 0}], (error) => { - callback(error); - queueCallback(error); - }); - }); - } - } - } - - bind(ep, cluster, target, callback) { - const friendlyName = this.getDeviceFriendlyName(ep.device.ieeeAddr); - const log = ` '${friendlyName}' - ${cluster}`; - target = !target ? this.getCoordinator() : target; - - this.queue.push(ep.device.ieeeAddr, (queueCallback) => { - logger.debug(`Binding ${log}`); - ep.bind(cluster, target, (error) => { - if (error) { - logger.error(`Failed to bind ${log} - (${error})`); - } else { - logger.debug(`Successfully bound ${log}`); - } - - callback(error); - queueCallback(error); - }); - }); - } - - unbind(ep, cluster, target, callback) { - const friendlyName = this.getDeviceFriendlyName(ep.device.ieeeAddr); - const log = ` '${friendlyName}' - ${cluster}`; - target = !target ? this.getCoordinator() : target; - - this.queue.push(ep.device.ieeeAddr, (queueCallback) => { - logger.debug(`Unbinding ${log}`); - ep.unbind(cluster, target, (error) => { - if (error) { - logger.error(`Failed to unbind ${log} - (${error})`); - } else { - logger.debug(`Successfully unbound ${log}`); - } - - callback(error); - queueCallback(error); - }); - }); - } - - /* - * Setup reporting. - * Attributes is an array of attribute objects. - * each attribute object should contain the following properties: - * attr the attribute name, - * min the minimal time between reports in seconds, - * max the maximum time between reports in seconds, - * change the minimum amount of change before sending a report - */ - report(ep, cluster, attributes) { - const friendlyName = this.getDeviceFriendlyName(ep.device.ieeeAddr); - const cfgArr = attributes.map((attribute) => { - const attrId = Zcl.getAttributeLegacy(cluster, attribute.attr).value; - const dataType = Zcl.getAttributeTypeLegacy(cluster, attribute.attr).value; + const setting = settings.getEntity(key.ieeeAddr); return { - direction: 0, - attrId, - dataType, - minRepIntval: attribute.min, - maxRepIntval: attribute.max, - repChange: attribute.change, + type: 'device', + device: key, + settings: setting, + mapped: zigbeeHerdsmanConverters.findByZigbeeModel(key.modelID), + name: setting ? setting.friendlyName : (key.type === 'Coordinator' ? 'Coordinator' : key.ieeeAddr), }; - }); - - const log=`for '${friendlyName}' - ${cluster} - ${attributes.length}`; - - const configReport = () => { - this.queue.push(ep.device.ieeeAddr, (queueCallback) => { - ep.foundation(cluster, 'configReport', cfgArr, defaultCfg, (error) => { - if (error) { - logger.error(`Failed to setup reporting ${log} - (${error})`); - } else { - logger.debug(`Successfully setup reporting ${log}`); - } - - queueCallback(error); - }); - }); - }; - - this.queue.push(ep.device.ieeeAddr, (queueCallback) => { - logger.debug(`Setup reporting ${log}`); - - ep.bind(cluster, this.getCoordinator(), (error) => { - if (error) { - logger.error(`Failed to bind for reporting ${log} - (${error})`); - } else { - // Only if binding succeeds, setting-up reporting makes sense. - configReport(); - } - - queueCallback(error); - }); - }); + } } + + async getGroup(query) { + return this.herdsman.getGroup(query); + } + + async getGroups(query) { + return this.herdsman.getGroups(query); + } + + async createGroup(groupID) { + return await this.herdsman.createGroup(groupID); + } + + // TODO + // _acceptDevIncoming(devInfo, callback) { + // logger.debug( + // `Accept device incoming with ieeeAddr '${devInfo.ieeeAddr}' permit join is '${this.getPermitJoin()}'` + // ); + // // If set whitelist devices, all other device will be ban or reject to join the network + // if (settings.get().whitelist.size>0) { + // if (settings.get().whitelist.includes(devInfo.ieeeAddr)) { + // logger.info(`whitelist device tried to connect (${devInfo.ieeeAddr})`); + // callback(null, true); + // } else { + // logger.debug(`Not allowing device '${devInfo.ieeeAddr}' to join`); + // callback(null, false); + // } + // } else { + // if (settings.get().ban.includes(devInfo.ieeeAddr)) { + // logger.info(`Banned device tried to connect (${devInfo.ieeeAddr})`); + // callback(null, false); + // } else { + // logger.debug(`Allowing device '${devInfo.ieeeAddr}' to join`); + // callback(null, true); + // } + // } + // } } module.exports = Zigbee; diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9af63935b..b083974ed 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -35,15 +35,6 @@ "source-map": "^0.5.0" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -177,17 +168,6 @@ "debug": "^4.1.0", "globals": "^11.1.0", "lodash": "^4.17.13" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } } }, "@babel/types": { @@ -456,21 +436,6 @@ "@types/istanbul-lib-report": "*" } }, - "@types/jest": { - "version": "24.0.18", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.18.tgz", - "integrity": "sha512-jcDDXdjTcrQzdN06+TSVsPPqxvsZA/5QkYfIZlq1JMw7FdP5AZylbOc+6B/cuDurctRe+MziUMtQ3xQdrbjqyQ==", - "dev": true, - "requires": { - "@types/jest-diff": "*" - } - }, - "@types/jest-diff": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jest-diff/-/jest-diff-20.0.1.tgz", - "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", - "dev": true - }, "@types/json-schema": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", @@ -677,11 +642,6 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" - }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -985,19 +945,6 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, - "chai": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", - "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "pathval": "^1.1.0", - "type-detect": "^4.0.5" - } - }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -1015,11 +962,6 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", "dev": true }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" - }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -1280,9 +1222,10 @@ "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==" }, "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, "requires": { "ms": "^2.1.1" } @@ -1299,19 +1242,6 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, - "deep-diff": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-1.0.2.tgz", - "integrity": "sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg==" - }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "requires": { - "type-detect": "^4.0.0" - } - }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1614,15 +1544,6 @@ "v8-compile-cache": "^2.0.3" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "glob-parent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", @@ -1864,6 +1785,17 @@ "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + } } }, "extglob": { @@ -2650,11 +2582,6 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" - }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -3251,15 +3178,6 @@ "source-map": "^0.6.1" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, "rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -4109,7 +4027,8 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true }, "mute-stream": { "version": "0.0.7", @@ -4507,11 +4426,6 @@ "pify": "^3.0.0" } }, - "pathval": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", - "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" - }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -4602,9 +4516,9 @@ } }, "psl": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.1.tgz", - "integrity": "sha512-2KLd5fKOdAfShtY2d/8XDWVRnmp3zp40Qt6ge2zBPFARLXOGUf2fHD5eg+TV/5oxBtQKVhjUaKFsAaE4HnwfSA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.4.0.tgz", + "integrity": "sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==", "dev": true }, "pump": { @@ -4649,14 +4563,6 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, - "queue": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.1.tgz", - "integrity": "sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg==", - "requires": { - "inherits": "~2.0.3" - } - }, "react-is": { "version": "16.9.0", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz", @@ -5519,12 +5425,23 @@ } }, "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", "dev": true, "requires": { - "os-tmpdir": "~1.0.2" + "rimraf": "^2.6.3" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "tmpl": { @@ -5650,11 +5567,6 @@ "prelude-ls": "~1.1.2" } }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" - }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -6026,19 +5938,14 @@ } }, "zigbee-herdsman": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.5.7.tgz", - "integrity": "sha512-5Ak/kPb5CC+smRU3n1v5g/GEGEMKHQoBrZKmaA1EFfTYx0hMC47055U/Ajatu1Urdd4sXFzSyxwHBMX0eaytGg==", + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.6.4.tgz", + "integrity": "sha512-Z+4fo8NOZTlWeV4dJc3c5AU/PjBJg52MGlE2rocbIo0tTZczODUliArrogAMvijn985qyInoY8fhpOZ+OmQtuQ==", "requires": { - "busyman": "^0.3.0", - "concentrate": "^0.2.3", "debug": "^4.1.1", - "dissolve": "0.3.3", - "enum": "^2.3.0", - "objectbox": "^0.3.0", - "proving": "^0.1.0", - "q": "^1.5.1", - "queue": "^6.0.1", + "fast-deep-equal": "^2.0.1", + "mixin-deep": "^2.0.1", + "nedb": "^1.8.0", "serialport": "7.1.5" }, "dependencies": { @@ -6100,15 +6007,6 @@ "@babel/types": "^7.0.0" } }, - "@babel/helper-builder-react-jsx": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz", - "integrity": "sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw==", - "requires": { - "@babel/types": "^7.3.0", - "esutils": "^2.0.0" - } - }, "@babel/helper-call-delegate": { "version": "7.4.4", "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", @@ -6313,6 +6211,15 @@ "@babel/plugin-syntax-async-generators": "^7.2.0" } }, + "@babel/plugin-proposal-class-properties": { + "version": "7.5.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz", + "integrity": "sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==", + "requires": { + "@babel/helper-create-class-features-plugin": "^7.5.5", + "@babel/helper-plugin-utils": "^7.0.0" + } + }, "@babel/plugin-proposal-dynamic-import": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", @@ -6383,14 +6290,6 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, - "@babel/plugin-syntax-jsx": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", - "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, "@babel/plugin-syntax-object-rest-spread": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", @@ -6624,42 +6523,6 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, - "@babel/plugin-transform-react-display-name": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz", - "integrity": "sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz", - "integrity": "sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==", - "requires": { - "@babel/helper-builder-react-jsx": "^7.3.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz", - "integrity": "sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz", - "integrity": "sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, "@babel/plugin-transform-regenerator": { "version": "7.4.5", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", @@ -6795,18 +6658,6 @@ "semver": "^5.5.0" } }, - "@babel/preset-react": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.0.0.tgz", - "integrity": "sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-react-jsx-self": "^7.0.0", - "@babel/plugin-transform-react-jsx-source": "^7.0.0" - } - }, "@babel/preset-typescript": { "version": "7.3.3", "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.3.3.tgz", @@ -6869,49 +6720,55 @@ } }, "@jest/console": { - "version": "24.7.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.7.1.tgz", - "integrity": "sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.9.0.tgz", + "integrity": "sha512-Zuj6b8TnKXi3q4ymac8EQfc3ea/uhLeCGThFqXeC8H9/raaH8ARPUTdId+XyGd03Z4In0/VjD2OYFcBF09fNLQ==", "requires": { - "@jest/source-map": "^24.3.0", + "@jest/source-map": "^24.9.0", "chalk": "^2.0.1", "slash": "^2.0.0" } }, "@jest/core": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.8.0.tgz", - "integrity": "sha512-R9rhAJwCBQzaRnrRgAdVfnglUuATXdwTRsYqs6NMdVcAl5euG8LtWDe+fVkN27YfKVBW61IojVsXKaOmSnqd/A==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.9.0.tgz", + "integrity": "sha512-Fogg3s4wlAr1VX7q+rhV9RVnUv5tD7VuWfYy1+whMiWUrvl7U3QJSJyWcDio9Lq2prqYsZaeTv2Rz24pWGkJ2A==", "requires": { "@jest/console": "^24.7.1", - "@jest/reporters": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", + "@jest/reporters": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", "ansi-escapes": "^3.0.0", "chalk": "^2.0.1", "exit": "^0.1.2", "graceful-fs": "^4.1.15", - "jest-changed-files": "^24.8.0", - "jest-config": "^24.8.0", - "jest-haste-map": "^24.8.0", - "jest-message-util": "^24.8.0", + "jest-changed-files": "^24.9.0", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", "jest-regex-util": "^24.3.0", - "jest-resolve-dependencies": "^24.8.0", - "jest-runner": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-snapshot": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", - "jest-watcher": "^24.8.0", + "jest-resolve": "^24.9.0", + "jest-resolve-dependencies": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", + "jest-watcher": "^24.9.0", "micromatch": "^3.1.10", "p-each-series": "^1.0.0", - "pirates": "^4.0.1", "realpath-native": "^1.1.0", "rimraf": "^2.5.4", + "slash": "^2.0.0", "strip-ansi": "^5.0.0" }, "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + }, "ansi-regex": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", @@ -6928,35 +6785,35 @@ } }, "@jest/environment": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.8.0.tgz", - "integrity": "sha512-vlGt2HLg7qM+vtBrSkjDxk9K0YtRBi7HfRFaDxoRtyi+DyVChzhF20duvpdAnKVBV6W5tym8jm0U9EfXbDk1tw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.9.0.tgz", + "integrity": "sha512-5A1QluTPhvdIPFYnO3sZC3smkNeXPVELz7ikPbhUj0bQjB07EoE9qtLrem14ZUYWdVayYbsjVwIiL4WBIMV4aQ==", "requires": { - "@jest/fake-timers": "^24.8.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "jest-mock": "^24.8.0" + "@jest/fake-timers": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0" } }, "@jest/fake-timers": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.8.0.tgz", - "integrity": "sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.9.0.tgz", + "integrity": "sha512-eWQcNa2YSwzXWIMC5KufBh3oWRIijrQFROsIqt6v/NS9Io/gknw1jsAC9c+ih/RQX4A3O7SeWAhQeN0goKhT9A==", "requires": { - "@jest/types": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-mock": "^24.8.0" + "@jest/types": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0" } }, "@jest/reporters": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.8.0.tgz", - "integrity": "sha512-eZ9TyUYpyIIXfYCrw0UHUWUvE35vx5I92HGMgS93Pv7du+GHIzl+/vh8Qj9MCWFK/4TqyttVBPakWMOfZRIfxw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.9.0.tgz", + "integrity": "sha512-mu4X0yjaHrffOsWmVLzitKmmmWSQ3GGuefgNscUSWNiUNcEOSEQk9k3pERKEQVBb0Cnn88+UESIsZEMH3o88Gw==", "requires": { - "@jest/environment": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", "chalk": "^2.0.1", "exit": "^0.1.2", "glob": "^7.1.2", @@ -6964,13 +6821,13 @@ "istanbul-lib-instrument": "^3.0.1", "istanbul-lib-report": "^2.0.4", "istanbul-lib-source-maps": "^3.0.1", - "istanbul-reports": "^2.1.1", - "jest-haste-map": "^24.8.0", - "jest-resolve": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-util": "^24.8.0", + "istanbul-reports": "^2.2.6", + "jest-haste-map": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", "jest-worker": "^24.6.0", - "node-notifier": "^5.2.1", + "node-notifier": "^5.4.2", "slash": "^2.0.0", "source-map": "^0.6.0", "string-length": "^2.0.0" @@ -6984,9 +6841,9 @@ } }, "@jest/source-map": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.3.0.tgz", - "integrity": "sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.9.0.tgz", + "integrity": "sha512-/Xw7xGlsZb4MJzNDgB7PW5crou5JqWiBQaz6xyPd3ArOg2nfn/PunV8+olXbbEZzNl591o5rWKE9BRDaFAuIBg==", "requires": { "callsites": "^3.0.0", "graceful-fs": "^4.1.15", @@ -7001,42 +6858,43 @@ } }, "@jest/test-result": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.8.0.tgz", - "integrity": "sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.9.0.tgz", + "integrity": "sha512-XEFrHbBonBJ8dGp2JmF8kP/nQI/ImPpygKHwQ/SY+es59Z3L5PI4Qb9TQQMAEeYsThG1xF0k6tmG0tIKATNiiA==", "requires": { - "@jest/console": "^24.7.1", - "@jest/types": "^24.8.0", + "@jest/console": "^24.9.0", + "@jest/types": "^24.9.0", "@types/istanbul-lib-coverage": "^2.0.0" } }, "@jest/test-sequencer": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.8.0.tgz", - "integrity": "sha512-OzL/2yHyPdCHXEzhoBuq37CE99nkme15eHkAzXRVqthreWZamEMA0WoetwstsQBCXABhczpK03JNbc4L01vvLg==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.9.0.tgz", + "integrity": "sha512-6qqsU4o0kW1dvA95qfNog8v8gkRN9ph6Lz7r96IvZpHdNipP2cBcb07J1Z45mz/VIS01OHJ3pY8T5fUY38tg4A==", "requires": { - "@jest/test-result": "^24.8.0", - "jest-haste-map": "^24.8.0", - "jest-runner": "^24.8.0", - "jest-runtime": "^24.8.0" + "@jest/test-result": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-runner": "^24.9.0", + "jest-runtime": "^24.9.0" } }, "@jest/transform": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.8.0.tgz", - "integrity": "sha512-xBMfFUP7TortCs0O+Xtez2W7Zu1PLH9bvJgtraN1CDST6LBM/eTOZ9SfwS/lvV8yOfcDpFmwf9bq5cYbXvqsvA==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.9.0.tgz", + "integrity": "sha512-TcQUmyNRxV94S0QpMOnZl0++6RMiqpbH/ZMccFB/amku6Uwvyb1cjYX7xkp5nGNkbX4QPH/FcB6q1HBTHynLmQ==", "requires": { "@babel/core": "^7.1.0", - "@jest/types": "^24.8.0", + "@jest/types": "^24.9.0", "babel-plugin-istanbul": "^5.1.0", "chalk": "^2.0.1", "convert-source-map": "^1.4.0", "fast-json-stable-stringify": "^2.0.0", "graceful-fs": "^4.1.15", - "jest-haste-map": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-util": "^24.8.0", + "jest-haste-map": "^24.9.0", + "jest-regex-util": "^24.9.0", + "jest-util": "^24.9.0", "micromatch": "^3.1.10", + "pirates": "^4.0.1", "realpath-native": "^1.1.0", "slash": "^2.0.0", "source-map": "^0.6.1", @@ -7051,13 +6909,13 @@ } }, "@jest/types": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.8.0.tgz", - "integrity": "sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.9.0.tgz", + "integrity": "sha512-XKK7ze1apu5JWQ5eZjHITP66AX+QsLlbaJRBGYr8pNzwcAE2JVkwnf0yqjHTsDRcjR0mujy/NmZMXw5kl+kGBw==", "requires": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^12.0.9" + "@types/yargs": "^13.0.0" } }, "@serialport/binding-abstract": { @@ -7169,6 +7027,11 @@ "@babel/types": "^7.3.0" } }, + "@types/debug": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.5.tgz", + "integrity": "sha512-Q1y515GcOdTHgagaVFhHnIFQ38ygs/kmxdNpvpou+raI9UO3YZcHDngBSYKQklcKlvA7iuQlmIKbzvmxcOE9CQ==" + }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -7197,9 +7060,9 @@ } }, "@types/jest": { - "version": "24.0.16", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.16.tgz", - "integrity": "sha512-JrAiyV+PPGKZzw6uxbI761cHZ0G7QMOHXPhtSpcl08rZH6CswXaaejckn3goFKmF7M3nzEoJ0lwYCbqLMmjziQ==", + "version": "24.0.18", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.18.tgz", + "integrity": "sha512-jcDDXdjTcrQzdN06+TSVsPPqxvsZA/5QkYfIZlq1JMw7FdP5AZylbOc+6B/cuDurctRe+MziUMtQ3xQdrbjqyQ==", "requires": { "@types/jest-diff": "*" } @@ -7214,10 +7077,18 @@ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==" }, + "@types/nedb": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/nedb/-/nedb-1.8.8.tgz", + "integrity": "sha512-krZu5LWqkLVlKPlmK/LozSMCZsKrMHIDNnjCctrG61/fnuSWFV2X190ry1xR7++ZUcJSNWOKZMH+Qs75xSNLFw==", + "requires": { + "@types/node": "*" + } + }, "@types/node": { - "version": "12.6.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.9.tgz", - "integrity": "sha512-+YB9FtyxXGyD54p8rXwWaN1EWEyar5L58GlGWgtH2I9rGmLGBQcw63+0jw+ujqVavNuO47S1ByAjm9zdHMnskw==" + "version": "12.7.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-12.7.2.tgz", + "integrity": "sha512-dyYO+f6ihZEtNPDcWNR1fkoTDf3zAK3lAABDze3mz6POyIercH0lEUawUFXlG8xaQZmm1yEBON/4TsYv/laDYg==" }, "@types/serialport": { "version": "7.0.4", @@ -7233,56 +7104,64 @@ "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" }, "@types/yargs": { - "version": "12.0.12", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz", - "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==" + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.2.tgz", + "integrity": "sha512-lwwgizwk/bIIU+3ELORkyuOgDjCh7zuWDFqRtPPhhVgq9N1F7CvLNKg1TX4f2duwtKQ0p044Au9r1PLIXHrIzQ==", + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-13.0.0.tgz", + "integrity": "sha512-wBlsw+8n21e6eTd4yVv8YD/E3xq0O6nNnJIquutAsFGE7EyMKz7W6RNT6BRu1SmdgmlCZ9tb0X+j+D6HGr8pZw==" }, "@typescript-eslint/eslint-plugin": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz", - "integrity": "sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.0.0.tgz", + "integrity": "sha512-Mo45nxTTELODdl7CgpZKJISvLb+Fu64OOO2ZFc2x8sYSnUpFrBUW3H+H/ZGYmEkfnL6VkdtOSxgdt+Av79j0sA==", "requires": { - "@typescript-eslint/experimental-utils": "1.13.0", - "eslint-utils": "^1.3.1", + "@typescript-eslint/experimental-utils": "2.0.0", + "eslint-utils": "^1.4.0", "functional-red-black-tree": "^1.0.1", "regexpp": "^2.0.1", - "tsutils": "^3.7.0" + "tsutils": "^3.14.0" } }, "@typescript-eslint/experimental-utils": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", - "integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.0.0.tgz", + "integrity": "sha512-XGJG6GNBXIEx/mN4eTRypN/EUmsd0VhVGQ1AG+WTgdvjHl0G8vHhVBHrd/5oI6RRYBRnedNymSYWW1HAdivtmg==", "requires": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/typescript-estree": "1.13.0", + "@typescript-eslint/typescript-estree": "2.0.0", "eslint-scope": "^4.0.0" } }, "@typescript-eslint/parser": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.13.0.tgz", - "integrity": "sha512-ITMBs52PCPgLb2nGPoeT4iU3HdQZHcPaZVw+7CsFagRJHUhyeTgorEwHXhFf3e7Evzi8oujKNpHc8TONth8AdQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.0.0.tgz", + "integrity": "sha512-ibyMBMr0383ZKserIsp67+WnNVoM402HKkxqXGlxEZsXtnGGurbnY90pBO3e0nBUM7chEEOcxUhgw9aPq7fEBA==", "requires": { "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "1.13.0", - "@typescript-eslint/typescript-estree": "1.13.0", + "@typescript-eslint/experimental-utils": "2.0.0", + "@typescript-eslint/typescript-estree": "2.0.0", "eslint-visitor-keys": "^1.0.0" } }, "@typescript-eslint/typescript-estree": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz", - "integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.0.0.tgz", + "integrity": "sha512-NXbmzA3vWrSgavymlzMWNecgNOuiMMp62MO3kI7awZRLRcsA1QrYWo6q08m++uuAGVbXH/prZi2y1AWuhSu63w==", "requires": { "lodash.unescape": "4.0.1", - "semver": "5.5.0" + "semver": "^6.2.0" }, "dependencies": { "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -7292,9 +7171,9 @@ "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==" }, "acorn": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", - "integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==" + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.0.0.tgz", + "integrity": "sha512-PaF/MduxijYYt7unVGRuds1vBC9bFxbNf+VWqhOClfdgy7RlVkQqt610ig1/yxTgsDIfW1cWDel5EBbOy3jdtQ==" }, "acorn-globals": { "version": "4.3.3", @@ -7303,12 +7182,19 @@ "requires": { "acorn": "^6.0.1", "acorn-walk": "^6.0.1" + }, + "dependencies": { + "acorn": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz", + "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==" + } } }, "acorn-jsx": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", - "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==" + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.2.tgz", + "integrity": "sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw==" }, "acorn-walk": { "version": "6.2.0", @@ -7327,9 +7213,12 @@ } }, "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", + "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", + "requires": { + "type-fest": "^0.5.2" + } }, "ansi-regex": { "version": "2.1.1", @@ -7365,35 +7254,6 @@ "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "argparse": { @@ -7483,15 +7343,15 @@ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" }, "babel-jest": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.8.0.tgz", - "integrity": "sha512-+5/kaZt4I9efoXzPlZASyK/lN9qdRKmmUav9smVc0ruPQD7IsfucQ87gpOE8mn2jbDuS6M/YOW6n3v9ZoIfgnw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", + "integrity": "sha512-ntuddfyiN+EhMw58PTNL1ph4C9rECiQXjI4nMMBKBaNjXvqLdkXpPRcMSr4iyBrJg/+wz9brFUD6RhOAT6r4Iw==", "requires": { - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", "@types/babel__core": "^7.1.0", "babel-plugin-istanbul": "^5.1.0", - "babel-preset-jest": "^24.6.0", + "babel-preset-jest": "^24.9.0", "chalk": "^2.4.2", "slash": "^2.0.0" } @@ -7516,20 +7376,20 @@ } }, "babel-plugin-jest-hoist": { - "version": "24.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.6.0.tgz", - "integrity": "sha512-3pKNH6hMt9SbOv0F3WVmy5CWQ4uogS3k0GY5XLyQHJ9EGpAT9XWkFd2ZiXXtkwFHdAHa5j7w7kfxSP5lAIwu7w==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.9.0.tgz", + "integrity": "sha512-2EMA2P8Vp7lG0RAzr4HXqtYwacfMErOuv1U3wrvxHX6rD1sV6xS3WXG3r8TRQ2r6w8OhvSdWt+z41hQNwNm3Xw==", "requires": { "@types/babel__traverse": "^7.0.6" } }, "babel-preset-jest": { - "version": "24.6.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz", - "integrity": "sha512-pdZqLEdmy1ZK5kyRUfvBb2IfTPb2BUvIJczlPspS8fWmBQslNNDBqVfh7BW5leOVJMDZKzjD8XEyABTk6gQ5yw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.9.0.tgz", + "integrity": "sha512-izTUuhE4TMfTRPF92fFwD2QfdXaZW08qvWTFCI51V8rW5x00UuPgc3ajRoWofXOuxjfcOM5zzSYsQS3H8KGCAg==", "requires": { "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "babel-plugin-jest-hoist": "^24.6.0" + "babel-plugin-jest-hoist": "^24.9.0" } }, "balanced-match": { @@ -7584,6 +7444,23 @@ "is-data-descriptor": "^1.0.0", "kind-of": "^6.0.2" } + }, + "is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "requires": { + "is-plain-object": "^2.0.4" + } + }, + "mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "requires": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + } } } }, @@ -7612,24 +7489,12 @@ } }, "bl": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-0.7.0.tgz", - "integrity": "sha1-P7BnBgKsKHjrdw3CA58YNr5irls=", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "requires": { - "readable-stream": "~1.0.2" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - } + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" } }, "brace-expansion": { @@ -7730,11 +7595,6 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, - "busyman": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/busyman/-/busyman-0.3.0.tgz", - "integrity": "sha1-FN6kn2JgfOSy0wsgPGMG72zKiKc=" - }, "cache-base": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", @@ -7762,9 +7622,9 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" }, "caniuse-lite": { - "version": "1.0.30000988", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000988.tgz", - "integrity": "sha512-lPj3T8poYrRc/bniW5SQPND3GRtSrQdUM/R4mCYTbZxyi3jQiggLvZH4+BYUuX0t4TXjU+vMM7KFDQg+rSzZUQ==" + "version": "1.0.30000989", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000989.tgz", + "integrity": "sha512-vrMcvSuMz16YY6GSVZ0dWDTJP8jqk3iFQ/Aq5iqblPwxSVVZI+zxDyTX0VPqtQsDnfdrBDcsmhgTEOh5R8Lbpw==" }, "capture-exit": { "version": "2.0.0", @@ -7826,11 +7686,11 @@ } }, "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", "requires": { - "restore-cursor": "^2.0.0" + "restore-cursor": "^3.1.0" } }, "cli-width": { @@ -7839,19 +7699,24 @@ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" }, "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", + "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "is-fullwidth-code-point": { "version": "2.0.0", @@ -7859,20 +7724,21 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { + "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "strip-ansi": "^5.1.0" } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } } } @@ -7933,11 +7799,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "concentrate": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/concentrate/-/concentrate-0.2.3.tgz", - "integrity": "sha1-+XoCBlIcLXxFFxtkz8TW+LYwJUI=" - }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", @@ -7957,13 +7818,12 @@ "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" }, "core-js-compat": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.4.tgz", - "integrity": "sha512-Z5zbO9f1d0YrJdoaQhphVAnKPimX92D6z8lCGphH89MNRxlL1prI9ExJPqVwP0/kgkQCv8c4GJGT8X16yUncOg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.2.1.tgz", + "integrity": "sha512-MwPZle5CF9dEaMYdDeWm73ao/IflDH+FjeJCWEADcEgFSE9TLimFKwJsfmkwzI8eC0Aj0mgvMDjeQjrElkz4/A==", "requires": { - "browserslist": "^4.6.2", - "core-js-pure": "3.1.4", - "semver": "^6.1.1" + "browserslist": "^4.6.6", + "semver": "^6.3.0" }, "dependencies": { "semver": { @@ -7973,11 +7833,6 @@ } } }, - "core-js-pure": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.4.tgz", - "integrity": "sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==" - }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -8140,18 +7995,9 @@ "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" }, "diff-sequences": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.3.0.tgz", - "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==" - }, - "dissolve": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/dissolve/-/dissolve-0.3.3.tgz", - "integrity": "sha1-uX7x/ymJx4nOz7AxB+F0EfqL5uU=", - "requires": { - "bl": "^0.7.0", - "readable-stream": "^1.0.26" - } + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.9.0.tgz", + "integrity": "sha512-Dj6Wk3tWyTE+Fo1rW8v0Xhwk80um6yFYKbuAxc9c3EZxIHFDYwbi34Uk42u1CdnIiVorvt4RmlSDjIPyzGC2ew==" }, "doctrine": { "version": "3.0.0", @@ -8179,14 +8025,14 @@ } }, "electron-to-chromium": { - "version": "1.3.212", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.212.tgz", - "integrity": "sha512-H8z5Smi1s1u1zGegEBfbxUAzrxyk1JoRHHHrlNGfhxv3sTb+p/Jz7JDvrR4196Q/Ip8r4+XwWcLvKrUjFKoJAg==" + "version": "1.3.237", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.237.tgz", + "integrity": "sha512-SPAFjDr/7iiVK2kgTluwxela6eaWjjFkS9rO/iYpB/KGXgccUom5YC7OIf19c8m8GGptWxLU0Em8xM64A/N7Fg==" }, "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" }, "end-of-stream": { "version": "1.4.1", @@ -8196,14 +8042,6 @@ "once": "^1.4.0" } }, - "enum": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/enum/-/enum-2.5.0.tgz", - "integrity": "sha1-8gW4xlozWorOgIEQXflxsYVouYQ=", - "requires": { - "is-buffer": "^1.1.0" - } - }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -8241,9 +8079,9 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" }, "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.12.0.tgz", + "integrity": "sha512-TuA+EhsanGcme5T3R0L80u4t8CpbXQjegRmf7+FPTJrtCTErXFeelblRgHQa1FofEzqYYJmJ/OqjTwREp9qgmg==", "requires": { "esprima": "^3.1.3", "estraverse": "^4.2.0", @@ -8266,9 +8104,9 @@ } }, "eslint": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.1.0.tgz", - "integrity": "sha512-QhrbdRD7ofuV09IuE2ySWBz0FyXCq0rriLTZXZqaWSI79CVtHVRdkFuFTViiqzZhkCgfOh9USpriuGN2gIpZDQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.2.1.tgz", + "integrity": "sha512-ES7BzEzr0Q6m5TK9i+/iTpKjclXitOdDK4vT07OqbkBT2/VcN/gO9EL1C4HlK3TAOXYv2ItcmbVR9jO1MR0fJg==", "requires": { "@babel/code-frame": "^7.0.0", "ajv": "^6.10.0", @@ -8277,9 +8115,9 @@ "debug": "^4.0.1", "doctrine": "^3.0.0", "eslint-scope": "^5.0.0", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^6.0.0", + "eslint-utils": "^1.4.2", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.0", "esquery": "^1.0.1", "esutils": "^2.0.2", "file-entry-cache": "^5.0.1", @@ -8353,26 +8191,26 @@ } }, "eslint-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.0.tgz", - "integrity": "sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", + "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", "requires": { "eslint-visitor-keys": "^1.0.0" } }, "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==" }, "espree": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.0.0.tgz", - "integrity": "sha512-lJvCS6YbCn3ImT3yKkPe0+tJ+mH6ljhGNjHQH9mRtiO6gjhVAOhVXW1yjnwqGwTkK3bGbye+hb00nFNmu0l/1Q==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.0.tgz", + "integrity": "sha512-boA7CHRLlVWUSg3iL5Kmlt/xT3Q+sXnKoRYYzj1YeM10A76TEJBbotV5pKbnK42hEUIr121zTv+QLRM5LsCPXQ==", "requires": { - "acorn": "^6.0.7", + "acorn": "^7.0.0", "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" + "eslint-visitor-keys": "^1.1.0" } }, "esprima": { @@ -8397,9 +8235,9 @@ } }, "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" }, "esutils": { "version": "2.0.3", @@ -8481,16 +8319,16 @@ "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" }, "expect": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-24.8.0.tgz", - "integrity": "sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-24.9.0.tgz", + "integrity": "sha512-wvVAx8XIol3Z5m9zvZXiyZOQ+sRJqNTIm6sGjdWlaZIeupQGO3WbYI+15D/AmEwZywL6wtJkbAbJtzkOfBuR0Q==", "requires": { - "@jest/types": "^24.8.0", + "@jest/types": "^24.9.0", "ansi-styles": "^3.2.0", - "jest-get-type": "^24.8.0", - "jest-matcher-utils": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-regex-util": "^24.3.0" + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-regex-util": "^24.9.0" } }, "extend": { @@ -8525,6 +8363,16 @@ "chardet": "^0.7.0", "iconv-lite": "^0.4.24", "tmp": "^0.0.33" + }, + "dependencies": { + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + } } }, "extglob": { @@ -8615,9 +8463,9 @@ } }, "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.0.0.tgz", + "integrity": "sha512-HKri+WoWoUgr83pehn/SIgLOMZ9nAWC6dcGj26RY2R4F50u4+RTUz0RCrUlOV3nKRAICW1UGzyb+kcX2qK1S/g==", "requires": { "escape-string-regexp": "^1.0.5" } @@ -9224,9 +9072,9 @@ } }, "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, "get-stream": { "version": "4.1.0", @@ -9292,9 +9140,9 @@ "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, "graceful-fs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", - "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", + "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==" }, "growly": { "version": "1.3.0", @@ -9386,9 +9234,9 @@ } }, "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.4.tgz", + "integrity": "sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ==" }, "html-encoding-sniffer": { "version": "1.0.2", @@ -9469,52 +9317,43 @@ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inquirer": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.0.tgz", - "integrity": "sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA==", + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.1.tgz", + "integrity": "sha512-uxNHBeQhRXIoHWTSNYUFhQVrHYFThIt6IVo2fFmSe8aBwdR3/w6b58hJpiL/fMukFkvGzjg+hSxFtwvVmKZmXw==", "requires": { - "ansi-escapes": "^3.2.0", + "ansi-escapes": "^4.2.1", "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", + "cli-cursor": "^3.1.0", "cli-width": "^2.0.0", "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", "run-async": "^2.2.0", "rxjs": "^6.4.0", - "string-width": "^2.1.0", + "string-width": "^4.1.0", "strip-ansi": "^5.1.0", "through": "^2.3.6" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.1.0.tgz", + "integrity": "sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ==", "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^5.2.0" } }, "strip-ansi": { @@ -9523,13 +9362,6 @@ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - } } } } @@ -9542,11 +9374,6 @@ "loose-envify": "^1.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" - }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -9727,9 +9554,9 @@ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" }, "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -9820,200 +9647,201 @@ } }, "jest": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-24.8.0.tgz", - "integrity": "sha512-o0HM90RKFRNWmAWvlyV8i5jGZ97pFwkeVoGvPW1EtLTgJc2+jcuqcbbqcSZLE/3f2S5pt0y2ZBETuhpWNl1Reg==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", + "integrity": "sha512-YvkBL1Zm7d2B1+h5fHEOdyjCG+sGMz4f8D86/0HiqJ6MB4MnDc8FgP5vdWsGnemOQro7lnYo8UakZ3+5A0jxGw==", "requires": { "import-local": "^2.0.0", - "jest-cli": "^24.8.0" + "jest-cli": "^24.9.0" }, "dependencies": { "jest-cli": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.8.0.tgz", - "integrity": "sha512-+p6J00jSMPQ116ZLlHJJvdf8wbjNbZdeSX9ptfHX06/MSNaXmKihQzx5vQcw0q2G6JsdVkUIdWbOWtSnaYs3yA==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.9.0.tgz", + "integrity": "sha512-+VLRKyitT3BWoMeSUIHRxV/2g8y9gw91Jh5z2UmXZzkZKpbC08CSehVxgHUwTpy+HwGcns/tqafQDJW7imYvGg==", "requires": { - "@jest/core": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", + "@jest/core": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", "chalk": "^2.0.1", "exit": "^0.1.2", "import-local": "^2.0.0", "is-ci": "^2.0.0", - "jest-config": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", + "jest-config": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", "prompts": "^2.0.1", "realpath-native": "^1.1.0", - "yargs": "^12.0.2" + "yargs": "^13.3.0" } } } }, "jest-changed-files": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.8.0.tgz", - "integrity": "sha512-qgANC1Yrivsq+UrLXsvJefBKVoCsKB0Hv+mBb6NMjjZ90wwxCDmU3hsCXBya30cH+LnPYjwgcU65i6yJ5Nfuug==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.9.0.tgz", + "integrity": "sha512-6aTWpe2mHF0DhL28WjdkO8LyGjs3zItPET4bMSeXU6T3ub4FPMw+mcOcbdGXQOAfmLcxofD23/5Bl9Z4AkFwqg==", "requires": { - "@jest/types": "^24.8.0", + "@jest/types": "^24.9.0", "execa": "^1.0.0", "throat": "^4.0.0" } }, "jest-config": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.8.0.tgz", - "integrity": "sha512-Czl3Nn2uEzVGsOeaewGWoDPD8GStxCpAe0zOYs2x2l0fZAgPbCr3uwUkgNKV3LwE13VXythM946cd5rdGkkBZw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.9.0.tgz", + "integrity": "sha512-RATtQJtVYQrp7fvWg6f5y3pEFj9I+H8sWw4aKxnDZ96mob5i5SD6ZEGWgMLXQ4LE8UurrjbdlLWdUeo+28QpfQ==", "requires": { "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^24.8.0", - "@jest/types": "^24.8.0", - "babel-jest": "^24.8.0", + "@jest/test-sequencer": "^24.9.0", + "@jest/types": "^24.9.0", + "babel-jest": "^24.9.0", "chalk": "^2.0.1", "glob": "^7.1.1", - "jest-environment-jsdom": "^24.8.0", - "jest-environment-node": "^24.8.0", - "jest-get-type": "^24.8.0", - "jest-jasmine2": "^24.8.0", + "jest-environment-jsdom": "^24.9.0", + "jest-environment-node": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-jasmine2": "^24.9.0", "jest-regex-util": "^24.3.0", - "jest-resolve": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", + "jest-resolve": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", "micromatch": "^3.1.10", - "pretty-format": "^24.8.0", + "pretty-format": "^24.9.0", "realpath-native": "^1.1.0" } }, "jest-diff": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.8.0.tgz", - "integrity": "sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.9.0.tgz", + "integrity": "sha512-qMfrTs8AdJE2iqrTp0hzh7kTd2PQWrsFyj9tORoKmu32xjPjeE4NyjVRDz8ybYwqS2ik8N4hsIpiVTyFeo2lBQ==", "requires": { "chalk": "^2.0.1", - "diff-sequences": "^24.3.0", - "jest-get-type": "^24.8.0", - "pretty-format": "^24.8.0" + "diff-sequences": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" } }, "jest-docblock": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.3.0.tgz", - "integrity": "sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.9.0.tgz", + "integrity": "sha512-F1DjdpDMJMA1cN6He0FNYNZlo3yYmOtRUnktrT9Q37njYzC5WEaDdmbynIgy0L/IvXvvgsG8OsqhLPXTpfmZAA==", "requires": { "detect-newline": "^2.1.0" } }, "jest-each": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.8.0.tgz", - "integrity": "sha512-NrwK9gaL5+XgrgoCsd9svsoWdVkK4gnvyhcpzd6m487tXHqIdYeykgq3MKI1u4I+5Zf0tofr70at9dWJDeb+BA==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.9.0.tgz", + "integrity": "sha512-ONi0R4BvW45cw8s2Lrx8YgbeXL1oCQ/wIDwmsM3CqM/nlblNCPmnC3IPQlMbRFZu3wKdQ2U8BqM6lh3LJ5Bsog==", "requires": { - "@jest/types": "^24.8.0", + "@jest/types": "^24.9.0", "chalk": "^2.0.1", - "jest-get-type": "^24.8.0", - "jest-util": "^24.8.0", - "pretty-format": "^24.8.0" + "jest-get-type": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0" } }, "jest-environment-jsdom": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.8.0.tgz", - "integrity": "sha512-qbvgLmR7PpwjoFjM/sbuqHJt/NCkviuq9vus9NBn/76hhSidO+Z6Bn9tU8friecegbJL8gzZQEMZBQlFWDCwAQ==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.9.0.tgz", + "integrity": "sha512-Zv9FV9NBRzLuALXjvRijO2351DRQeLYXtpD4xNvfoVFw21IOKNhZAEUKcbiEtjTkm2GsJ3boMVgkaR7rN8qetA==", "requires": { - "@jest/environment": "^24.8.0", - "@jest/fake-timers": "^24.8.0", - "@jest/types": "^24.8.0", - "jest-mock": "^24.8.0", - "jest-util": "^24.8.0", + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0", "jsdom": "^11.5.1" } }, "jest-environment-node": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.8.0.tgz", - "integrity": "sha512-vIGUEScd1cdDgR6sqn2M08sJTRLQp6Dk/eIkCeO4PFHxZMOgy+uYLPMC4ix3PEfM5Au/x3uQ/5Tl0DpXXZsJ/Q==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.9.0.tgz", + "integrity": "sha512-6d4V2f4nxzIzwendo27Tr0aFm+IXWa0XEUnaH6nU0FMaozxovt+sfRvh4J47wL1OvF83I3SSTu0XK+i4Bqe7uA==", "requires": { - "@jest/environment": "^24.8.0", - "@jest/fake-timers": "^24.8.0", - "@jest/types": "^24.8.0", - "jest-mock": "^24.8.0", - "jest-util": "^24.8.0" + "@jest/environment": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/types": "^24.9.0", + "jest-mock": "^24.9.0", + "jest-util": "^24.9.0" } }, "jest-get-type": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.8.0.tgz", - "integrity": "sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ==" + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz", + "integrity": "sha512-lUseMzAley4LhIcpSP9Jf+fTrQ4a1yHQwLNeeVa2cEmbCGeoZAtYPOIv8JaxLD/sUpKxetKGP+gsHl8f8TSj8Q==" }, "jest-haste-map": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.8.1.tgz", - "integrity": "sha512-SwaxMGVdAZk3ernAx2Uv2sorA7jm3Kx+lR0grp6rMmnY06Kn/urtKx1LPN2mGTea4fCT38impYT28FfcLUhX0g==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.9.0.tgz", + "integrity": "sha512-kfVFmsuWui2Sj1Rp1AJ4D9HqJwE4uwTlS/vO+eRUaMmd54BFpli2XhMQnPC2k4cHFVbB2Q2C+jtI1AGLgEnCjQ==", "requires": { - "@jest/types": "^24.8.0", + "@jest/types": "^24.9.0", "anymatch": "^2.0.0", "fb-watchman": "^2.0.0", "fsevents": "^1.2.7", "graceful-fs": "^4.1.15", "invariant": "^2.2.4", - "jest-serializer": "^24.4.0", - "jest-util": "^24.8.0", - "jest-worker": "^24.6.0", + "jest-serializer": "^24.9.0", + "jest-util": "^24.9.0", + "jest-worker": "^24.9.0", "micromatch": "^3.1.10", "sane": "^4.0.3", "walker": "^1.0.7" } }, "jest-jasmine2": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz", - "integrity": "sha512-cEky88npEE5LKd5jPpTdDCLvKkdyklnaRycBXL6GNmpxe41F0WN44+i7lpQKa/hcbXaQ+rc9RMaM4dsebrYong==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.9.0.tgz", + "integrity": "sha512-Cq7vkAgaYKp+PsX+2/JbTarrk0DmNhsEtqBXNwUHkdlbrTBLtMJINADf2mf5FkowNsq8evbPc07/qFO0AdKTzw==", "requires": { "@babel/traverse": "^7.1.0", - "@jest/environment": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", "chalk": "^2.0.1", "co": "^4.6.0", - "expect": "^24.8.0", + "expect": "^24.9.0", "is-generator-fn": "^2.0.0", - "jest-each": "^24.8.0", - "jest-matcher-utils": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-snapshot": "^24.8.0", - "jest-util": "^24.8.0", - "pretty-format": "^24.8.0", + "jest-each": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "pretty-format": "^24.9.0", "throat": "^4.0.0" } }, "jest-leak-detector": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.8.0.tgz", - "integrity": "sha512-cG0yRSK8A831LN8lIHxI3AblB40uhv0z+SsQdW3GoMMVcK+sJwrIIyax5tu3eHHNJ8Fu6IMDpnLda2jhn2pD/g==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.9.0.tgz", + "integrity": "sha512-tYkFIDsiKTGwb2FG1w8hX9V0aUb2ot8zY/2nFg087dUageonw1zrLMP4W6zsRO59dPkTSKie+D4rhMuP9nRmrA==", "requires": { - "pretty-format": "^24.8.0" + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" } }, "jest-matcher-utils": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz", - "integrity": "sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.9.0.tgz", + "integrity": "sha512-OZz2IXsu6eaiMAwe67c1T+5tUAtQyQx27/EMEkbFAGiw52tB9em+uGbzpcgYVpA8wl0hlxKPZxrly4CXU/GjHA==", "requires": { "chalk": "^2.0.1", - "jest-diff": "^24.8.0", - "jest-get-type": "^24.8.0", - "pretty-format": "^24.8.0" + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "pretty-format": "^24.9.0" } }, "jest-message-util": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.8.0.tgz", - "integrity": "sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.9.0.tgz", + "integrity": "sha512-oCj8FiZ3U0hTP4aSui87P4L4jC37BtQwUMqk+zk/b11FR19BJDeZsZAvIHutWnmtw7r85UmR3CEWZ0HWU2mAlw==", "requires": { "@babel/code-frame": "^7.0.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", "@types/stack-utils": "^1.0.1", "chalk": "^2.0.1", "micromatch": "^3.1.10", @@ -10022,11 +9850,11 @@ } }, "jest-mock": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.8.0.tgz", - "integrity": "sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.9.0.tgz", + "integrity": "sha512-3BEYN5WbSq9wd+SyLDES7AHnjH9A/ROBwmz7l2y+ol+NtSFO8DYiEBzoO1CeFc9a8DYy10EO4dDFVv/wN3zl1w==", "requires": { - "@jest/types": "^24.8.0" + "@jest/types": "^24.9.0" } }, "jest-pnp-resolver": { @@ -10035,16 +9863,16 @@ "integrity": "sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==" }, "jest-regex-util": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.3.0.tgz", - "integrity": "sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg==" + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.9.0.tgz", + "integrity": "sha512-05Cmb6CuxaA+Ys6fjr3PhvV3bGQmO+2p2La4hFbU+W5uOc479f7FdLXUWXw4pYMAhhSZIuKHwSXSu6CsSBAXQA==" }, "jest-resolve": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.8.0.tgz", - "integrity": "sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.9.0.tgz", + "integrity": "sha512-TaLeLVL1l08YFZAt3zaPtjiVvyy4oSA6CRe+0AFPPVX3Q/VI0giIWWoAvoS5L96vj9Dqxj4fB5p2qrHCmTU/MQ==", "requires": { - "@jest/types": "^24.8.0", + "@jest/types": "^24.9.0", "browser-resolve": "^1.11.3", "chalk": "^2.0.1", "jest-pnp-resolver": "^1.2.1", @@ -10052,105 +9880,113 @@ } }, "jest-resolve-dependencies": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.8.0.tgz", - "integrity": "sha512-hyK1qfIf/krV+fSNyhyJeq3elVMhK9Eijlwy+j5jqmZ9QsxwKBiP6qukQxaHtK8k6zql/KYWwCTQ+fDGTIJauw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.9.0.tgz", + "integrity": "sha512-Fm7b6AlWnYhT0BXy4hXpactHIqER7erNgIsIozDXWl5dVm+k8XdGVe1oTg1JyaFnOxarMEbax3wyRJqGP2Pq+g==", "requires": { - "@jest/types": "^24.8.0", + "@jest/types": "^24.9.0", "jest-regex-util": "^24.3.0", - "jest-snapshot": "^24.8.0" + "jest-snapshot": "^24.9.0" } }, "jest-runner": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.8.0.tgz", - "integrity": "sha512-utFqC5BaA3JmznbissSs95X1ZF+d+4WuOWwpM9+Ak356YtMhHE/GXUondZdcyAAOTBEsRGAgH/0TwLzfI9h7ow==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.9.0.tgz", + "integrity": "sha512-KksJQyI3/0mhcfspnxxEOBueGrd5E4vV7ADQLT9ESaCzz02WnbdbKWIf5Mkaucoaj7obQckYPVX6JJhgUcoWWg==", "requires": { "@jest/console": "^24.7.1", - "@jest/environment": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", + "@jest/environment": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", "chalk": "^2.4.2", "exit": "^0.1.2", "graceful-fs": "^4.1.15", - "jest-config": "^24.8.0", + "jest-config": "^24.9.0", "jest-docblock": "^24.3.0", - "jest-haste-map": "^24.8.0", - "jest-jasmine2": "^24.8.0", - "jest-leak-detector": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-resolve": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-util": "^24.8.0", + "jest-haste-map": "^24.9.0", + "jest-jasmine2": "^24.9.0", + "jest-leak-detector": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", + "jest-runtime": "^24.9.0", + "jest-util": "^24.9.0", "jest-worker": "^24.6.0", "source-map-support": "^0.5.6", "throat": "^4.0.0" } }, "jest-runtime": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.8.0.tgz", - "integrity": "sha512-Mq0aIXhvO/3bX44ccT+czU1/57IgOMyy80oM0XR/nyD5zgBcesF84BPabZi39pJVA6UXw+fY2Q1N+4BiVUBWOA==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.9.0.tgz", + "integrity": "sha512-8oNqgnmF3v2J6PVRM2Jfuj8oX3syKmaynlDMMKQ4iyzbQzIG6th5ub/lM2bCMTmoTKM3ykcUYI2Pw9xwNtjMnw==", "requires": { "@jest/console": "^24.7.1", - "@jest/environment": "^24.8.0", + "@jest/environment": "^24.9.0", "@jest/source-map": "^24.3.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "@types/yargs": "^12.0.2", + "@jest/transform": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", "chalk": "^2.0.1", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.1.15", - "jest-config": "^24.8.0", - "jest-haste-map": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-mock": "^24.8.0", + "jest-config": "^24.9.0", + "jest-haste-map": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-mock": "^24.9.0", "jest-regex-util": "^24.3.0", - "jest-resolve": "^24.8.0", - "jest-snapshot": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", + "jest-resolve": "^24.9.0", + "jest-snapshot": "^24.9.0", + "jest-util": "^24.9.0", + "jest-validate": "^24.9.0", "realpath-native": "^1.1.0", "slash": "^2.0.0", "strip-bom": "^3.0.0", - "yargs": "^12.0.2" + "yargs": "^13.3.0" } }, "jest-serializer": { - "version": "24.4.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.4.0.tgz", - "integrity": "sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q==" + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.9.0.tgz", + "integrity": "sha512-DxYipDr8OvfrKH3Kel6NdED3OXxjvxXZ1uIY2I9OFbGg+vUkkg7AGvi65qbhbWNPvDckXmzMPbK3u3HaDO49bQ==" }, "jest-snapshot": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.8.0.tgz", - "integrity": "sha512-5ehtWoc8oU9/cAPe6fez6QofVJLBKyqkY2+TlKTOf0VllBB/mqUNdARdcjlZrs9F1Cv+/HKoCS/BknT0+tmfPg==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.9.0.tgz", + "integrity": "sha512-uI/rszGSs73xCM0l+up7O7a40o90cnrk429LOiK3aeTvfC0HHmldbd81/B7Ix81KSFe1lwkbl7GnBGG4UfuDew==", "requires": { "@babel/types": "^7.0.0", - "@jest/types": "^24.8.0", + "@jest/types": "^24.9.0", "chalk": "^2.0.1", - "expect": "^24.8.0", - "jest-diff": "^24.8.0", - "jest-matcher-utils": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-resolve": "^24.8.0", + "expect": "^24.9.0", + "jest-diff": "^24.9.0", + "jest-get-type": "^24.9.0", + "jest-matcher-utils": "^24.9.0", + "jest-message-util": "^24.9.0", + "jest-resolve": "^24.9.0", "mkdirp": "^0.5.1", "natural-compare": "^1.4.0", - "pretty-format": "^24.8.0", - "semver": "^5.5.0" + "pretty-format": "^24.9.0", + "semver": "^6.2.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "jest-util": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.8.0.tgz", - "integrity": "sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.9.0.tgz", + "integrity": "sha512-x+cZU8VRmOJxbA1K5oDBdxQmdq0OIdADarLxk0Mq+3XS4jgvhG/oKGWcIDCtPG0HgjxOYvF+ilPJQsAyXfbNOg==", "requires": { - "@jest/console": "^24.7.1", - "@jest/fake-timers": "^24.8.0", - "@jest/source-map": "^24.3.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", + "@jest/console": "^24.9.0", + "@jest/fake-timers": "^24.9.0", + "@jest/source-map": "^24.9.0", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", "callsites": "^3.0.0", "chalk": "^2.0.1", "graceful-fs": "^4.1.15", @@ -10168,38 +10004,45 @@ } }, "jest-validate": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.8.0.tgz", - "integrity": "sha512-+/N7VOEMW1Vzsrk3UWBDYTExTPwf68tavEPKDnJzrC6UlHtUDU/fuEdXqFoHzv9XnQ+zW6X3qMZhJ3YexfeLDA==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.9.0.tgz", + "integrity": "sha512-HPIt6C5ACwiqSiwi+OfSSHbK8sG7akG8eATl+IPKaeIjtPOeBUd/g3J7DghugzxrGjI93qS/+RPKe1H6PqvhRQ==", "requires": { - "@jest/types": "^24.8.0", - "camelcase": "^5.0.0", + "@jest/types": "^24.9.0", + "camelcase": "^5.3.1", "chalk": "^2.0.1", - "jest-get-type": "^24.8.0", - "leven": "^2.1.0", - "pretty-format": "^24.8.0" + "jest-get-type": "^24.9.0", + "leven": "^3.1.0", + "pretty-format": "^24.9.0" } }, "jest-watcher": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.8.0.tgz", - "integrity": "sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.9.0.tgz", + "integrity": "sha512-+/fLOfKPXXYJDYlks62/4R4GoT+GU1tYZed99JSCOsmzkkF7727RqKrjNAxtfO4YpGv11wybgRvCjR73lK2GZw==", "requires": { - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "@types/yargs": "^12.0.9", + "@jest/test-result": "^24.9.0", + "@jest/types": "^24.9.0", + "@types/yargs": "^13.0.0", "ansi-escapes": "^3.0.0", "chalk": "^2.0.1", - "jest-util": "^24.8.0", + "jest-util": "^24.9.0", "string-length": "^2.0.0" + }, + "dependencies": { + "ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" + } } }, "jest-worker": { - "version": "24.6.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.6.0.tgz", - "integrity": "sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz", + "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==", "requires": { - "merge-stream": "^1.0.1", + "merge-stream": "^2.0.0", "supports-color": "^6.1.0" }, "dependencies": { @@ -10343,23 +10186,15 @@ "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "requires": { - "invert-kv": "^2.0.0" - } - }, "left-pad": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" }, "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" }, "levn": { "version": "0.3.0", @@ -10453,14 +10288,6 @@ "tmpl": "1.0.x" } }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "requires": { - "p-defer": "^1.0.0" - } - }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -10474,59 +10301,10 @@ "object-visit": "^1.0.0" } }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - } - } - }, "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "requires": { - "readable-stream": "^2.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, "micromatch": { "version": "3.1.10", @@ -10562,9 +10340,9 @@ } }, "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" }, "mimic-response": { "version": "1.0.1", @@ -10585,23 +10363,9 @@ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-2.0.1.tgz", + "integrity": "sha512-imbHQNRglyaplMmjBLL3V5R6Bfq5oM+ivds3SKgc6oRtzErEnBUUc5No11Z2pilkUvl42gJvi285xTNswcKCMA==" }, "mkdirp": { "version": "0.5.1", @@ -10617,9 +10381,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" }, "nan": { "version": "2.14.0", @@ -10677,9 +10441,9 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, "node-abi": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.10.0.tgz", - "integrity": "sha512-OT0WepUvYHXdki6DU8LWhEkuo3M44i2paWBYtH9qXtPb9YiKlYEKa5WUII20XEcOv7UJPzfB0kZfPZdW46zdkw==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.11.0.tgz", + "integrity": "sha512-kuy/aEg75u40v378WRllQ4ZexaXJiCvB68D2scDXclp/I4cRq6togpbOoKhmN07tns9Zldu51NNERo0wehfX9g==", "requires": { "semver": "^5.4.1" } @@ -10695,9 +10459,9 @@ "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" }, "node-notifier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", - "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.3.tgz", + "integrity": "sha512-M4UBGcs4jeOK9CjTsYwkvH6/MzuUmGCyTW+kCY7uO+1ZVr0+FHGdPdIf5CCLqAaxnRrWidyoQlNkMIIVwbKB8Q==", "requires": { "growly": "^1.3.0", "is-wsl": "^1.1.0", @@ -10707,9 +10471,9 @@ } }, "node-releases": { - "version": "1.1.26", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.26.tgz", - "integrity": "sha512-fZPsuhhUHMTlfkhDLGtfY80DSJTjOcx+qD1j5pqPkuhUHVS7xHZIg9EE4DHK8O3f0zTxXHX5VIkDG8pu98/wfQ==", + "version": "1.1.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.27.tgz", + "integrity": "sha512-9iXUqHKSGo6ph/tdXVbHFbhRVQln4ZDTIBJCzsa90HimnBYc5jw8RWYt4wBYFHehGyC3koIz5O4mb2fHrbPOuA==", "requires": { "semver": "^5.3.0" } @@ -10846,15 +10610,6 @@ "isobject": "^3.0.1" } }, - "objectbox": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/objectbox/-/objectbox-0.3.0.tgz", - "integrity": "sha1-pefMgeff0iA/6HSI/9InaEXAjhs=", - "requires": { - "busyman": "^0.3.0", - "nedb": "^1.8.0" - } - }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -10864,11 +10619,11 @@ } }, "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", "requires": { - "mimic-fn": "^1.0.0" + "mimic-fn": "^2.1.0" } }, "optimist": { @@ -10905,26 +10660,11 @@ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" - }, "p-each-series": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", @@ -10938,15 +10678,10 @@ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" - }, "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.1.tgz", + "integrity": "sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg==", "requires": { "p-try": "^2.0.0" } @@ -11096,11 +10831,11 @@ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" }, "pretty-format": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.8.0.tgz", - "integrity": "sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw==", + "version": "24.9.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.9.0.tgz", + "integrity": "sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==", "requires": { - "@jest/types": "^24.8.0", + "@jest/types": "^24.9.0", "ansi-regex": "^4.0.0", "ansi-styles": "^3.2.0", "react-is": "^16.8.4" @@ -11129,19 +10864,14 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" }, "prompts": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.1.0.tgz", - "integrity": "sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.2.1.tgz", + "integrity": "sha512-VObPvJiWPhpZI6C5m60XOzTfnYg/xc/an+r9VYymj9WJW3B/DIH+REzjpAACPf8brwPeP+7vz3bIim3S+AaMjw==", "requires": { - "kleur": "^3.0.2", - "sisteransi": "^1.0.0" + "kleur": "^3.0.3", + "sisteransi": "^1.0.3" } }, - "proving": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/proving/-/proving-0.1.0.tgz", - "integrity": "sha1-dL8fZ4yeXaeHV3CAyQxLaff/pqU=" - }, "psl": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz", @@ -11161,24 +10891,11 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" - }, "qs": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" }, - "queue": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.1.tgz", - "integrity": "sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg==", - "requires": { - "inherits": "~2.0.3" - } - }, "rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -11198,9 +10915,9 @@ } }, "react-is": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", - "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz", + "integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==" }, "read-pkg": { "version": "3.0.0", @@ -11222,14 +10939,17 @@ } }, "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "requires": { "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "realpath-native": { @@ -11286,12 +11006,12 @@ "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==" }, "regexpu-core": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.4.tgz", - "integrity": "sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.5.tgz", + "integrity": "sha512-FpI67+ky9J+cDizQUJlIlNZFKual/lUkFr1AG6zOCpwZ9cLrg8UUVakyUQJD7fCDIe9Z2nwTQJNPyonatNmDFQ==", "requires": { "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.0.2", + "regenerate-unicode-properties": "^8.1.0", "regjsgen": "^0.5.0", "regjsparser": "^0.6.0", "unicode-match-property-ecmascript": "^1.0.4", @@ -11438,11 +11158,11 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" }, "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "requires": { - "onetime": "^2.0.0", + "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, @@ -11527,9 +11247,9 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" }, "serialport": { "version": "7.1.5", @@ -11613,9 +11333,9 @@ } }, "sisteransi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.2.tgz", - "integrity": "sha512-ZcYcZcT69nSLAR2oLN2JwNmLkJEKGooFMCdvOkFrToUt/WfcRWqhIg4P4KwY4dmLbuyXIx4o4YmPsvMRJYJd/w==" + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.3.tgz", + "integrity": "sha512-SbEG75TzH8G7eVXFSN5f9EExILKfly7SUvVY5DhhYLvfhKqhDFY0OzevWa/zwak0RLRfWS5AvfMWpd9gJvr5Yg==" }, "slash": { "version": "2.0.0", @@ -11908,9 +11628,12 @@ } }, "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } }, "strip-ansi": { "version": "3.0.1", @@ -11949,9 +11672,9 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, "table": { - "version": "5.4.5", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.5.tgz", - "integrity": "sha512-oGa2Hl7CQjfoaogtrOHEJroOcYILTx7BZWLGsJIlzoWmB2zmguhNfPJZsWPKYek/MgCxfco54gEi31d1uN2hFA==", + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", "requires": { "ajv": "^6.10.2", "lodash": "^4.17.14", @@ -11964,6 +11687,11 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -12023,44 +11751,6 @@ "readable-stream": "^2.3.0", "to-buffer": "^1.1.1", "xtend": "^4.0.0" - }, - "dependencies": { - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "test-exclude": { @@ -12090,11 +11780,11 @@ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", + "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", "requires": { - "os-tmpdir": "~1.0.2" + "rimraf": "^2.6.3" } }, "tmpl": { @@ -12178,9 +11868,9 @@ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, "tsutils": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.1.tgz", - "integrity": "sha512-kiuZzD1uUA5DxGj/uxbde+ymp6VVdAxdzOIlAFbYKrPyla8/uiJ9JLBm1QsPhOm4Muj0/+cWEDP99yoCUcSl6Q==", + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", "requires": { "tslib": "^1.8.1" } @@ -12206,6 +11896,11 @@ "prelude-ls": "~1.1.2" } }, + "type-fest": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", + "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==" + }, "typescript": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", @@ -12302,11 +11997,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" } } }, @@ -12343,14 +12033,14 @@ } }, "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", + "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, "v8-compile-cache": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", - "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==" + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==" }, "validate-npm-package-license": { "version": "3.0.4", @@ -12447,12 +12137,48 @@ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" }, "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", + "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } } }, "wrappy": { @@ -12502,62 +12228,61 @@ "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" }, "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.0.tgz", + "integrity": "sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA==", "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", + "cliui": "^5.0.0", "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^3.0.0", "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" + "y18n": "^4.0.0", + "yargs-parser": "^13.1.1" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "requires": { + "emoji-regex": "^7.0.1", "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" + "strip-ansi": "^5.1.0" } }, "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "requires": { - "ansi-regex": "^3.0.0" + "ansi-regex": "^4.1.0" } } } }, "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", + "version": "13.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.1.tgz", + "integrity": "sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ==", "requires": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -12565,6637 +12290,12 @@ } } }, - "zigbee-shepherd-converters": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/zigbee-shepherd-converters/-/zigbee-shepherd-converters-10.2.5.tgz", - "integrity": "sha512-yV7enkUtffYFIBYOtbg2AdnslxvVHJF++k1NJphnafLA9qVNZ3/i4pTNSMkRlsBZCHRNSHxHoTBHkW62zDToGA==", + "zigbee-herdsman-converters": { + "version": "11.0.8", + "resolved": "https://registry.npmjs.org/zigbee-herdsman-converters/-/zigbee-herdsman-converters-11.0.8.tgz", + "integrity": "sha512-pyaTlsKtGzWKIN9GGZa+5wysFZDL9pBfIYuHyoDs7RA8ncCfxV+tZLZ9kxp6aJ5YSpJIhPLR7SJnvtiR/HBm/w==", "requires": { - "chai": "*", - "debounce": "*", - "debug": "3.2.6", - "zigbee-herdsman": "^0.4.0" - }, - "dependencies": { - "zigbee-herdsman": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/zigbee-herdsman/-/zigbee-herdsman-0.4.5.tgz", - "integrity": "sha512-sDRXPj1dq4DVWJrVjn4zsnletWlx3bwc7cxqgSxjUxXWVzOiuDicVDQPPuQEUL7h1oxH+8KCzVlw11oGvuYneg==", - "requires": { - "busyman": "^0.3.0", - "concentrate": "^0.2.3", - "debug": "^3.2.6", - "dissolve": "0.3.3", - "enum": "^2.3.0", - "objectbox": "^0.3.0", - "proving": "^0.1.0", - "q": "^1.5.1", - "queue": "^6.0.1", - "serialport": "7.1.5" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", - "integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==", - "requires": { - "@babel/highlight": "^7.0.0" - } - }, - "@babel/core": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.5.5.tgz", - "integrity": "sha512-i4qoSr2KTtce0DmkuuQBV4AuQgGPUcPXMr9L5MyYAtk06z068lQ10a4O009fe5OB/DfNV+h+qqT7ddNV8UnRjg==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.5.5", - "@babel/helpers": "^7.5.5", - "@babel/parser": "^7.5.5", - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.5.5", - "@babel/types": "^7.5.5", - "convert-source-map": "^1.1.0", - "debug": "^4.1.0", - "json5": "^2.1.0", - "lodash": "^4.17.13", - "resolve": "^1.3.2", - "semver": "^5.4.1", - "source-map": "^0.5.0" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@babel/generator": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.5.5.tgz", - "integrity": "sha512-ETI/4vyTSxTzGnU2c49XHv2zhExkv9JHLTwDAFz85kmcwuShvYG2H08FwgIguQf4JC75CBnXAUM5PqeF4fj0nQ==", - "requires": { - "@babel/types": "^7.5.5", - "jsesc": "^2.5.1", - "lodash": "^4.17.13", - "source-map": "^0.5.0", - "trim-right": "^1.0.1" - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.0.0.tgz", - "integrity": "sha512-3UYcJUj9kvSLbLbUIfQTqzcy5VX7GRZ/CCDrnOaZorFFM01aXp1+GJwuFGV4NDDoAS+mOUyHcO6UD/RfqOks3Q==", - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.1.0.tgz", - "integrity": "sha512-qNSR4jrmJ8M1VMM9tibvyRAHXQs2PmaksQF7c1CGJNipfe3D8p+wgNwgso/P2A2r2mdgBWAXljNWR0QRZAMW8w==", - "requires": { - "@babel/helper-explode-assignable-expression": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-builder-react-jsx": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.3.0.tgz", - "integrity": "sha512-MjA9KgwCuPEkQd9ncSXvSyJ5y+j2sICHyrI0M3L+6fnS4wMSNDc1ARXsbTfbb2cXHn17VisSnU/sHFTCxVxSMw==", - "requires": { - "@babel/types": "^7.3.0", - "esutils": "^2.0.0" - } - }, - "@babel/helper-call-delegate": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-call-delegate/-/helper-call-delegate-7.4.4.tgz", - "integrity": "sha512-l79boDFJ8S1c5hvQvG+rc+wHw6IuH7YldmRKsYtpbawsxURu/paVy57FZMomGK22/JckepaikOkY0MoAmdyOlQ==", - "requires": { - "@babel/helper-hoist-variables": "^7.4.4", - "@babel/traverse": "^7.4.4", - "@babel/types": "^7.4.4" - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.5.tgz", - "integrity": "sha512-ZsxkyYiRA7Bg+ZTRpPvB6AbOFKTFFK4LrvTet8lInm0V468MWCaSYJE+I7v2z2r8KNLtYiV+K5kTCnR7dvyZjg==", - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-member-expression-to-functions": "^7.5.5", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.5.5", - "@babel/helper-split-export-declaration": "^7.4.4" - } - }, - "@babel/helper-define-map": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-map/-/helper-define-map-7.5.5.tgz", - "integrity": "sha512-fTfxx7i0B5NJqvUOBBGREnrqbTxRh7zinBANpZXAVDlsZxYdclDp467G1sQ8VZYMnAURY3RpBUAgOYT9GfzHBg==", - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/types": "^7.5.5", - "lodash": "^4.17.13" - } - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.1.0.tgz", - "integrity": "sha512-NRQpfHrJ1msCHtKjbzs9YcMmJZOg6mQMmGRB+hbamEdG5PNpaSm95275VD92DvJKuyl0s2sFiDmMZ+EnnvufqA==", - "requires": { - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-function-name": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz", - "integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==", - "requires": { - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-get-function-arity": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz", - "integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==", - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.4.4.tgz", - "integrity": "sha512-VYk2/H/BnYbZDDg39hr3t2kKyifAm1W6zHRfhx8jGjIHpQEBv9dry7oQ2f3+J703TLu69nYdxsovl0XYfcnK4w==", - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz", - "integrity": "sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA==", - "requires": { - "@babel/types": "^7.5.5" - } - }, - "@babel/helper-module-imports": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.0.0.tgz", - "integrity": "sha512-aP/hlLq01DWNEiDg4Jn23i+CXxW/owM4WpDLFUbpjxe4NS3BhLVZQ5i7E0ZrxuQ/vwekIeciyamgB1UIYxxM6A==", - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-module-transforms": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.5.5.tgz", - "integrity": "sha512-jBeCvETKuJqeiaCdyaheF40aXnnU1+wkSiUs/IQg3tB85up1LyL8x77ClY8qJpuRJUcXQo+ZtdNESmZl4j56Pw==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-simple-access": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/template": "^7.4.4", - "@babel/types": "^7.5.5", - "lodash": "^4.17.13" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz", - "integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==", - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz", - "integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==" - }, - "@babel/helper-regex": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-regex/-/helper-regex-7.5.5.tgz", - "integrity": "sha512-CkCYQLkfkiugbRDO8eZn6lRuR8kzZoGXCg3149iTk5se7g6qykSpy3+hELSwquhu+TgHn8nkLiBwHvNX8Hofcw==", - "requires": { - "lodash": "^4.17.13" - } - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.1.0.tgz", - "integrity": "sha512-3fOK0L+Fdlg8S5al8u/hWE6vhufGSn0bN09xm2LXMy//REAF8kDCrYoOBKYmA8m5Nom+sV9LyLCwrFynA8/slg==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-wrap-function": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-replace-supers": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz", - "integrity": "sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg==", - "requires": { - "@babel/helper-member-expression-to-functions": "^7.5.5", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/traverse": "^7.5.5", - "@babel/types": "^7.5.5" - } - }, - "@babel/helper-simple-access": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.1.0.tgz", - "integrity": "sha512-Vk+78hNjRbsiu49zAPALxTb+JUQCz1aolpd8osOF16BGnLtseD21nbHgLPGUwrXEurZgiCOUmvs3ExTu4F5x6w==", - "requires": { - "@babel/template": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz", - "integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==", - "requires": { - "@babel/types": "^7.4.4" - } - }, - "@babel/helper-wrap-function": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.2.0.tgz", - "integrity": "sha512-o9fP1BZLLSrYlxYEYyl2aS+Flun5gtjTIG8iln+XuEzQTs0PLagAGSXUcqruJwD5fM48jzIEggCKpIfWTcR7pQ==", - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/template": "^7.1.0", - "@babel/traverse": "^7.1.0", - "@babel/types": "^7.2.0" - } - }, - "@babel/helpers": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.5.5.tgz", - "integrity": "sha512-nRq2BUhxZFnfEn/ciJuhklHvFOqjJUD5wpx+1bxUF2axL9C+v4DE/dmp5sT2dKnpOs4orZWzpAZqlCy8QqE/7g==", - "requires": { - "@babel/template": "^7.4.4", - "@babel/traverse": "^7.5.5", - "@babel/types": "^7.5.5" - } - }, - "@babel/highlight": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz", - "integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==", - "requires": { - "chalk": "^2.0.0", - "esutils": "^2.0.2", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.5.5.tgz", - "integrity": "sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g==" - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.2.0.tgz", - "integrity": "sha512-+Dfo/SCQqrwx48ptLVGLdE39YtWRuKc/Y9I5Fy0P1DDBB9lsAHpjcEJQt+4IifuSOSTLBKJObJqMvaO1pIE8LQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0", - "@babel/plugin-syntax-async-generators": "^7.2.0" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz", - "integrity": "sha512-x/iMjggsKTFHYC6g11PL7Qy58IK8H5zqfm9e6hu4z1iH2IRyAp9u9dL80zA6R76yFovETFLKz2VJIC2iIPBuFw==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.2.0.tgz", - "integrity": "sha512-MAFV1CA/YVmYwZG0fBQyXhmj0BHCB5egZHCKWIFVv/XCxAeVGIHfos3SwDck4LvCllENIAg7xMKOG5kH0dzyUg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-json-strings": "^7.2.0" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.5.5.tgz", - "integrity": "sha512-F2DxJJSQ7f64FyTVl5cw/9MWn6naXGdk3Q3UhDbFEEHv+EilCPoeRD3Zh/Utx1CJz4uyKlQ4uH+bJPbEhMV7Zw==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.2.0.tgz", - "integrity": "sha512-mgYj3jCcxug6KUcX4OBoOJz3CMrwRfQELPQ5560F70YQUBZB7uac9fqaWamKR1iWUzGiK2t0ygzjTScZnVz75g==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.4.4.tgz", - "integrity": "sha512-j1NwnOqMG9mFUOH58JTFsA/+ZYzQLUZ/drqWUqxCYLGeu2JFZL8YrNC9hBxKmWtAuOCHPcRpgv7fhap09Fb4kA==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.5.4" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.2.0.tgz", - "integrity": "sha512-1ZrIRBv2t0GSlcwVoQ6VgSLpLgiN/FVQUzt9znxo7v2Ov4jJrs8RY8tv0wvDmFN3qIdMKWrmMMW6yZ0G19MfGg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.2.0.tgz", - "integrity": "sha512-mVxuJ0YroI/h/tbFTPGZR8cv6ai+STMKNBq0f8hFxsxWjl94qqhsb+wXbpNMDPU3cfR1TIsVFzU3nXyZMqyK4w==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.2.0.tgz", - "integrity": "sha512-5UGYnMSLRE1dqqZwug+1LISpA403HzlSfsg6P9VXU6TBjcSHeNlw4DxDx7LgpF+iKZoOG/+uzqoRHTdcUpiZNg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.2.0.tgz", - "integrity": "sha512-VyN4QANJkRW6lDBmENzRszvZf3/4AXaj9YR7GwrWeeN9tEBPuXbmDYVU9bYBN0D70zCWVwUy0HWq2553VCb6Hw==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz", - "integrity": "sha512-t0JKGgqk2We+9may3t0xDdmneaXmyxq0xieYcKHxIsrJO64n1OiMWNUtc5gQK1PA0NpdCRrtZp4z+IUaKugrSA==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.2.0.tgz", - "integrity": "sha512-bDe4xKNhb0LI7IvZHiA13kff0KEfaGX/Hv4lMA9+7TEc63hMNvfKo6ZFpXhKuEp+II/q35Gc4NoMeDZyaUbj9w==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-syntax-typescript": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.3.3.tgz", - "integrity": "sha512-dGwbSMA1YhVS8+31CnPR7LB4pcbrzcV99wQzby4uAfrkZPYZlQ7ImwdpzLqi6Z6IL02b8IAL379CaMwo0x5Lag==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.2.0.tgz", - "integrity": "sha512-ER77Cax1+8/8jCB9fo4Ud161OZzWN5qawi4GusDuRLcDbDG+bIGYY20zb2dfAFdTRGzrfq2xZPvF0R64EHnimg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.5.0.tgz", - "integrity": "sha512-mqvkzwIGkq0bEF1zLRRiTdjfomZJDV33AH3oQzHVGkI2VzEmXLpKKOBvEVaFZBJdN0XTyH38s9j/Kiqr68dggg==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-remap-async-to-generator": "^7.1.0" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.2.0.tgz", - "integrity": "sha512-ntQPR6q1/NKuphly49+QiQiTN0O63uOwjdD6dhIjSWBI5xlrbUFh720TIpzBhpnrLfv2tNH/BXvLIab1+BAI0w==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.5.5.tgz", - "integrity": "sha512-82A3CLRRdYubkG85lKwhZB0WZoHxLGsJdux/cOVaJCJpvYFl1LVzAIFyRsa7CvXqW8rBM4Zf3Bfn8PHt5DP0Sg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "lodash": "^4.17.13" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.5.5.tgz", - "integrity": "sha512-U2htCNK/6e9K7jGyJ++1p5XRU+LJjrwtoiVn9SzRlDT2KubcZ11OOwy3s24TjHxPgxNwonCYP7U2K51uVYCMDg==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-define-map": "^7.5.5", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-optimise-call-expression": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.5.5", - "@babel/helper-split-export-declaration": "^7.4.4", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.2.0.tgz", - "integrity": "sha512-kP/drqTxY6Xt3NNpKiMomfgkNn4o7+vKxK2DDKcBG9sHj51vHqMBGy8wbDS/J4lMxnqs153/T3+DmCEAkC5cpA==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.5.0.tgz", - "integrity": "sha512-YbYgbd3TryYYLGyC7ZR+Tq8H/+bCmwoaxHfJHupom5ECstzbRLTch6gOQbhEY9Z4hiCNHEURgq06ykFv9JZ/QQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.4.4.tgz", - "integrity": "sha512-P05YEhRc2h53lZDjRPk/OektxCVevFzZs2Gfjd545Wde3k+yFDbXORgl2e0xpbq8mLcKJ7Idss4fAg0zORN/zg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.5.4" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.5.0.tgz", - "integrity": "sha512-igcziksHizyQPlX9gfSjHkE2wmoCH3evvD2qR5w29/Dk0SMKE/eOI7f1HhBdNhR/zxJDqrgpoDTq5YSLH/XMsQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.2.0.tgz", - "integrity": "sha512-umh4hR6N7mu4Elq9GG8TOu9M0bakvlsREEC+ialrQN6ABS4oDQ69qJv1VtR3uxlKMCQMCvzk7vr17RHKcjx68A==", - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.4.4.tgz", - "integrity": "sha512-9T/5Dlr14Z9TIEXLXkt8T1DU7F24cbhwhMNUziN3hB1AXoZcdzPcTiKGRn/6iOymDqtTKWnr/BtRKN9JwbKtdQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.4.4.tgz", - "integrity": "sha512-iU9pv7U+2jC9ANQkKeNF6DrPy4GBa4NWQtl6dHB4Pb3izX2JOEvDTFarlNsBj/63ZEzNNIAMs3Qw4fNCcSOXJA==", - "requires": { - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.2.0.tgz", - "integrity": "sha512-2ThDhm4lI4oV7fVQ6pNNK+sx+c/GM5/SaML0w/r4ZB7sAneD/piDJtwdKlNckXeyGK7wlwg2E2w33C/Hh+VFCg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.2.0.tgz", - "integrity": "sha512-HiU3zKkSU6scTidmnFJ0bMX8hz5ixC93b4MHMiYebmk2lUVNGOboPsqQvx5LzooihijUoLR/v7Nc1rbBtnc7FA==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.5.0.tgz", - "integrity": "sha512-n20UsQMKnWrltocZZm24cRURxQnWIvsABPJlw/fvoy9c6AgHZzoelAIzajDHAQrDpuKFFPPcFGd7ChsYuIUMpg==", - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0", - "babel-plugin-dynamic-import-node": "^2.3.0" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.5.0.tgz", - "integrity": "sha512-xmHq0B+ytyrWJvQTc5OWAC4ii6Dhr0s22STOoydokG51JjWhyYo5mRPXoi+ZmtHQhZZwuXNN+GG5jy5UZZJxIQ==", - "requires": { - "@babel/helper-module-transforms": "^7.4.4", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-simple-access": "^7.1.0", - "babel-plugin-dynamic-import-node": "^2.3.0" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.5.0.tgz", - "integrity": "sha512-Q2m56tyoQWmuNGxEtUyeEkm6qJYFqs4c+XyXH5RAuYxObRNz9Zgj/1g2GMnjYp2EUyEy7YTrxliGCXzecl/vJg==", - "requires": { - "@babel/helper-hoist-variables": "^7.4.4", - "@babel/helper-plugin-utils": "^7.0.0", - "babel-plugin-dynamic-import-node": "^2.3.0" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.2.0.tgz", - "integrity": "sha512-BV3bw6MyUH1iIsGhXlOK6sXhmSarZjtJ/vMiD9dNmpY8QXFFQTj+6v92pcfy1iqa8DeAfJFwoxcrS/TUZda6sw==", - "requires": { - "@babel/helper-module-transforms": "^7.1.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.4.5.tgz", - "integrity": "sha512-z7+2IsWafTBbjNsOxU/Iv5CvTJlr5w4+HGu1HovKYTtgJ362f7kBcQglkfmlspKKZ3bgrbSGvLfNx++ZJgCWsg==", - "requires": { - "regexp-tree": "^0.1.6" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.4.4.tgz", - "integrity": "sha512-r1z3T2DNGQwwe2vPGZMBNjioT2scgWzK9BCnDEh+46z8EEwXBq24uRzd65I7pjtugzPSj921aM15RpESgzsSuA==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.5.5.tgz", - "integrity": "sha512-un1zJQAhSosGFBduPgN/YFNvWVpRuHKU7IHBglLoLZsGmruJPOo6pbInneflUdmq7YvSVqhpPs5zdBvLnteltQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-replace-supers": "^7.5.5" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.4.4.tgz", - "integrity": "sha512-oMh5DUO1V63nZcu/ZVLQFqiihBGo4OpxJxR1otF50GMeCLiRx5nUdtokd+u9SuVJrvvuIh9OosRFPP4pIPnwmw==", - "requires": { - "@babel/helper-call-delegate": "^7.4.4", - "@babel/helper-get-function-arity": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.2.0.tgz", - "integrity": "sha512-9q7Dbk4RhgcLp8ebduOpCbtjh7C0itoLYHXd9ueASKAG/is5PQtMR5VJGka9NKqGhYEGn5ITahd4h9QeBMylWQ==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-react-display-name": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.2.0.tgz", - "integrity": "sha512-Htf/tPa5haZvRMiNSQSFifK12gtr/8vwfr+A9y69uF0QcU77AVu4K7MiHEkTxF7lQoHOL0F9ErqgfNEAKgXj7A==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-react-jsx": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.3.0.tgz", - "integrity": "sha512-a/+aRb7R06WcKvQLOu4/TpjKOdvVEKRLWFpKcNuHhiREPgGRB4TQJxq07+EZLS8LFVYpfq1a5lDUnuMdcCpBKg==", - "requires": { - "@babel/helper-builder-react-jsx": "^7.3.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@babel/plugin-transform-react-jsx-self": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.2.0.tgz", - "integrity": "sha512-v6S5L/myicZEy+jr6ielB0OR8h+EH/1QFx/YJ7c7Ua+7lqsjj/vW6fD5FR9hB/6y7mGbfT4vAURn3xqBxsUcdg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@babel/plugin-transform-react-jsx-source": { - "version": "7.5.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.5.0.tgz", - "integrity": "sha512-58Q+Jsy4IDCZx7kqEZuSDdam/1oW8OdDX8f+Loo6xyxdfg1yF0GE2XNJQSTZCaMol93+FBzpWiPEwtbMloAcPg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.4.5.tgz", - "integrity": "sha512-gBKRh5qAaCWntnd09S8QC7r3auLCqq5DI6O0DlfoyDjslSBVqBibrMdsqO+Uhmx3+BlOmE/Kw1HFxmGbv0N9dA==", - "requires": { - "regenerator-transform": "^0.14.0" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.2.0.tgz", - "integrity": "sha512-fz43fqW8E1tAB3DKF19/vxbpib1fuyCwSPE418ge5ZxILnBhWyhtPgz8eh1RCGGJlwvksHkyxMxh0eenFi+kFw==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.2.0.tgz", - "integrity": "sha512-QP4eUM83ha9zmYtpbnyjTLAGKQritA5XW/iG9cjtuOI8s1RuL/3V6a3DeSHfKutJQ+ayUfeZJPcnCYEQzaPQqg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.2.2.tgz", - "integrity": "sha512-KWfky/58vubwtS0hLqEnrWJjsMGaOeSBn90Ezn5Jeg9Z8KKHmELbP1yGylMlm5N6TPKeY9A2+UaSYLdxahg01w==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.2.0.tgz", - "integrity": "sha512-KKYCoGaRAf+ckH8gEL3JHUaFVyNHKe3ASNsZ+AlktgHevvxGigoIttrEJb8iKN03Q7Eazlv1s6cx2B2cQ3Jabw==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.0.0" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.4.4.tgz", - "integrity": "sha512-mQrEC4TWkhLN0z8ygIvEL9ZEToPhG5K7KDW3pzGqOfIGZ28Jb0POUkeWcoz8HnHvhFy6dwAT1j8OzqN8s804+g==", - "requires": { - "@babel/helper-annotate-as-pure": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.2.0.tgz", - "integrity": "sha512-2LNhETWYxiYysBtrBTqL8+La0jIoQQnIScUJc74OYvUGRmkskNY4EzLCnjHBzdmb38wqtTaixpo1NctEcvMDZw==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0" - } - }, - "@babel/plugin-transform-typescript": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.5.5.tgz", - "integrity": "sha512-pehKf4m640myZu5B2ZviLaiBlxMCjSZ1qTEO459AXKX5GnPueyulJeCqZFs1nz/Ya2dDzXQ1NxZ/kKNWyD4h6w==", - "requires": { - "@babel/helper-create-class-features-plugin": "^7.5.5", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-syntax-typescript": "^7.2.0" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.4.4.tgz", - "integrity": "sha512-il+/XdNw01i93+M9J9u4T7/e/Ue/vWfNZE4IRUQjplu2Mqb/AFTDimkw2tdEdSH50wuQXZAbXSql0UphQke+vA==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/helper-regex": "^7.4.4", - "regexpu-core": "^4.5.4" - } - }, - "@babel/preset-env": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.5.5.tgz", - "integrity": "sha512-GMZQka/+INwsMz1A5UEql8tG015h5j/qjptpKY2gJ7giy8ohzU710YciJB5rcKsWGWHiW3RUnHib0E5/m3Tp3A==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-async-generator-functions": "^7.2.0", - "@babel/plugin-proposal-dynamic-import": "^7.5.0", - "@babel/plugin-proposal-json-strings": "^7.2.0", - "@babel/plugin-proposal-object-rest-spread": "^7.5.5", - "@babel/plugin-proposal-optional-catch-binding": "^7.2.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-syntax-async-generators": "^7.2.0", - "@babel/plugin-syntax-dynamic-import": "^7.2.0", - "@babel/plugin-syntax-json-strings": "^7.2.0", - "@babel/plugin-syntax-object-rest-spread": "^7.2.0", - "@babel/plugin-syntax-optional-catch-binding": "^7.2.0", - "@babel/plugin-transform-arrow-functions": "^7.2.0", - "@babel/plugin-transform-async-to-generator": "^7.5.0", - "@babel/plugin-transform-block-scoped-functions": "^7.2.0", - "@babel/plugin-transform-block-scoping": "^7.5.5", - "@babel/plugin-transform-classes": "^7.5.5", - "@babel/plugin-transform-computed-properties": "^7.2.0", - "@babel/plugin-transform-destructuring": "^7.5.0", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/plugin-transform-duplicate-keys": "^7.5.0", - "@babel/plugin-transform-exponentiation-operator": "^7.2.0", - "@babel/plugin-transform-for-of": "^7.4.4", - "@babel/plugin-transform-function-name": "^7.4.4", - "@babel/plugin-transform-literals": "^7.2.0", - "@babel/plugin-transform-member-expression-literals": "^7.2.0", - "@babel/plugin-transform-modules-amd": "^7.5.0", - "@babel/plugin-transform-modules-commonjs": "^7.5.0", - "@babel/plugin-transform-modules-systemjs": "^7.5.0", - "@babel/plugin-transform-modules-umd": "^7.2.0", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.4.5", - "@babel/plugin-transform-new-target": "^7.4.4", - "@babel/plugin-transform-object-super": "^7.5.5", - "@babel/plugin-transform-parameters": "^7.4.4", - "@babel/plugin-transform-property-literals": "^7.2.0", - "@babel/plugin-transform-regenerator": "^7.4.5", - "@babel/plugin-transform-reserved-words": "^7.2.0", - "@babel/plugin-transform-shorthand-properties": "^7.2.0", - "@babel/plugin-transform-spread": "^7.2.0", - "@babel/plugin-transform-sticky-regex": "^7.2.0", - "@babel/plugin-transform-template-literals": "^7.4.4", - "@babel/plugin-transform-typeof-symbol": "^7.2.0", - "@babel/plugin-transform-unicode-regex": "^7.4.4", - "@babel/types": "^7.5.5", - "browserslist": "^4.6.0", - "core-js-compat": "^3.1.1", - "invariant": "^2.2.2", - "js-levenshtein": "^1.1.3", - "semver": "^5.5.0" - } - }, - "@babel/preset-react": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.0.0.tgz", - "integrity": "sha512-oayxyPS4Zj+hF6Et11BwuBkmpgT/zMxyuZgFrMeZID6Hdh3dGlk4sHCAhdBCpuCKW2ppBfl2uCCetlrUIJRY3w==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-transform-react-display-name": "^7.0.0", - "@babel/plugin-transform-react-jsx": "^7.0.0", - "@babel/plugin-transform-react-jsx-self": "^7.0.0", - "@babel/plugin-transform-react-jsx-source": "^7.0.0" - } - }, - "@babel/preset-typescript": { - "version": "7.3.3", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.3.3.tgz", - "integrity": "sha512-mzMVuIP4lqtn4du2ynEfdO0+RYcslwrZiJHXu4MGaC1ctJiW2fyaeDrtjJGs7R/KebZ1sgowcIoWf4uRpEfKEg==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-transform-typescript": "^7.3.2" - } - }, - "@babel/template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.4.4.tgz", - "integrity": "sha512-CiGzLN9KgAvgZsnivND7rkA+AeJ9JB0ciPOD4U59GKbQP2iQl+olF1l76kJOupqidozfZ32ghwBEJDhnk9MEcw==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.4.4", - "@babel/types": "^7.4.4" - } - }, - "@babel/traverse": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.5.5.tgz", - "integrity": "sha512-MqB0782whsfffYfSjH4TM+LMjrJnhCNEDMDIjeTpl+ASaUvxcjoiVCo/sM1GhS1pHOXYfWVCYneLjMckuUxDaQ==", - "requires": { - "@babel/code-frame": "^7.5.5", - "@babel/generator": "^7.5.5", - "@babel/helper-function-name": "^7.1.0", - "@babel/helper-split-export-declaration": "^7.4.4", - "@babel/parser": "^7.5.5", - "@babel/types": "^7.5.5", - "debug": "^4.1.0", - "globals": "^11.1.0", - "lodash": "^4.17.13" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@babel/types": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.5.5.tgz", - "integrity": "sha512-s63F9nJioLqOlW3UkyMd+BYhXt44YuaFm/VV0VwuteqjYwRrObkU7ra9pY4wAJR3oXi8hJrMcrcJdO/HH33vtw==", - "requires": { - "esutils": "^2.0.2", - "lodash": "^4.17.13", - "to-fast-properties": "^2.0.0" - } - }, - "@cnakazawa/watch": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.3.tgz", - "integrity": "sha512-r5160ogAvGyHsal38Kux7YYtodEKOj89RGb28ht1jh3SJb08VwRwAKKJL0bGb04Zd/3r9FL3BFIc3bBidYffCA==", - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "@jest/console": { - "version": "24.7.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-24.7.1.tgz", - "integrity": "sha512-iNhtIy2M8bXlAOULWVTUxmnelTLFneTNEkHCgPmgd+zNwy9zVddJ6oS5rZ9iwoscNdT5mMwUd0C51v/fSlzItg==", - "requires": { - "@jest/source-map": "^24.3.0", - "chalk": "^2.0.1", - "slash": "^2.0.0" - } - }, - "@jest/core": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-24.8.0.tgz", - "integrity": "sha512-R9rhAJwCBQzaRnrRgAdVfnglUuATXdwTRsYqs6NMdVcAl5euG8LtWDe+fVkN27YfKVBW61IojVsXKaOmSnqd/A==", - "requires": { - "@jest/console": "^24.7.1", - "@jest/reporters": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "graceful-fs": "^4.1.15", - "jest-changed-files": "^24.8.0", - "jest-config": "^24.8.0", - "jest-haste-map": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-resolve-dependencies": "^24.8.0", - "jest-runner": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-snapshot": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", - "jest-watcher": "^24.8.0", - "micromatch": "^3.1.10", - "p-each-series": "^1.0.0", - "pirates": "^4.0.1", - "realpath-native": "^1.1.0", - "rimraf": "^2.5.4", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "@jest/environment": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-24.8.0.tgz", - "integrity": "sha512-vlGt2HLg7qM+vtBrSkjDxk9K0YtRBi7HfRFaDxoRtyi+DyVChzhF20duvpdAnKVBV6W5tym8jm0U9EfXbDk1tw==", - "requires": { - "@jest/fake-timers": "^24.8.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "jest-mock": "^24.8.0" - } - }, - "@jest/fake-timers": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-24.8.0.tgz", - "integrity": "sha512-2M4d5MufVXwi6VzZhJ9f5S/wU4ud2ck0kxPof1Iz3zWx6Y+V2eJrES9jEktB6O3o/oEyk+il/uNu9PvASjWXQw==", - "requires": { - "@jest/types": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-mock": "^24.8.0" - } - }, - "@jest/reporters": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-24.8.0.tgz", - "integrity": "sha512-eZ9TyUYpyIIXfYCrw0UHUWUvE35vx5I92HGMgS93Pv7du+GHIzl+/vh8Qj9MCWFK/4TqyttVBPakWMOfZRIfxw==", - "requires": { - "@jest/environment": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "glob": "^7.1.2", - "istanbul-lib-coverage": "^2.0.2", - "istanbul-lib-instrument": "^3.0.1", - "istanbul-lib-report": "^2.0.4", - "istanbul-lib-source-maps": "^3.0.1", - "istanbul-reports": "^2.1.1", - "jest-haste-map": "^24.8.0", - "jest-resolve": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-util": "^24.8.0", - "jest-worker": "^24.6.0", - "node-notifier": "^5.2.1", - "slash": "^2.0.0", - "source-map": "^0.6.0", - "string-length": "^2.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "@jest/source-map": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-24.3.0.tgz", - "integrity": "sha512-zALZt1t2ou8le/crCeeiRYzvdnTzaIlpOWaet45lNSqNJUnXbppUUFR4ZUAlzgDmKee4Q5P/tKXypI1RiHwgag==", - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.1.15", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "@jest/test-result": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-24.8.0.tgz", - "integrity": "sha512-+YdLlxwizlfqkFDh7Mc7ONPQAhA4YylU1s529vVM1rsf67vGZH/2GGm5uO8QzPeVyaVMobCQ7FTxl38QrKRlng==", - "requires": { - "@jest/console": "^24.7.1", - "@jest/types": "^24.8.0", - "@types/istanbul-lib-coverage": "^2.0.0" - } - }, - "@jest/test-sequencer": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-24.8.0.tgz", - "integrity": "sha512-OzL/2yHyPdCHXEzhoBuq37CE99nkme15eHkAzXRVqthreWZamEMA0WoetwstsQBCXABhczpK03JNbc4L01vvLg==", - "requires": { - "@jest/test-result": "^24.8.0", - "jest-haste-map": "^24.8.0", - "jest-runner": "^24.8.0", - "jest-runtime": "^24.8.0" - } - }, - "@jest/transform": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-24.8.0.tgz", - "integrity": "sha512-xBMfFUP7TortCs0O+Xtez2W7Zu1PLH9bvJgtraN1CDST6LBM/eTOZ9SfwS/lvV8yOfcDpFmwf9bq5cYbXvqsvA==", - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^24.8.0", - "babel-plugin-istanbul": "^5.1.0", - "chalk": "^2.0.1", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.1.15", - "jest-haste-map": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-util": "^24.8.0", - "micromatch": "^3.1.10", - "realpath-native": "^1.1.0", - "slash": "^2.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "2.4.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "@jest/types": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-24.8.0.tgz", - "integrity": "sha512-g17UxVr2YfBtaMUxn9u/4+siG1ptg9IGYAYwvpwn61nBg779RXnjE/m7CxYcIzEt0AbHZZAHSEZNhkE2WxURVg==", - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", - "@types/yargs": "^12.0.9" - } - }, - "@serialport/binding-abstract": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@serialport/binding-abstract/-/binding-abstract-2.0.5.tgz", - "integrity": "sha512-oRg0QRsXJFKHQbQjmo0regKLZ9JhjLmTqc47ocJgYM5UtU9Q1VFrVPh0B2lr2pfm/tr3aNvTLX1eiVAvXyZ/bg==", - "requires": { - "debug": "^4.1.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@serialport/binding-mock": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@serialport/binding-mock/-/binding-mock-2.0.5.tgz", - "integrity": "sha512-1kD1qI686pIIolGZ6TPjAtvy8c3XIUlE4OXRZf7ZHaZgGaOUHAUMLKZt4tNTxsfedRTFyiYyHoe5QAbx82R9pQ==", - "requires": { - "@serialport/binding-abstract": "^2.0.5", - "debug": "^4.1.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@serialport/bindings": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@serialport/bindings/-/bindings-2.0.8.tgz", - "integrity": "sha512-paKLa9JkoH5FAy2sATTdXLCiKpuKn0pN15/etcCqzX8vi25fnQgJ8Yx9Z6zdbcKe1No7s/9PuH9yfjDR61fbOQ==", - "requires": { - "@serialport/binding-abstract": "^2.0.5", - "@serialport/parser-readline": "^2.0.2", - "bindings": "^1.3.0", - "debug": "^4.1.1", - "nan": "^2.13.2", - "prebuild-install": "^5.2.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@serialport/parser-byte-length": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@serialport/parser-byte-length/-/parser-byte-length-2.0.2.tgz", - "integrity": "sha512-cUOprk1uRLucCJy6m+wAM4pwdBaB5D4ySi6juwRScP9DTjKUvGWYj5jzuqvftFBvYFmFza89aLj5K23xiiqj7Q==" - }, - "@serialport/parser-cctalk": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@serialport/parser-cctalk/-/parser-cctalk-2.0.2.tgz", - "integrity": "sha512-5LMysRv7De+TeeoKzi4+sgouD4tqZEAn1agAVevw+7ILM0m30i1zgZLtchgxtCH7OoQRAkENEVEPc0OwhghKgw==" - }, - "@serialport/parser-delimiter": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@serialport/parser-delimiter/-/parser-delimiter-2.0.2.tgz", - "integrity": "sha512-zB02LahFfyZmJqak9l37vP/F1K+KCUxd1KQj35OhD1+0q/unMjVTZmsfkxFSM4gkaxP9j7+8USk+LQJ3V8U26Q==" - }, - "@serialport/parser-readline": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@serialport/parser-readline/-/parser-readline-2.0.2.tgz", - "integrity": "sha512-thL26dGEHB+eINNydJmzcLLhiqcBQkF+wNTbRaYblTP/6dm7JsfjYSud7bTkN63AgE0xpe9tKXBFqc8zgJ1VKg==", - "requires": { - "@serialport/parser-delimiter": "^2.0.2" - } - }, - "@serialport/parser-ready": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@serialport/parser-ready/-/parser-ready-2.0.2.tgz", - "integrity": "sha512-6ynQ+HIIkFQcEO2Hrq4Qmdz+hlJ7kjTHGQ1E7SRN7f70nnys1v3HSke8mjK3RzVw+SwL0rBYjftUdCTrU+7c+Q==" - }, - "@serialport/parser-regex": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@serialport/parser-regex/-/parser-regex-2.0.2.tgz", - "integrity": "sha512-7qjYd7AdHUK8fJOmHpXlMRipqRCVMMyDFyf/5TQQiOt6q+BiFjLOtSpVXhakHwgnXanzDYKeRSB8zM0pZZg+LA==" - }, - "@serialport/stream": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@serialport/stream/-/stream-2.0.5.tgz", - "integrity": "sha512-9gc3zPoAqs/04mvq8TdZ7GxtnacCDuw3/u0u18UXXHgC/5tNDYkY+hXFIJB1fQFnP5yyNB1L2XLfX974ySJg9Q==", - "requires": { - "@serialport/binding-mock": "^2.0.5", - "debug": "^4.1.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "@types/babel__core": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.2.tgz", - "integrity": "sha512-cfCCrFmiGY/yq0NuKNxIQvZFy9kY/1immpSpTngOnyIbD4+eJOG5mxphhHDv3CHL9GltO4GcKr54kGBg3RNdbg==", - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "@types/babel__generator": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.0.2.tgz", - "integrity": "sha512-NHcOfab3Zw4q5sEE2COkpfXjoE7o+PmqD9DQW4koUT3roNxwziUdXGnRndMat/LJNUtePwn1TlP4do3uoe3KZQ==", - "requires": { - "@babel/types": "^7.0.0" - } - }, - "@types/babel__template": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", - "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "@types/babel__traverse": { - "version": "7.0.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.7.tgz", - "integrity": "sha512-CeBpmX1J8kWLcDEnI3Cl2Eo6RfbGvzUctA+CjZUhOKDFbLfcr7fc4usEqLNWetrlJd7RhAkyYe2czXop4fICpw==", - "requires": { - "@babel/types": "^7.3.0" - } - }, - "@types/eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==" - }, - "@types/istanbul-lib-coverage": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", - "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==" - }, - "@types/istanbul-lib-report": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-1.1.1.tgz", - "integrity": "sha512-3BUTyMzbZa2DtDI2BkERNC6jJw2Mr2Y0oGI7mRxYNBPxppbtEK1F66u3bKwU2g+wxwWI7PAoRpJnOY1grJqzHg==", - "requires": { - "@types/istanbul-lib-coverage": "*" - } - }, - "@types/istanbul-reports": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", - "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", - "requires": { - "@types/istanbul-lib-coverage": "*", - "@types/istanbul-lib-report": "*" - } - }, - "@types/jest": { - "version": "24.0.15", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.15.tgz", - "integrity": "sha512-MU1HIvWUme74stAoc3mgAi+aMlgKOudgEvQDIm1v4RkrDudBh1T+NFp5sftpBAdXdx1J0PbdpJ+M2EsSOi1djA==", - "requires": { - "@types/jest-diff": "*" - } - }, - "@types/jest-diff": { - "version": "20.0.1", - "resolved": "https://registry.npmjs.org/@types/jest-diff/-/jest-diff-20.0.1.tgz", - "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==" - }, - "@types/node": { - "version": "12.6.8", - "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.8.tgz", - "integrity": "sha512-aX+gFgA5GHcDi89KG5keey2zf0WfZk/HAQotEamsK2kbey+8yGKcson0hbK8E+v0NArlCJQCqMP161YhV6ZXLg==" - }, - "@types/serialport": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/@types/serialport/-/serialport-7.0.4.tgz", - "integrity": "sha512-EdLAkoUQq0A0UrbGDiOMVQP+vmDncKUw4liXfR4T26KHOJEGJD5WIBtfb59JQNgeecC0aXMAg5il3gDCuHPNsg==", - "requires": { - "@types/node": "*" - } - }, - "@types/stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" - }, - "@types/yargs": { - "version": "12.0.12", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-12.0.12.tgz", - "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==" - }, - "@typescript-eslint/eslint-plugin": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.12.0.tgz", - "integrity": "sha512-J/ZTZF+pLNqjXBGNfq5fahsoJ4vJOkYbitWPavA05IrZ7BXUaf4XWlhUB/ic1lpOGTRpLWF+PLAePjiHp6dz8g==", - "requires": { - "@typescript-eslint/experimental-utils": "1.12.0", - "eslint-utils": "^1.3.1", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^2.0.1", - "tsutils": "^3.7.0" - } - }, - "@typescript-eslint/experimental-utils": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.12.0.tgz", - "integrity": "sha512-s0soOTMJloytr9GbPteMLNiO2HvJ+qgQkRNplABXiVw6vq7uQRvidkby64Gqt/nA7pys74HksHwRULaB/QRVyw==", - "requires": { - "@typescript-eslint/typescript-estree": "1.12.0", - "eslint-scope": "^4.0.0" - } - }, - "@typescript-eslint/parser": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-1.12.0.tgz", - "integrity": "sha512-0uzbaa9ZLCA5yMWJywnJJ7YVENKGWVUhJDV5UrMoldC5HoI54W5kkdPhTfmtFKpPFp93MIwmJj0/61ztvmz5Dw==", - "requires": { - "@types/eslint-visitor-keys": "^1.0.0", - "@typescript-eslint/experimental-utils": "1.12.0", - "@typescript-eslint/typescript-estree": "1.12.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "@typescript-eslint/typescript-estree": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.12.0.tgz", - "integrity": "sha512-nwN6yy//XcVhFs0ZyU+teJHB8tbCm7AIA8mu6E2r5hu6MajwYBY3Uwop7+rPZWUN/IUOHpL8C+iUPMDVYUU3og==", - "requires": { - "lodash.unescape": "4.0.1", - "semver": "5.5.0" - }, - "dependencies": { - "semver": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", - "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==" - } - } - }, - "abab": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.0.tgz", - "integrity": "sha512-sY5AXXVZv4Y1VACTtR11UJCPHHudgY5i26Qj5TypE6DKlIApbwb5uqhXcJ5UUGbvZNRh7EeIoW+LrJumBsKp7w==" - }, - "acorn": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.2.1.tgz", - "integrity": "sha512-JD0xT5FCRDNyjDda3Lrg/IxFscp9q4tiYtxE1/nOzlKCk7hIRuYjhq1kCNkbPjMRMZuFq20HNQn1I9k8Oj0E+Q==" - }, - "acorn-globals": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.2.tgz", - "integrity": "sha512-BbzvZhVtZP+Bs1J1HcwrQe8ycfO0wStkSGxuul3He3GkHOIZ6eTqOkPuw9IP1X3+IkOo4wiJmwkobzXYz4wewQ==", - "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - } - }, - "acorn-jsx": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.0.1.tgz", - "integrity": "sha512-HJ7CfNHrfJLlNTzIEUTj43LNWGkqpRLxm3YjAlcD0ACydk9XynzYsCBHxut+iqt+1aBXkx9UP/w/ZqMr13XIzg==" - }, - "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==" - }, - "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", - "requires": { - "fast-deep-equal": "^2.0.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==" - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" - }, - "are-we-there-yet": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", - "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=" - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==" - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=" - }, - "array-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=" - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=" - }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" - }, - "astral-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", - "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==" - }, - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - }, - "async-limiter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.0.tgz", - "integrity": "sha512-jp/uFnooOiO+L211eZOoSyzpOITMXx1rBITauYykG3BRYPu8h0UcxsPNB04RR5vo4Tyz3+ay17tR6JVf9qzYWg==" - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" - }, - "aws4": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz", - "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==" - }, - "babel-jest": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.8.0.tgz", - "integrity": "sha512-+5/kaZt4I9efoXzPlZASyK/lN9qdRKmmUav9smVc0ruPQD7IsfucQ87gpOE8mn2jbDuS6M/YOW6n3v9ZoIfgnw==", - "requires": { - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "@types/babel__core": "^7.1.0", - "babel-plugin-istanbul": "^5.1.0", - "babel-preset-jest": "^24.6.0", - "chalk": "^2.4.2", - "slash": "^2.0.0" - } - }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.0.tgz", - "integrity": "sha512-o6qFkpeQEBxcqt0XYlWzAVxNCSCZdUgcR8IRlhD/8DylxjjO4foPcvTW0GGKa/cVt3rvxZ7o5ippJ+/0nvLhlQ==", - "requires": { - "object.assign": "^4.1.0" - } - }, - "babel-plugin-istanbul": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-5.2.0.tgz", - "integrity": "sha512-5LphC0USA8t4i1zCtjbbNb6jJj/9+X6P37Qfirc/70EQ34xKlMW+a1RHGwxGI+SwWpNwZ27HqvzAobeqaXwiZw==", - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "find-up": "^3.0.0", - "istanbul-lib-instrument": "^3.3.0", - "test-exclude": "^5.2.3" - } - }, - "babel-plugin-jest-hoist": { - "version": "24.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-24.6.0.tgz", - "integrity": "sha512-3pKNH6hMt9SbOv0F3WVmy5CWQ4uogS3k0GY5XLyQHJ9EGpAT9XWkFd2ZiXXtkwFHdAHa5j7w7kfxSP5lAIwu7w==", - "requires": { - "@types/babel__traverse": "^7.0.6" - } - }, - "babel-preset-jest": { - "version": "24.6.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-24.6.0.tgz", - "integrity": "sha512-pdZqLEdmy1ZK5kyRUfvBb2IfTPb2BUvIJczlPspS8fWmBQslNNDBqVfh7BW5leOVJMDZKzjD8XEyABTk6gQ5yw==", - "requires": { - "@babel/plugin-syntax-object-rest-spread": "^7.0.0", - "babel-plugin-jest-hoist": "^24.6.0" - } - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "binary-search-tree": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/binary-search-tree/-/binary-search-tree-0.2.5.tgz", - "integrity": "sha1-fbs7IQ/coIJFDa0jNMMErzm9x4Q=", - "requires": { - "underscore": "~1.4.4" - } - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bl": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-0.7.0.tgz", - "integrity": "sha1-P7BnBgKsKHjrdw3CA58YNr5irls=", - "requires": { - "readable-stream": "~1.0.2" - }, - "dependencies": { - "readable-stream": { - "version": "1.0.34", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.0.34.tgz", - "integrity": "sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "browser-process-hrtime": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-0.1.3.tgz", - "integrity": "sha512-bRFnI4NnjO6cnyLmOV/7PVoDEMJChlcfN0z4s1YMBY989/SvlfMI1lgCnkFUs53e9gQF+w7qu7XdllSTiSl8Aw==" - }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" - } - } - }, - "browserslist": { - "version": "4.6.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.6.6.tgz", - "integrity": "sha512-D2Nk3W9JL9Fp/gIcWei8LrERCS+eXu9AM5cfXA8WEZ84lFks+ARnZ0q/R69m2SV3Wjma83QDDPxsNKXUwdIsyA==", - "requires": { - "caniuse-lite": "^1.0.30000984", - "electron-to-chromium": "^1.3.191", - "node-releases": "^1.1.25" - } - }, - "bser": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.0.tgz", - "integrity": "sha512-8zsjWrQkkBoLK6uxASk1nJ2SKv97ltiGDo6A3wA0/yRPz+CwmEyDo0hUrhIuukG2JHpAl3bvFIixw2/3Hi0DOg==", - "requires": { - "node-int64": "^0.4.0" - } - }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" - } - }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==" - }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=" - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" - }, - "busyman": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/busyman/-/busyman-0.3.0.tgz", - "integrity": "sha1-FN6kn2JgfOSy0wsgPGMG72zKiKc=" - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" - }, - "caniuse-lite": { - "version": "1.0.30000985", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30000985.tgz", - "integrity": "sha512-1ngiwkgqAYPG0JSSUp3PUDGPKKY59EK7NrGGX+VOxaKCNzRbNc7uXMny+c3VJfZxtoK3wSImTvG9T9sXiTw2+w==" - }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", - "requires": { - "rsvp": "^4.8.4" - } - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" - }, - "chownr": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.2.tgz", - "integrity": "sha512-GkfeAQh+QNy3wquu9oIZr6SS5x7wGdSgNQvD10X3r+AZr1Oys22HW8kAmDMvNg2+Dm0TeGaEuO8gFwdBXxwO8A==" - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", - "requires": { - "restore-cursor": "^2.0.0" - } - }, - "cli-width": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", - "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=" - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" - }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", - "optional": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" - }, - "concentrate": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/concentrate/-/concentrate-0.2.3.tgz", - "integrity": "sha1-+XoCBlIcLXxFFxtkz8TW+LYwJUI=" - }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" - }, - "convert-source-map": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", - "integrity": "sha512-eFu7XigvxdZ1ETfbgPBohgyQ/Z++C0eEhTor0qRwBw9unw+L0/6V8wkSuGgzdThkiS5lSpdptOQPD8Ak40a+7A==", - "requires": { - "safe-buffer": "~5.1.1" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=" - }, - "core-js-compat": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.1.4.tgz", - "integrity": "sha512-Z5zbO9f1d0YrJdoaQhphVAnKPimX92D6z8lCGphH89MNRxlL1prI9ExJPqVwP0/kgkQCv8c4GJGT8X16yUncOg==", - "requires": { - "browserslist": "^4.6.2", - "core-js-pure": "3.1.4", - "semver": "^6.1.1" - }, - "dependencies": { - "semver": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", - "integrity": "sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==" - } - } - }, - "core-js-pure": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.1.4.tgz", - "integrity": "sha512-uJ4Z7iPNwiu1foygbcZYJsJs1jiXrTTCvxfLDXNhI/I+NHbSIEyr548y4fcsCEyWY0XgfAG/qqaunJ1SThHenA==" - }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==" - }, - "cssstyle": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.4.0.tgz", - "integrity": "sha512-GBrLZYZ4X4x6/QEoBnIrqb8B/f5l4+8me2dkom/j1Gtbxy0kBv6OGzKuAsGM75bkGwGAFkt56Iwg28S3XTZgSA==", - "requires": { - "cssom": "0.3.x" - } - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", - "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" - }, - "dependencies": { - "whatwg-url": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.0.0.tgz", - "integrity": "sha512-37GeVSIJ3kn1JgKyjiYNmSLP1yzbpb29jdmwBSgkD9h40/hyrR/OifpVUndji3tmwGgD8qpw7iQu3RSbCrBpsQ==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - } - } - }, - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", - "requires": { - "mimic-response": "^1.0.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" - }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" - }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" - }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" - }, - "detect-newline": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-2.1.0.tgz", - "integrity": "sha1-9B8cEL5LAOh7XxPaaAdZ8sW/0+I=" - }, - "diff-sequences": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-24.3.0.tgz", - "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==" - }, - "dissolve": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/dissolve/-/dissolve-0.3.3.tgz", - "integrity": "sha1-uX7x/ymJx4nOz7AxB+F0EfqL5uU=", - "requires": { - "bl": "^0.7.0", - "readable-stream": "^1.0.26" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "requires": { - "esutils": "^2.0.2" - } - }, - "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", - "requires": { - "webidl-conversions": "^4.0.2" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "electron-to-chromium": { - "version": "1.3.197", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.197.tgz", - "integrity": "sha512-SfD7WfmueKrtKeHUESLczuANgnpdnfrSz3ZzerLdtmZf2UBZmAB3z9Q525zI5p3n9I7ii/lllUlyKHm2pIG7QQ==" - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "end-of-stream": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", - "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", - "requires": { - "once": "^1.4.0" - } - }, - "enum": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/enum/-/enum-2.5.0.tgz", - "integrity": "sha1-8gW4xlozWorOgIEQXflxsYVouYQ=", - "requires": { - "is-buffer": "^1.1.0" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.13.0.tgz", - "integrity": "sha512-vDZfg/ykNxQVwup/8E1BZhVzFfBxs9NqMzGcvIJrqg5k2/5Za2bWo40dK2J1pgLngZ7c+Shh8lwYtLGyrwPutg==", - "requires": { - "es-to-primitive": "^1.2.0", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "is-callable": "^1.1.4", - "is-regex": "^1.0.4", - "object-keys": "^1.0.12" - } - }, - "es-to-primitive": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.0.tgz", - "integrity": "sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", - "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", - "requires": { - "esprima": "^3.1.3", - "estraverse": "^4.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, - "dependencies": { - "esprima": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-3.1.3.tgz", - "integrity": "sha1-/cpRzuYTOJXjyI1TXOSdv/YqRjM=" - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - } - } - }, - "eslint": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.1.0.tgz", - "integrity": "sha512-QhrbdRD7ofuV09IuE2ySWBz0FyXCq0rriLTZXZqaWSI79CVtHVRdkFuFTViiqzZhkCgfOh9USpriuGN2gIpZDQ==", - "requires": { - "@babel/code-frame": "^7.0.0", - "ajv": "^6.10.0", - "chalk": "^2.1.0", - "cross-spawn": "^6.0.5", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "eslint-scope": "^5.0.0", - "eslint-utils": "^1.3.1", - "eslint-visitor-keys": "^1.0.0", - "espree": "^6.0.0", - "esquery": "^1.0.1", - "esutils": "^2.0.2", - "file-entry-cache": "^5.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.0.0", - "globals": "^11.7.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "inquirer": "^6.4.1", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.3.0", - "lodash": "^4.17.14", - "minimatch": "^3.0.4", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "optionator": "^0.8.2", - "progress": "^2.0.0", - "regexpp": "^2.0.1", - "semver": "^6.1.2", - "strip-ansi": "^5.2.0", - "strip-json-comments": "^3.0.1", - "table": "^5.2.3", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "eslint-scope": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", - "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "semver": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", - "integrity": "sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==" - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - }, - "strip-json-comments": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", - "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==" - } - } - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.0.tgz", - "integrity": "sha512-7ehnzPaP5IIEh1r1tkjuIrxqhNkzUJa9z3R92tLJdZIVdWaczEhr3EbhGtsMrVxi1KeR8qA7Off6SWc5WNQqyQ==", - "requires": { - "eslint-visitor-keys": "^1.0.0" - } - }, - "eslint-visitor-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", - "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==" - }, - "espree": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-6.0.0.tgz", - "integrity": "sha512-lJvCS6YbCn3ImT3yKkPe0+tJ+mH6ljhGNjHQH9mRtiO6gjhVAOhVXW1yjnwqGwTkK3bGbye+hb00nFNmu0l/1Q==", - "requires": { - "acorn": "^6.0.7", - "acorn-jsx": "^5.0.0", - "eslint-visitor-keys": "^1.0.0" - } - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" - }, - "esquery": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", - "requires": { - "estraverse": "^4.0.0" - } - }, - "esrecurse": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", - "requires": { - "estraverse": "^4.1.0" - } - }, - "estraverse": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", - "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=" - }, - "esutils": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" - }, - "exec-sh": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.2.tgz", - "integrity": "sha512-9sLAvzhI5nc8TpuQUh4ahMdCrWT00wPWz7j47/emR5+2qEfoZP5zzUXvx+vdx+H6ohhnsYC31iX04QLYJK8zTg==" - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=" - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "expand-template": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", - "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" - }, - "expect": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-24.8.0.tgz", - "integrity": "sha512-/zYvP8iMDrzaaxHVa724eJBCKqSHmO0FA7EDkBiRHxg6OipmMn1fN+C8T9L9K8yr7UONkOifu6+LLH+z76CnaA==", - "requires": { - "@jest/types": "^24.8.0", - "ansi-styles": "^3.2.0", - "jest-get-type": "^24.8.0", - "jest-matcher-utils": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-regex-util": "^24.3.0" - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "requires": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" - }, - "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" - }, - "fast-json-stable-stringify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" - }, - "fb-watchman": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.0.tgz", - "integrity": "sha1-VOmr99+i8mzZsWNsWIwa/AXeXVg=", - "requires": { - "bser": "^2.0.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-entry-cache": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", - "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", - "requires": { - "flat-cache": "^2.0.1" - } - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "flat-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", - "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", - "requires": { - "flatted": "^2.0.0", - "rimraf": "2.6.3", - "write": "1.0.3" - } - }, - "flatted": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", - "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==" - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=" - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", - "requires": { - "map-cache": "^0.2.2" - } - }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz", - "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==", - "optional": true, - "requires": { - "nan": "^2.12.1", - "node-pre-gyp": "^0.12.0" - }, - "dependencies": { - "abbrev": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "ansi-regex": { - "version": "2.1.1", - "bundled": true, - "optional": true - }, - "aproba": { - "version": "1.2.0", - "bundled": true, - "optional": true - }, - "are-we-there-yet": { - "version": "1.1.5", - "bundled": true, - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - } - }, - "balanced-match": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "brace-expansion": { - "version": "1.1.11", - "bundled": true, - "optional": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "chownr": { - "version": "1.1.1", - "bundled": true, - "optional": true - }, - "code-point-at": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "concat-map": { - "version": "0.0.1", - "bundled": true, - "optional": true - }, - "console-control-strings": { - "version": "1.1.0", - "bundled": true, - "optional": true - }, - "core-util-is": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "debug": { - "version": "4.1.1", - "bundled": true, - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "deep-extend": { - "version": "0.6.0", - "bundled": true, - "optional": true - }, - "delegates": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "detect-libc": { - "version": "1.0.3", - "bundled": true, - "optional": true - }, - "fs-minipass": { - "version": "1.2.5", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "fs.realpath": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "gauge": { - "version": "2.7.4", - "bundled": true, - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "glob": { - "version": "7.1.3", - "bundled": true, - "optional": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "has-unicode": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "iconv-lite": { - "version": "0.4.24", - "bundled": true, - "optional": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore-walk": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, - "inflight": { - "version": "1.0.6", - "bundled": true, - "optional": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.3", - "bundled": true, - "optional": true - }, - "ini": { - "version": "1.3.5", - "bundled": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "bundled": true, - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "isarray": { - "version": "1.0.0", - "bundled": true, - "optional": true - }, - "minimatch": { - "version": "3.0.4", - "bundled": true, - "optional": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "bundled": true, - "optional": true - }, - "minipass": { - "version": "2.3.5", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.2.1", - "bundled": true, - "optional": true, - "requires": { - "minipass": "^2.2.1" - } - }, - "mkdirp": { - "version": "0.5.1", - "bundled": true, - "optional": true, - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.1", - "bundled": true, - "optional": true - }, - "needle": { - "version": "2.3.0", - "bundled": true, - "optional": true, - "requires": { - "debug": "^4.1.0", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - } - }, - "node-pre-gyp": { - "version": "0.12.0", - "bundled": true, - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.1", - "bundled": true, - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, - "npm-bundled": { - "version": "1.0.6", - "bundled": true, - "optional": true - }, - "npm-packlist": { - "version": "1.4.1", - "bundled": true, - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "bundled": true, - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "object-assign": { - "version": "4.1.1", - "bundled": true, - "optional": true - }, - "once": { - "version": "1.4.0", - "bundled": true, - "optional": true, - "requires": { - "wrappy": "1" - } - }, - "os-homedir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "osenv": { - "version": "0.1.5", - "bundled": true, - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, - "path-is-absolute": { - "version": "1.0.1", - "bundled": true, - "optional": true - }, - "process-nextick-args": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "rc": { - "version": "1.2.8", - "bundled": true, - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "bundled": true, - "optional": true - } - } - }, - "readable-stream": { - "version": "2.3.6", - "bundled": true, - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "rimraf": { - "version": "2.6.3", - "bundled": true, - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, - "safe-buffer": { - "version": "5.1.2", - "bundled": true, - "optional": true - }, - "safer-buffer": { - "version": "2.1.2", - "bundled": true, - "optional": true - }, - "sax": { - "version": "1.2.4", - "bundled": true, - "optional": true - }, - "semver": { - "version": "5.7.0", - "bundled": true, - "optional": true - }, - "set-blocking": { - "version": "2.0.0", - "bundled": true, - "optional": true - }, - "signal-exit": { - "version": "3.0.2", - "bundled": true, - "optional": true - }, - "string-width": { - "version": "1.0.2", - "bundled": true, - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "1.1.1", - "bundled": true, - "optional": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "bundled": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "bundled": true, - "optional": true - }, - "tar": { - "version": "4.4.8", - "bundled": true, - "optional": true, - "requires": { - "chownr": "^1.1.1", - "fs-minipass": "^1.2.5", - "minipass": "^2.3.4", - "minizlib": "^1.1.1", - "mkdirp": "^0.5.0", - "safe-buffer": "^5.1.2", - "yallist": "^3.0.2" - } - }, - "util-deprecate": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "wide-align": { - "version": "1.1.3", - "bundled": true, - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wrappy": { - "version": "1.0.2", - "bundled": true, - "optional": true - }, - "yallist": { - "version": "3.0.3", - "bundled": true, - "optional": true - } - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=" - }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "requires": { - "pump": "^3.0.0" - }, - "dependencies": { - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=" - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", - "requires": { - "assert-plus": "^1.0.0" - } - }, - "github-from-package": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", - "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" - }, - "glob": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", - "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.0.0.tgz", - "integrity": "sha512-Z2RwiujPRGluePM6j699ktJYxmPpJKCfpGA13jz2hmFZC7gKetzrWvg5KN3+OsIFmydGyZ1AVwERCq1w/ZZwRg==", - "requires": { - "is-glob": "^4.0.1" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" - }, - "graceful-fs": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.0.tgz", - "integrity": "sha512-jpSvDPV4Cq/bgtpndIWbI5hmYxhQGHPC4d4cqBPb4DLniCfhJokdXhwhaDuLBGLQdvvRum/UiX6ECVIPvDXqdg==" - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=" - }, - "handlebars": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz", - "integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==", - "requires": { - "neo-async": "^2.6.0", - "optimist": "^0.6.1", - "source-map": "^0.6.1", - "uglify-js": "^3.1.4" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" - }, - "har-validator": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.3.tgz", - "integrity": "sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==", - "requires": { - "ajv": "^6.5.5", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.0.tgz", - "integrity": "sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q=" - }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "hosted-git-info": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz", - "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==" - }, - "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", - "requires": { - "whatwg-encoding": "^1.0.1" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" - }, - "immediate": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", - "integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps=" - }, - "import-fresh": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", - "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - } - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" - }, - "inquirer": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.5.0.tgz", - "integrity": "sha512-scfHejeG/lVZSpvCXpsB4j/wQNPM5JC8kiElOI0OUTwmc1RTpXr4H32/HOlQHcZiYl2z2VElwuCVDRG8vFmbnA==", - "requires": { - "ansi-escapes": "^3.2.0", - "chalk": "^2.4.2", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^3.0.3", - "figures": "^2.0.0", - "lodash": "^4.17.12", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^6.4.0", - "string-width": "^2.1.0", - "strip-ansi": "^5.1.0", - "through": "^2.3.6" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - } - } - } - } - }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "requires": { - "loose-envify": "^1.0.0" - } - }, - "invert-kv": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", - "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-callable": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", - "integrity": "sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA==" - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", - "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=" - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==" - } - } - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=" - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" - }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "requires": { - "number-is-nan": "^1.0.0" - } - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==" - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "requires": { - "isobject": "^3.0.1" - } - }, - "is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", - "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=" - }, - "is-regex": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", - "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", - "requires": { - "has": "^1.0.1" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" - }, - "is-symbol": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.2.tgz", - "integrity": "sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw==", - "requires": { - "has-symbols": "^1.0.0" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=" - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=" - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" - }, - "istanbul-lib-coverage": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.5.tgz", - "integrity": "sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==" - }, - "istanbul-lib-instrument": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-3.3.0.tgz", - "integrity": "sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==", - "requires": { - "@babel/generator": "^7.4.0", - "@babel/parser": "^7.4.3", - "@babel/template": "^7.4.0", - "@babel/traverse": "^7.4.3", - "@babel/types": "^7.4.0", - "istanbul-lib-coverage": "^2.0.5", - "semver": "^6.0.0" - }, - "dependencies": { - "semver": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.2.0.tgz", - "integrity": "sha512-jdFC1VdUGT/2Scgbimf7FSx9iJLXoqfglSF+gJeuNWVpiE37OIbc1jywR/GJyFdz3mnkz2/id0L0J/cr0izR5A==" - } - } - }, - "istanbul-lib-report": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-2.0.8.tgz", - "integrity": "sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==", - "requires": { - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "supports-color": "^6.1.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "istanbul-lib-source-maps": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-3.0.6.tgz", - "integrity": "sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==", - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^2.0.5", - "make-dir": "^2.1.0", - "rimraf": "^2.6.3", - "source-map": "^0.6.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "istanbul-reports": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-2.2.6.tgz", - "integrity": "sha512-SKi4rnMyLBKe0Jy2uUdx28h8oG7ph2PPuQPvIAh31d+Ci+lSiEu4C+h3oBPuJ9+mPKhOyW0M8gY4U5NM1WLeXA==", - "requires": { - "handlebars": "^4.1.2" - } - }, - "jest": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-24.8.0.tgz", - "integrity": "sha512-o0HM90RKFRNWmAWvlyV8i5jGZ97pFwkeVoGvPW1EtLTgJc2+jcuqcbbqcSZLE/3f2S5pt0y2ZBETuhpWNl1Reg==", - "requires": { - "import-local": "^2.0.0", - "jest-cli": "^24.8.0" - }, - "dependencies": { - "jest-cli": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-24.8.0.tgz", - "integrity": "sha512-+p6J00jSMPQ116ZLlHJJvdf8wbjNbZdeSX9ptfHX06/MSNaXmKihQzx5vQcw0q2G6JsdVkUIdWbOWtSnaYs3yA==", - "requires": { - "@jest/core": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "import-local": "^2.0.0", - "is-ci": "^2.0.0", - "jest-config": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", - "prompts": "^2.0.1", - "realpath-native": "^1.1.0", - "yargs": "^12.0.2" - } - } - } - }, - "jest-changed-files": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-24.8.0.tgz", - "integrity": "sha512-qgANC1Yrivsq+UrLXsvJefBKVoCsKB0Hv+mBb6NMjjZ90wwxCDmU3hsCXBya30cH+LnPYjwgcU65i6yJ5Nfuug==", - "requires": { - "@jest/types": "^24.8.0", - "execa": "^1.0.0", - "throat": "^4.0.0" - } - }, - "jest-config": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-24.8.0.tgz", - "integrity": "sha512-Czl3Nn2uEzVGsOeaewGWoDPD8GStxCpAe0zOYs2x2l0fZAgPbCr3uwUkgNKV3LwE13VXythM946cd5rdGkkBZw==", - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^24.8.0", - "@jest/types": "^24.8.0", - "babel-jest": "^24.8.0", - "chalk": "^2.0.1", - "glob": "^7.1.1", - "jest-environment-jsdom": "^24.8.0", - "jest-environment-node": "^24.8.0", - "jest-get-type": "^24.8.0", - "jest-jasmine2": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-resolve": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", - "micromatch": "^3.1.10", - "pretty-format": "^24.8.0", - "realpath-native": "^1.1.0" - } - }, - "jest-diff": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-24.8.0.tgz", - "integrity": "sha512-wxetCEl49zUpJ/bvUmIFjd/o52J+yWcoc5ZyPq4/W1LUKGEhRYDIbP1KcF6t+PvqNrGAFk4/JhtxDq/Nnzs66g==", - "requires": { - "chalk": "^2.0.1", - "diff-sequences": "^24.3.0", - "jest-get-type": "^24.8.0", - "pretty-format": "^24.8.0" - } - }, - "jest-docblock": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-24.3.0.tgz", - "integrity": "sha512-nlANmF9Yq1dufhFlKG9rasfQlrY7wINJbo3q01tu56Jv5eBU5jirylhF2O5ZBnLxzOVBGRDz/9NAwNyBtG4Nyg==", - "requires": { - "detect-newline": "^2.1.0" - } - }, - "jest-each": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-24.8.0.tgz", - "integrity": "sha512-NrwK9gaL5+XgrgoCsd9svsoWdVkK4gnvyhcpzd6m487tXHqIdYeykgq3MKI1u4I+5Zf0tofr70at9dWJDeb+BA==", - "requires": { - "@jest/types": "^24.8.0", - "chalk": "^2.0.1", - "jest-get-type": "^24.8.0", - "jest-util": "^24.8.0", - "pretty-format": "^24.8.0" - } - }, - "jest-environment-jsdom": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-24.8.0.tgz", - "integrity": "sha512-qbvgLmR7PpwjoFjM/sbuqHJt/NCkviuq9vus9NBn/76hhSidO+Z6Bn9tU8friecegbJL8gzZQEMZBQlFWDCwAQ==", - "requires": { - "@jest/environment": "^24.8.0", - "@jest/fake-timers": "^24.8.0", - "@jest/types": "^24.8.0", - "jest-mock": "^24.8.0", - "jest-util": "^24.8.0", - "jsdom": "^11.5.1" - } - }, - "jest-environment-node": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-24.8.0.tgz", - "integrity": "sha512-vIGUEScd1cdDgR6sqn2M08sJTRLQp6Dk/eIkCeO4PFHxZMOgy+uYLPMC4ix3PEfM5Au/x3uQ/5Tl0DpXXZsJ/Q==", - "requires": { - "@jest/environment": "^24.8.0", - "@jest/fake-timers": "^24.8.0", - "@jest/types": "^24.8.0", - "jest-mock": "^24.8.0", - "jest-util": "^24.8.0" - } - }, - "jest-get-type": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.8.0.tgz", - "integrity": "sha512-RR4fo8jEmMD9zSz2nLbs2j0zvPpk/KCEz3a62jJWbd2ayNo0cb+KFRxPHVhE4ZmgGJEQp0fosmNz84IfqM8cMQ==" - }, - "jest-haste-map": { - "version": "24.8.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-24.8.1.tgz", - "integrity": "sha512-SwaxMGVdAZk3ernAx2Uv2sorA7jm3Kx+lR0grp6rMmnY06Kn/urtKx1LPN2mGTea4fCT38impYT28FfcLUhX0g==", - "requires": { - "@jest/types": "^24.8.0", - "anymatch": "^2.0.0", - "fb-watchman": "^2.0.0", - "fsevents": "^1.2.7", - "graceful-fs": "^4.1.15", - "invariant": "^2.2.4", - "jest-serializer": "^24.4.0", - "jest-util": "^24.8.0", - "jest-worker": "^24.6.0", - "micromatch": "^3.1.10", - "sane": "^4.0.3", - "walker": "^1.0.7" - } - }, - "jest-jasmine2": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-24.8.0.tgz", - "integrity": "sha512-cEky88npEE5LKd5jPpTdDCLvKkdyklnaRycBXL6GNmpxe41F0WN44+i7lpQKa/hcbXaQ+rc9RMaM4dsebrYong==", - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "chalk": "^2.0.1", - "co": "^4.6.0", - "expect": "^24.8.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^24.8.0", - "jest-matcher-utils": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-snapshot": "^24.8.0", - "jest-util": "^24.8.0", - "pretty-format": "^24.8.0", - "throat": "^4.0.0" - } - }, - "jest-leak-detector": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-24.8.0.tgz", - "integrity": "sha512-cG0yRSK8A831LN8lIHxI3AblB40uhv0z+SsQdW3GoMMVcK+sJwrIIyax5tu3eHHNJ8Fu6IMDpnLda2jhn2pD/g==", - "requires": { - "pretty-format": "^24.8.0" - } - }, - "jest-matcher-utils": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-24.8.0.tgz", - "integrity": "sha512-lex1yASY51FvUuHgm0GOVj7DCYEouWSlIYmCW7APSqB9v8mXmKSn5+sWVF0MhuASG0bnYY106/49JU1FZNl5hw==", - "requires": { - "chalk": "^2.0.1", - "jest-diff": "^24.8.0", - "jest-get-type": "^24.8.0", - "pretty-format": "^24.8.0" - } - }, - "jest-message-util": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-24.8.0.tgz", - "integrity": "sha512-p2k71rf/b6ns8btdB0uVdljWo9h0ovpnEe05ZKWceQGfXYr4KkzgKo3PBi8wdnd9OtNh46VpNIJynUn/3MKm1g==", - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "@types/stack-utils": "^1.0.1", - "chalk": "^2.0.1", - "micromatch": "^3.1.10", - "slash": "^2.0.0", - "stack-utils": "^1.0.1" - } - }, - "jest-mock": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-24.8.0.tgz", - "integrity": "sha512-6kWugwjGjJw+ZkK4mDa0Df3sDlUTsV47MSrT0nGQ0RBWJbpODDQ8MHDVtGtUYBne3IwZUhtB7elxHspU79WH3A==", - "requires": { - "@jest/types": "^24.8.0" - } - }, - "jest-pnp-resolver": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz", - "integrity": "sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==" - }, - "jest-regex-util": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-24.3.0.tgz", - "integrity": "sha512-tXQR1NEOyGlfylyEjg1ImtScwMq8Oh3iJbGTjN7p0J23EuVX1MA8rwU69K4sLbCmwzgCUbVkm0FkSF9TdzOhtg==" - }, - "jest-resolve": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-24.8.0.tgz", - "integrity": "sha512-+hjSzi1PoRvnuOICoYd5V/KpIQmkAsfjFO71458hQ2Whi/yf1GDeBOFj8Gxw4LrApHsVJvn5fmjcPdmoUHaVKw==", - "requires": { - "@jest/types": "^24.8.0", - "browser-resolve": "^1.11.3", - "chalk": "^2.0.1", - "jest-pnp-resolver": "^1.2.1", - "realpath-native": "^1.1.0" - } - }, - "jest-resolve-dependencies": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-24.8.0.tgz", - "integrity": "sha512-hyK1qfIf/krV+fSNyhyJeq3elVMhK9Eijlwy+j5jqmZ9QsxwKBiP6qukQxaHtK8k6zql/KYWwCTQ+fDGTIJauw==", - "requires": { - "@jest/types": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-snapshot": "^24.8.0" - } - }, - "jest-runner": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-24.8.0.tgz", - "integrity": "sha512-utFqC5BaA3JmznbissSs95X1ZF+d+4WuOWwpM9+Ak356YtMhHE/GXUondZdcyAAOTBEsRGAgH/0TwLzfI9h7ow==", - "requires": { - "@jest/console": "^24.7.1", - "@jest/environment": "^24.8.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "chalk": "^2.4.2", - "exit": "^0.1.2", - "graceful-fs": "^4.1.15", - "jest-config": "^24.8.0", - "jest-docblock": "^24.3.0", - "jest-haste-map": "^24.8.0", - "jest-jasmine2": "^24.8.0", - "jest-leak-detector": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-resolve": "^24.8.0", - "jest-runtime": "^24.8.0", - "jest-util": "^24.8.0", - "jest-worker": "^24.6.0", - "source-map-support": "^0.5.6", - "throat": "^4.0.0" - } - }, - "jest-runtime": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-24.8.0.tgz", - "integrity": "sha512-Mq0aIXhvO/3bX44ccT+czU1/57IgOMyy80oM0XR/nyD5zgBcesF84BPabZi39pJVA6UXw+fY2Q1N+4BiVUBWOA==", - "requires": { - "@jest/console": "^24.7.1", - "@jest/environment": "^24.8.0", - "@jest/source-map": "^24.3.0", - "@jest/transform": "^24.8.0", - "@jest/types": "^24.8.0", - "@types/yargs": "^12.0.2", - "chalk": "^2.0.1", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.1.15", - "jest-config": "^24.8.0", - "jest-haste-map": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-mock": "^24.8.0", - "jest-regex-util": "^24.3.0", - "jest-resolve": "^24.8.0", - "jest-snapshot": "^24.8.0", - "jest-util": "^24.8.0", - "jest-validate": "^24.8.0", - "realpath-native": "^1.1.0", - "slash": "^2.0.0", - "strip-bom": "^3.0.0", - "yargs": "^12.0.2" - } - }, - "jest-serializer": { - "version": "24.4.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-24.4.0.tgz", - "integrity": "sha512-k//0DtglVstc1fv+GY/VHDIjrtNjdYvYjMlbLUed4kxrE92sIUewOi5Hj3vrpB8CXfkJntRPDRjCrCvUhBdL8Q==" - }, - "jest-snapshot": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-24.8.0.tgz", - "integrity": "sha512-5ehtWoc8oU9/cAPe6fez6QofVJLBKyqkY2+TlKTOf0VllBB/mqUNdARdcjlZrs9F1Cv+/HKoCS/BknT0+tmfPg==", - "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^24.8.0", - "chalk": "^2.0.1", - "expect": "^24.8.0", - "jest-diff": "^24.8.0", - "jest-matcher-utils": "^24.8.0", - "jest-message-util": "^24.8.0", - "jest-resolve": "^24.8.0", - "mkdirp": "^0.5.1", - "natural-compare": "^1.4.0", - "pretty-format": "^24.8.0", - "semver": "^5.5.0" - } - }, - "jest-util": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-24.8.0.tgz", - "integrity": "sha512-DYZeE+XyAnbNt0BG1OQqKy/4GVLPtzwGx5tsnDrFcax36rVE3lTA5fbvgmbVPUZf9w77AJ8otqR4VBbfFJkUZA==", - "requires": { - "@jest/console": "^24.7.1", - "@jest/fake-timers": "^24.8.0", - "@jest/source-map": "^24.3.0", - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "callsites": "^3.0.0", - "chalk": "^2.0.1", - "graceful-fs": "^4.1.15", - "is-ci": "^2.0.0", - "mkdirp": "^0.5.1", - "slash": "^2.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "jest-validate": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-24.8.0.tgz", - "integrity": "sha512-+/N7VOEMW1Vzsrk3UWBDYTExTPwf68tavEPKDnJzrC6UlHtUDU/fuEdXqFoHzv9XnQ+zW6X3qMZhJ3YexfeLDA==", - "requires": { - "@jest/types": "^24.8.0", - "camelcase": "^5.0.0", - "chalk": "^2.0.1", - "jest-get-type": "^24.8.0", - "leven": "^2.1.0", - "pretty-format": "^24.8.0" - } - }, - "jest-watcher": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-24.8.0.tgz", - "integrity": "sha512-SBjwHt5NedQoVu54M5GEx7cl7IGEFFznvd/HNT8ier7cCAx/Qgu9ZMlaTQkvK22G1YOpcWBLQPFSImmxdn3DAw==", - "requires": { - "@jest/test-result": "^24.8.0", - "@jest/types": "^24.8.0", - "@types/yargs": "^12.0.9", - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.1", - "jest-util": "^24.8.0", - "string-length": "^2.0.0" - } - }, - "jest-worker": { - "version": "24.6.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.6.0.tgz", - "integrity": "sha512-jDwgW5W9qGNvpI1tNnvajh0a5IE/PuGLFmHk6aR/BZFz8tSgGw17GsDPXAJ6p91IvYDjOw8GpFbvvZGAK+DPQQ==", - "requires": { - "merge-stream": "^1.0.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "js-levenshtein": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", - "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" - }, - "jsdom": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-11.12.0.tgz", - "integrity": "sha512-y8Px43oyiBM13Zc1z780FrfNLJCXTL40EWlty/LXUtcjykRBNgLlCjWXpfSPBl2iv+N7koQN+dvqszHZgT/Fjw==", - "requires": { - "abab": "^2.0.0", - "acorn": "^5.5.3", - "acorn-globals": "^4.1.0", - "array-equal": "^1.0.0", - "cssom": ">= 0.3.2 < 0.4.0", - "cssstyle": "^1.0.0", - "data-urls": "^1.0.0", - "domexception": "^1.0.1", - "escodegen": "^1.9.1", - "html-encoding-sniffer": "^1.0.2", - "left-pad": "^1.3.0", - "nwsapi": "^2.0.7", - "parse5": "4.0.0", - "pn": "^1.1.0", - "request": "^2.87.0", - "request-promise-native": "^1.0.5", - "sax": "^1.2.4", - "symbol-tree": "^3.2.2", - "tough-cookie": "^2.3.4", - "w3c-hr-time": "^1.0.1", - "webidl-conversions": "^4.0.2", - "whatwg-encoding": "^1.0.3", - "whatwg-mimetype": "^2.1.0", - "whatwg-url": "^6.4.1", - "ws": "^5.2.0", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz", - "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw==" - } - } - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==" - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" - }, - "json5": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.0.tgz", - "integrity": "sha512-8Mh9h6xViijj36g7Dxi+Y4S6hNGV96vcJZr/SrlHh1LR/pEn/8j/+qIBbs44YKl69Lrfctp4QD+AdWLTMqEZAQ==", - "requires": { - "minimist": "^1.2.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" - } - }, - "kind-of": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz", - "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==" - }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==" - }, - "lcid": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", - "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", - "requires": { - "invert-kv": "^2.0.0" - } - }, - "left-pad": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz", - "integrity": "sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA==" - }, - "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=", - "requires": { - "immediate": "~3.0.5" - } - }, - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "localforage": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz", - "integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==", - "requires": { - "lie": "3.1.1" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" - }, - "lodash.sortby": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", - "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=" - }, - "lodash.unescape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", - "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=" - }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "dependencies": { - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==" - } - } - }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", - "requires": { - "tmpl": "1.0.x" - } - }, - "map-age-cleaner": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", - "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", - "requires": { - "p-defer": "^1.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=" - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", - "requires": { - "object-visit": "^1.0.0" - } - }, - "mem": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", - "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" - } - } - }, - "merge-stream": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-1.0.1.tgz", - "integrity": "sha1-QEEgLVCKNCugAXQAjfDCUbjBNeE=", - "requires": { - "readable-stream": "^2.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - } - }, - "mime-db": { - "version": "1.40.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz", - "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==" - }, - "mime-types": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz", - "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==", - "requires": { - "mime-db": "1.40.0" - } - }, - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" - }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "requires": { - "minimist": "0.0.8" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=" - }, - "nan": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", - "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - } - }, - "napi-build-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.1.tgz", - "integrity": "sha512-boQj1WFgQH3v4clhu3mTNfP+vOBxorDlE8EKiMjUlLG3C4qAESnn9AxIOkFgTR2c9LtzNjPrjS60cT27ZKBhaA==" - }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=" - }, - "nedb": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/nedb/-/nedb-1.8.0.tgz", - "integrity": "sha1-DjUCzYLABNU1WkPJ5VV3vXvZHYg=", - "requires": { - "async": "0.2.10", - "binary-search-tree": "0.2.5", - "localforage": "^1.3.0", - "mkdirp": "~0.5.1", - "underscore": "~1.4.4" - } - }, - "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" - }, - "node-abi": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-2.9.0.tgz", - "integrity": "sha512-jmEOvv0eanWjhX8dX1pmjb7oJl1U1oR4FOh0b2GnvALwSYoOdU7sj+kLDSAyjo4pfC9aj/IxkloxdLJQhSSQBA==", - "requires": { - "semver": "^5.4.1" - } - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=" - }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=" - }, - "node-notifier": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", - "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", - "requires": { - "growly": "^1.3.0", - "is-wsl": "^1.1.0", - "semver": "^5.5.0", - "shellwords": "^0.1.1", - "which": "^1.3.0" - } - }, - "node-releases": { - "version": "1.1.25", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.25.tgz", - "integrity": "sha512-fI5BXuk83lKEoZDdH3gRhtsNgh05/wZacuXkgbiYkceE7+QIMXOg98n9ZV7mz27B+kFHnqHcUpscZZlGRSmTpQ==", - "requires": { - "semver": "^5.3.0" - } - }, - "noop-logger": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/noop-logger/-/noop-logger-0.1.1.tgz", - "integrity": "sha1-lKKxYzxPExdVMAfYlm/Q6EG2pMI=" - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "requires": { - "remove-trailing-separator": "^1.0.1" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", - "requires": { - "path-key": "^2.0.0" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" - }, - "nwsapi": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.4.tgz", - "integrity": "sha512-iGfd9Y6SFdTNldEy2L0GUhcarIutFmk+MPWIn9dmj8NMIup03G08uUF2KGbbmv/Ux4RT0VZJoP/sVbWA6d/VIw==" - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", - "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "requires": { - "define-properties": "^1.1.2", - "function-bind": "^1.1.1", - "has-symbols": "^1.0.0", - "object-keys": "^1.0.11" - } - }, - "object.getownpropertydescriptors": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.0.3.tgz", - "integrity": "sha1-h1jIRvW0B62rDyNuCYbxSwUcqhY=", - "requires": { - "define-properties": "^1.1.2", - "es-abstract": "^1.5.1" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", - "requires": { - "isobject": "^3.0.1" - } - }, - "objectbox": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/objectbox/-/objectbox-0.3.0.tgz", - "integrity": "sha1-pefMgeff0iA/6HSI/9InaEXAjhs=", - "requires": { - "busyman": "^0.3.0", - "nedb": "^1.8.0" - } - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" - } - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "requires": { - "mimic-fn": "^1.0.0" - } - }, - "optimist": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", - "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", - "requires": { - "minimist": "~0.0.1", - "wordwrap": "~0.0.2" - }, - "dependencies": { - "wordwrap": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", - "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" - } - } - }, - "optionator": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", - "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.4", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "wordwrap": "~1.0.0" - } - }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" - }, - "os-locale": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", - "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - } - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" - }, - "p-defer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", - "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" - }, - "p-each-series": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-1.0.0.tgz", - "integrity": "sha1-kw89Et0fUOdDRFeiLNbwSsatf3E=", - "requires": { - "p-reduce": "^1.0.0" - } - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" - }, - "p-is-promise": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz", - "integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==" - }, - "p-limit": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", - "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" - } - }, - "p-reduce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", - "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=" - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" - }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "requires": { - "callsites": "^3.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "parse5": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-4.0.0.tgz", - "integrity": "sha512-VrZ7eOd3T1Fk4XWNXMgiGBK/z0MG48BWG2uQNU4I72fkQuKUTZpl+u9k+CxEG0twMVzSmXEEz12z5Fnw1jIQFA==" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "requires": { - "pify": "^3.0.0" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=" - }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", - "requires": { - "node-modules-regexp": "^1.0.0" - } - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "requires": { - "find-up": "^3.0.0" - } - }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=" - }, - "prebuild-install": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-5.3.0.tgz", - "integrity": "sha512-aaLVANlj4HgZweKttFNUVNRxDukytuIuxeK2boIMHjagNJCiVKWFsKF4tCE3ql3GbrD2tExPQ7/pwtEJcHNZeg==", - "requires": { - "detect-libc": "^1.0.3", - "expand-template": "^2.0.3", - "github-from-package": "0.0.0", - "minimist": "^1.2.0", - "mkdirp": "^0.5.1", - "napi-build-utils": "^1.0.1", - "node-abi": "^2.7.0", - "noop-logger": "^0.1.1", - "npmlog": "^4.0.1", - "os-homedir": "^1.0.1", - "pump": "^2.0.1", - "rc": "^1.2.7", - "simple-get": "^2.7.0", - "tar-fs": "^1.13.0", - "tunnel-agent": "^0.6.0", - "which-pm-runs": "^1.0.0" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" - }, - "pretty-format": { - "version": "24.8.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.8.0.tgz", - "integrity": "sha512-P952T7dkrDEplsR+TuY7q3VXDae5Sr7zmQb12JU/NDQa/3CH7/QW0yvqLcGN6jL+zQFKaoJcPc+yJxMTGmosqw==", - "requires": { - "@jest/types": "^24.8.0", - "ansi-regex": "^4.0.0", - "ansi-styles": "^3.2.0", - "react-is": "^16.8.4" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - } - } - }, - "private": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/private/-/private-0.1.8.tgz", - "integrity": "sha512-VvivMrbvd2nKkiG38qjULzlc+4Vx4wm/whI9pQD35YrARNnhxeiRktSOhSukRLFNlzg6Br/cJPet5J/u19r/mg==" - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" - }, - "prompts": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.1.0.tgz", - "integrity": "sha512-+x5TozgqYdOwWsQFZizE/Tra3fKvAoy037kOyU6cgz84n8f6zxngLOV4O32kTwt9FcLCxAqw0P/c8rOr9y+Gfg==", - "requires": { - "kleur": "^3.0.2", - "sisteransi": "^1.0.0" - } - }, - "proving": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/proving/-/proving-0.1.0.tgz", - "integrity": "sha1-dL8fZ4yeXaeHV3CAyQxLaff/pqU=" - }, - "psl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.2.0.tgz", - "integrity": "sha512-GEn74ZffufCmkDDLNcl3uuyF/aSD6exEyh1v/ZSdAomB82t6G9hzJVRx0jBmLDW+VfZqks3aScmMw9DszwUalA==" - }, - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=" - }, - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" - }, - "queue": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.1.tgz", - "integrity": "sha512-AJBQabRCCNr9ANq8v77RJEv73DPbn55cdTb+Giq4X0AVnNVZvMHlYp7XlQiN+1npCZj1DuSmaA2hYVUUDgxFDg==", - "requires": { - "inherits": "~2.0.3" - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "react-is": { - "version": "16.8.6", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz", - "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==" - }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - } - }, - "read-pkg-up": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", - "integrity": "sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==", - "requires": { - "find-up": "^3.0.0", - "read-pkg": "^3.0.0" - } - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "realpath-native": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", - "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", - "requires": { - "util.promisify": "^1.0.0" - } - }, - "regenerate": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz", - "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg==" - }, - "regenerate-unicode-properties": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz", - "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==", - "requires": { - "regenerate": "^1.4.0" - } - }, - "regenerator-runtime": { - "version": "0.13.3", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", - "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" - }, - "regenerator-transform": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.1.tgz", - "integrity": "sha512-flVuee02C3FKRISbxhXl9mGzdbWUVHubl1SMaknjxkFB1/iqpJhArQUvRxOOPEc/9tAiX0BaQ28FJH10E4isSQ==", - "requires": { - "private": "^0.1.6" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - } - }, - "regexp-tree": { - "version": "0.1.11", - "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.11.tgz", - "integrity": "sha512-7/l/DgapVVDzZobwMCCgMlqiqyLFJ0cduo/j+3BcDJIB+yJdsYCfKuI3l/04NV+H/rfNRdPIDbXNZHM9XvQatg==" - }, - "regexpp": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", - "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==" - }, - "regexpu-core": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.5.4.tgz", - "integrity": "sha512-BtizvGtFQKGPUcTy56o3nk1bGRp4SZOTYrDtGNlqCQufptV5IkkLN6Emw+yunAJjzf+C9FQFtvq7IoA3+oMYHQ==", - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.0.2", - "regjsgen": "^0.5.0", - "regjsparser": "^0.6.0", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.1.0" - } - }, - "regjsgen": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.0.tgz", - "integrity": "sha512-RnIrLhrXCX5ow/E5/Mh2O4e/oa1/jW0eaBKTSy3LaCj+M3Bqvm97GWDp2yUtzIs4LEn65zR2yiYGFqb2ApnzDA==" - }, - "regjsparser": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.0.tgz", - "integrity": "sha512-RQ7YyokLiQBomUJuUG8iGVvkgOLxwyZM8k6d3q5SAXpg4r5TZJZigKFvC6PpD+qQ98bCDC5YelPeA3EucDoNeQ==", - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=" - } - } - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==" - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" - }, - "request": { - "version": "2.88.0", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz", - "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==", - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.0", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.4.3", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "dependencies": { - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" - }, - "tough-cookie": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", - "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==", - "requires": { - "psl": "^1.1.24", - "punycode": "^1.4.1" - } - } - } - }, - "request-promise-core": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", - "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", - "requires": { - "lodash": "^4.17.11" - } - }, - "request-promise-native": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", - "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", - "requires": { - "request-promise-core": "1.1.2", - "stealthy-require": "^1.1.1", - "tough-cookie": "^2.3.3" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "resolve": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz", - "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==", - "requires": { - "path-parse": "^1.0.6" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "requires": { - "resolve-from": "^3.0.0" - }, - "dependencies": { - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=" - } - } - }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=" - }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" - }, - "rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", - "requires": { - "glob": "^7.1.3" - } - }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==" - }, - "run-async": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", - "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", - "requires": { - "is-promise": "^2.1.0" - } - }, - "rxjs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz", - "integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==", - "requires": { - "tslib": "^1.9.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", - "requires": { - "ret": "~0.1.10" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" - }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" - }, - "dependencies": { - "minimist": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" - } - } - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, - "semver": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz", - "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA==" - }, - "serialport": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/serialport/-/serialport-7.1.5.tgz", - "integrity": "sha512-NplGdqaY+ZL8t3t5egbT+3oqLW4d7WvDT/x1ACxAyWa1fSnx+KTAmlDHeCls39lXwu8voaOr3bPOW4bwM7PdAA==", - "requires": { - "@serialport/binding-mock": "^2.0.5", - "@serialport/bindings": "^2.0.8", - "@serialport/parser-byte-length": "^2.0.2", - "@serialport/parser-cctalk": "^2.0.2", - "@serialport/parser-delimiter": "^2.0.2", - "@serialport/parser-readline": "^2.0.2", - "@serialport/parser-ready": "^2.0.2", - "@serialport/parser-regex": "^2.0.2", - "@serialport/stream": "^2.0.5", - "debug": "^4.1.1" - }, - "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - } - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==" - }, - "signal-exit": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" - }, - "simple-concat": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", - "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" - }, - "simple-get": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-2.8.1.tgz", - "integrity": "sha512-lSSHRSw3mQNUGPAYRqo7xy9dhKmxFXIjLjp4KHpf99GEH2VH7C3AM+Qfx6du6jhfUi6Vm7XnbEVEf7Wb6N8jRw==", - "requires": { - "decompress-response": "^3.3.0", - "once": "^1.3.1", - "simple-concat": "^1.0.0" - } - }, - "sisteransi": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.2.tgz", - "integrity": "sha512-ZcYcZcT69nSLAR2oLN2JwNmLkJEKGooFMCdvOkFrToUt/WfcRWqhIg4P4KwY4dmLbuyXIx4o4YmPsvMRJYJd/w==" - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==" - }, - "slice-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", - "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", - "requires": { - "ansi-styles": "^3.2.0", - "astral-regex": "^1.0.0", - "is-fullwidth-code-point": "^2.0.0" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - } - } - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-resolve": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.2.tgz", - "integrity": "sha512-MjqsvNwyz1s0k81Goz/9vRBe9SZdB09Bdw+/zYyO+3CuPk6fouTaxscHkgtE8jKvf01kVfl8riHzERQ/kefaSA==", - "requires": { - "atob": "^2.1.1", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz", - "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==", - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - } - } - }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=" - }, - "spdx-correct": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", - "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-exceptions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz", - "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==" - }, - "spdx-expression-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", - "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } - }, - "spdx-license-ids": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", - "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==" - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "requires": { - "extend-shallow": "^3.0.0" - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" - }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "stack-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==" - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "requires": { - "is-descriptor": "^0.1.0" - } - } - } - }, - "stealthy-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", - "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" - }, - "string-length": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-2.0.0.tgz", - "integrity": "sha1-1A27aGo6zpYMHP/KVivyxF+DY+0=", - "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^4.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=" - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { - "has-flag": "^3.0.0" - } - }, - "symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" - }, - "table": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/table/-/table-5.4.4.tgz", - "integrity": "sha512-IIfEAUx5QlODLblLrGTTLJA7Tk0iLSGBvgY8essPRVNGHAzThujww1YqHLs6h3HfTg55h++RzLHH5Xw/rfv+mg==", - "requires": { - "ajv": "^6.10.2", - "lodash": "^4.17.14", - "slice-ansi": "^2.1.0", - "string-width": "^3.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "tar-fs": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-1.16.3.tgz", - "integrity": "sha512-NvCeXpYx7OsmOh8zIOP/ebG55zZmxLE0etfWRbWok+q2Qo8x/vOR/IJT1taADXPe+jsiu9axDb3X4B+iIgNlKw==", - "requires": { - "chownr": "^1.0.1", - "mkdirp": "^0.5.1", - "pump": "^1.0.0", - "tar-stream": "^1.1.2" - }, - "dependencies": { - "pump": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-1.0.3.tgz", - "integrity": "sha512-8k0JupWme55+9tCVE+FS5ULT3K6AbgqrGa58lTT49RpyfwwcGedHqaC5LlQNdEAumn/wFsu6aPwkuPMioy8kqw==", - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - }, - "dependencies": { - "bl": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", - "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - }, - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } - } - }, - "test-exclude": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", - "integrity": "sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==", - "requires": { - "glob": "^7.1.3", - "minimatch": "^3.0.4", - "read-pkg-up": "^4.0.0", - "require-main-filename": "^2.0.0" - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" - }, - "throat": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", - "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=" - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" - }, - "tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "requires": { - "os-tmpdir": "~1.0.2" - } - }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=" - }, - "to-buffer": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", - "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==" - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tr46": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", - "integrity": "sha1-qLE/1r/SSJUZZ0zN5VujaTtwbQk=", - "requires": { - "punycode": "^2.1.0" - } - }, - "trim-right": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/trim-right/-/trim-right-1.0.1.tgz", - "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=" - }, - "tslib": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" - }, - "tsutils": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.14.0.tgz", - "integrity": "sha512-SmzGbB0l+8I0QwsPgjooFRaRvHLBLNYM8SeQ0k6rtNDru5sCGeLJcZdwilNndN+GysuFjF5EIYgN8GfFG6UeUw==", - "requires": { - "tslib": "^1.8.1" - } - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "requires": { - "prelude-ls": "~1.1.2" - } - }, - "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==" - }, - "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", - "optional": true, - "requires": { - "commander": "~2.20.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "optional": true - } - } - }, - "underscore": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", - "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=" - }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==" - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz", - "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g==" - }, - "unicode-property-aliases-ecmascript": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz", - "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw==" - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=" - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" - } - } - }, - "uri-js": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=" - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==" - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "uuid": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", - "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" - }, - "v8-compile-cache": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.0.3.tgz", - "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==" - }, - "validate-npm-package-license": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", - "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", - "requires": { - "spdx-correct": "^3.0.0", - "spdx-expression-parse": "^3.0.0" - } - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "w3c-hr-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", - "integrity": "sha1-gqwr/2PZUOqeMYmlimViX+3xkEU=", - "requires": { - "browser-process-hrtime": "^0.1.2" - } - }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", - "requires": { - "makeerror": "1.0.x" - } - }, - "webidl-conversions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", - "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==" - }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", - "requires": { - "iconv-lite": "0.4.24" - } - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==" - }, - "whatwg-url": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-6.5.0.tgz", - "integrity": "sha512-rhRZRqx/TLJQWUpQ6bmrt2UV4f0HCQ463yQuONJqC6fO2VoEb1pTYddbe59SkYq87aoM5A3bdhMZiUiVws+fzQ==", - "requires": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "requires": { - "isexe": "^2.0.0" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" - }, - "which-pm-runs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", - "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=" - }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "requires": { - "string-width": "^1.0.2 || 2" - } - }, - "wordwrap": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", - "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=" - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" - }, - "write": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", - "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", - "requires": { - "mkdirp": "^0.5.1" - } - }, - "write-file-atomic": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", - "integrity": "sha512-TGHFeZEZMnv+gBFRfjAcxL5bPHrsGKtnb4qsFAws7/vlh+QfwAaySIw4AXP9ZskTTh5GWu3FLuJhsWVdiJPGvg==", - "requires": { - "graceful-fs": "^4.1.11", - "imurmurhash": "^0.1.4", - "signal-exit": "^3.0.2" - } - }, - "ws": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", - "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", - "requires": { - "async-limiter": "~1.0.0" - } - }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "y18n": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", - "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" - }, - "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", - "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.0.0", - "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", - "set-blocking": "^2.0.0", - "string-width": "^2.0.0", - "which-module": "^2.0.0", - "y18n": "^3.2.1 || ^4.0.0", - "yargs-parser": "^11.1.1" - }, - "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=" - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "requires": { - "ansi-regex": "^3.0.0" - } - } - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } - } - } + "debounce": "^1.2.0" } } } diff --git a/package.json b/package.json index 4d9e5d5ed..30587eae7 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,11 @@ "cc2531" ], "scripts": { - "coverage": "jest --collect-coverage", - "eslint": "node_modules/.bin/eslint .", + "test-with-coverage": "jest test --coverage", + "eslint": "node_modules/.bin/eslint lib/", "start": "node index.js", - "test": "jest" + "test": "jest test", + "test-watch": "jest test --watch" }, "author": "Koen Kanters", "license": "GPL-3.0", @@ -39,19 +40,30 @@ "moment": "*", "mqtt": "*", "object-assign-deep": "*", - "queue": "*", "rimraf": "*", "semver": "*", "winston": "2.4.2", - "zigbee-herdsman": "0.5.7", - "zigbee-shepherd-converters": "10.2.5", - "deep-diff": "*" + "zigbee-herdsman": "0.6.4", + "zigbee-herdsman-converters": "11.0.8" }, "devDependencies": { - "@types/jest": "^24.0.11", "eslint": "*", "eslint-config-google": "*", - "eslint-plugin-jest": "^22.3.0", - "jest": "^24.3.1" + "eslint-plugin-jest": "*", + "jest": "*", + "tmp": "*" + }, + "jest": { + "coverageThreshold": { + "global": { + "branches": 100, + "functions": 100, + "lines": 100, + "statements": 100 + } + }, + "collectCoverageFrom": [ + "lib/**" + ] } } diff --git a/test/bridgeConfig.test.js b/test/bridgeConfig.test.js index dcffbbf1d..150074952 100644 --- a/test/bridgeConfig.test.js +++ b/test/bridgeConfig.test.js @@ -1,174 +1,292 @@ -const BridgeConfig = require('../lib/extension/bridgeConfig'); +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +const MQTT = require('./stub/mqtt'); +const path = require('path'); +const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); const settings = require('../lib/util/settings'); -const fs = require('../lib/util/fs'); -const objectAssignDeep = require('object-assign-deep'); -const data = require('../lib/util/data'); -const configurationFile = data.joinPath('configuration.yaml'); -const utils = require('./utils'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); -const mqtt = { - subscribe: (topic) => {}, - log: (type, message) => {}, -}; -describe('BridgeConfig', () => { - let bridgeConfig; - const write = (file, json) => fs.writeYaml(file, json); - const read = (file) => fs.readYaml(file); - const files = new Map(); +describe('Bridge config', () => { + let controller; - beforeAll(() => { - utils.stubLogger(jest); - jest.spyOn(fs, 'readYaml').mockImplementation((file) => { - if (files.has(file)) return objectAssignDeep.noMutate(files.get(file)); - throw new Error(`Fake file not found: ${file}`); - }); - jest.spyOn(fs, 'readYamlIfExists').mockImplementation((file) => { - if (files.has(file)) return objectAssignDeep.noMutate(files.get(file)); - return null; - }); - jest.spyOn(fs, 'writeYaml').mockImplementation((file, content) => { - files.set(file, objectAssignDeep.noMutate(content)); - }); - }); + beforeAll(async () => { + this.version = await require('../lib/util/utils').getZigbee2mqttVersion(); + controller = new Controller(); + await controller.start(); + }) beforeEach(() => { - settings._clear(); - files.clear(); - bridgeConfig = new BridgeConfig(null, mqtt, null, null); + data.writeDefaultConfiguration(); + settings._reRead(); + data.writeDefaultState(); }); - afterAll(() => { - fs.readYaml.mockRestore(); - fs.readYamlIfExists.mockRestore(); - fs.writeYaml.mockRestore(); + it('Should publish bridge configuration on startup', async () => { + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/config', + JSON.stringify({"version":this.version.version,"commit":this.version.commitHash,"coordinator":{"type":"z-Stack","meta":{"version":1}},"log_level":1,"permit_join":false}), + { retain: true, qos: 0 }, + expect.any(Function) + ); }); - it('Setting elapsed false', async () => { - write(configurationFile, {advanced: {elapsed: true}}); - bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/elapsed', 'false'); - const expected = { - advanced: { - elapsed: false, - }, - }; - - expect(read(configurationFile)).toStrictEqual(expected); + it('Should allow to set elapsed', async () => { + MQTT.events.message('zigbee2mqtt/bridge/config/elapsed', 'true'); + await flushPromises(); + expect(settings.get().advanced.elapsed).toBe(true); + MQTT.events.message('zigbee2mqtt/bridge/config/elapsed', 'false'); + await flushPromises(); + expect(settings.get().advanced.elapsed).toBe(false); + MQTT.events.message('zigbee2mqtt/bridge/config/elapsed', 'wrong'); + await flushPromises(); + expect(settings.get().advanced.elapsed).toBe(false); }); - it('Add groups when there are none', async () => { - write(configurationFile, {}); - bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/add_group', 'test123'); - const expected = { - groups: { - '1': { - friendly_name: 'test123', - }, - }, - }; + it('Should allow whitelist', async () => { + const bulb_color = zigbeeHerdsman.devices.bulb_color; + const bulb = zigbeeHerdsman.devices.bulb; + expect(settings.get().whitelist).toStrictEqual([]); + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb_color'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + JSON.stringify({type: "device_whitelisted", "message": "bulb_color"}), + { retain: false, qos: 0 }, + expect.any(Function) + ); - expect(read(configurationFile)).toStrictEqual(expected); + MQTT.publish.mockClear() + expect(settings.get().whitelist).toStrictEqual([bulb_color.ieeeAddr]); + MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + JSON.stringify({type: "device_whitelisted", "message": "bulb"}), + { retain: false, qos: 0 }, + expect.any(Function) + ); + + MQTT.publish.mockClear() + expect(settings.get().whitelist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]); + MQTT.events.message('zigbee2mqtt/bridge/config/whitelist', 'bulb'); + await flushPromises(); + expect(settings.get().whitelist).toStrictEqual([bulb_color.ieeeAddr, bulb.ieeeAddr]); + expect(MQTT.publish).toHaveBeenCalledTimes(0); }); - it('Add groups when there are', async () => { - write(configurationFile, { - groups: { - '1': { - friendly_name: 'test123', - }, - }, - }); - - bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/add_group', 'test1234'); - const expected = { - groups: { - '1': { - friendly_name: 'test123', - }, - '2': { - friendly_name: 'test1234', - }, - }, - }; - - expect(read(configurationFile)).toStrictEqual(expected); + it('Should allow changing device options', async () => { + const bulb_color = zigbeeHerdsman.devices.bulb_color; + expect(settings.getDevice('bulb_color')).toStrictEqual( + {"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": false} + ); + MQTT.events.message('zigbee2mqtt/bridge/config/device_options', JSON.stringify({friendly_name: 'bulb_color', options: {retain: true}})); + await flushPromises(); + expect(settings.getDevice('bulb_color')).toStrictEqual( + {"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": true} + ); + MQTT.events.message('zigbee2mqtt/bridge/config/device_options', JSON.stringify({friendly_name: 'bulb_color', optionswrong: {retain: true}})); + await flushPromises(); + expect(settings.getDevice('bulb_color')).toStrictEqual( + {"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": true} + ); + MQTT.events.message('zigbee2mqtt/bridge/config/device_options', "{friendly_name: 'bulb_color'malformed: {retain: true}}"); + await flushPromises(); + expect(settings.getDevice('bulb_color')).toStrictEqual( + {"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": true} + ); }); - it('Remove group', async () => { - write(configurationFile, { - groups: { - '1': { - friendly_name: 'test123', - }, - '2': { - friendly_name: 'test1234', - }, - }, - }); - - bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/remove_group', 'test123'); - const expected = { - groups: { - '2': { - friendly_name: 'test1234', - }, - }, - }; - - expect(read(configurationFile)).toStrictEqual(expected); + it('Should allow permit join', async () => { + zigbeeHerdsman.permitJoin.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/permit_join', 'true'); + await flushPromises(); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true); + zigbeeHerdsman.permitJoin.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/permit_join', 'false'); + await flushPromises(); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false); }); - it('Get groups', async () => { - jest.spyOn(mqtt, 'log').mockImplementation((type, message) => { - expect(type).toBe('groups'); - expect(Object.keys(message)).toHaveLength(2); - expect(message['1'].friendly_name).toBe('test1'); - expect(message['4711'].friendly_name).toBe('test42'); - }); - - write(configurationFile, { - groups: { - '1': { - friendly_name: 'test1', - }, - '4711': { - friendly_name: 'test42', - }, - }, - }); - - bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/groups', 'whatever'); + it('Should allow to reset', async () => { + zigbeeHerdsman.softReset.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/reset', ''); + await flushPromises(); + expect(zigbeeHerdsman.softReset).toHaveBeenCalledTimes(1); + zigbeeHerdsman.softReset.mockImplementationOnce(() => {throw new Error('')}); + MQTT.events.message('zigbee2mqtt/bridge/config/reset', ''); + await flushPromises(); + expect(zigbeeHerdsman.softReset).toHaveBeenCalledTimes(2); }); - it('Rename device with é', async () => { - jest.spyOn(mqtt, 'log').mockImplementation((type, message) => { - expect(type).toBe('device_renamed'); - }); + it('Should allow to set last_seen', async () => { + MQTT.events.message('zigbee2mqtt/bridge/config/last_seen', 'ISO_8601'); + await flushPromises(); + expect(settings.get().advanced.last_seen).toBe('ISO_8601'); + MQTT.events.message('zigbee2mqtt/bridge/config/last_seen', 'disable'); + await flushPromises(); + expect(settings.get().advanced.last_seen).toBe('disable'); + MQTT.events.message('zigbee2mqtt/bridge/config/last_seen', 'notvalid'); + await flushPromises(); + expect(settings.get().advanced.last_seen).toBe('disable'); + }); - write(configurationFile, { - devices: { - '0x12345678': { - friendly_name: 'test123', - retain: false, - }, - }, - }); + it('Should allow to set log_level', async () => { + MQTT.events.message('zigbee2mqtt/bridge/config/log_level', 'debug'); + await flushPromises(); + expect(logger.transports.console.level).toBe('debug'); + expect(logger.transports.file.level).toBe('debug'); + MQTT.events.message('zigbee2mqtt/bridge/config/log_level', 'error'); + await flushPromises(); + expect(logger.transports.console.level).toBe('error'); + expect(logger.transports.file.level).toBe('error'); + MQTT.events.message('zigbee2mqtt/bridge/config/log_level', 'notvalid'); + await flushPromises(); + expect(logger.transports.console.level).toBe('error'); + expect(logger.transports.file.level).toBe('error'); + }); - const payload = { - old: 'test123', - new: 'tést123', - }; + it('Should allow to get devices', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/devices/get', ''); + await flushPromises(); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/config/devices'); + const payload = JSON.parse(MQTT.publish.mock.calls[0][1]); + expect(payload.length).toStrictEqual(Object.values(zigbeeHerdsman.devices).length); + expect(payload[0]).toStrictEqual({"ieeeAddr": "0x00124b00120144ae", "type": "Coordinator"}); + expect(payload[1]).toStrictEqual({"friendly_name": "bulb", "ieeeAddr": "0x000b57fffec6a5b2", "manufacturerID": 4476, "model": "LED1545G12", "modelID": "TRADFRI bulb E27 WS opal 980lm", "networkAddress": 40369, "powerSource": "Mains (single phase)", "type": "Router"}); + }); - bridgeConfig.onMQTTMessage('zigbee2mqtt/bridge/config/rename', JSON.stringify(payload)); - const expected = { - devices: { - '0x12345678': { - friendly_name: 'tést123', - retain: false, - }, - }, - }; + it('Should allow to get groups', async () => { + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/groups', ''); + await flushPromises(); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + const payload = JSON.parse(MQTT.publish.mock.calls[0][1]); + expect(payload).toStrictEqual({"message": [{"ID": 1, "friendly_name": "group_1", "retain": false, 'devices': []}, {"ID": 2, "friendly_name": "group_2", "retain": false, "devices": []}], "type": "groups"}); + }); - expect(read(configurationFile)).toStrictEqual(expected); + it('Should allow rename devices', async () => { + const bulb_color2 = {"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color2", "friendly_name": "bulb_color2", "retain": false}; + MQTT.publish.mockClear(); + expect(settings.getDevice('bulb_color')).toStrictEqual({"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": false}); + MQTT.events.message('zigbee2mqtt/bridge/config/rename', JSON.stringify({old: 'bulb_color', new: 'bulb_color2'})); + await flushPromises(); + expect(settings.getDevice('bulb_color')).toStrictEqual(null); + expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + JSON.stringify({type: 'device_renamed', message: {from: 'bulb_color', to: 'bulb_color2'}}), + {qos: 0, retain: false}, + expect.any(Function) + ); + + MQTT.events.message('zigbee2mqtt/bridge/config/rename', JSON.stringify({old: 'bulb_color2', newmalformed: 'bulb_color3'})); + await flushPromises(); + expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2); + + MQTT.events.message('zigbee2mqtt/bridge/config/rename', "{old: 'bulb_color2'newmalformed: 'bulb_color3'}"); + await flushPromises(); + expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2); + + MQTT.events.message('zigbee2mqtt/bridge/config/rename', JSON.stringify({old: 'bulb_color', new: 'bulb_color3'})); + await flushPromises(); + expect(settings.getDevice('bulb_color2')).toStrictEqual(bulb_color2); + }); + + it('Should allow to add groups', async () => { + zigbeeHerdsman.createGroup.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/add_group', 'new_group'); + await flushPromises(); + expect(settings.getGroup('new_group')).toStrictEqual({"ID": 3, "friendlyName": "new_group", "friendly_name": "new_group", devices: []}); + expect(zigbeeHerdsman.createGroup).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.createGroup).toHaveBeenCalledWith(3) + }); + + it('Should allow to remove groups', async () => { + settings.addGroup('to_be_removed') + MQTT.events.message('zigbee2mqtt/bridge/config/remove_group', 'to_be_removed'); + await flushPromises(); + expect(settings.getGroup('to_be_removed')).toStrictEqual(null); + }); + + it('Shouldnt do anything on unsupported topic', async () => { + await flushPromises(); + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/not_supported', 'to_be_removed'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); + + it('Should allow to remove device', async () => { + controller.state.state = {'0x000b57fffec6a5b3': {brightness: 100}}; + const device = zigbeeHerdsman.devices.bulb_color; + device.removeFromNetwork.mockClear(); + await flushPromises(); + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'bulb_color'); + await flushPromises(); + expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(controller.state[device.ieeeAddr]).toBeUndefined(); + expect(settings.getDevice('bulb_color')).toBeNull(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + JSON.stringify({type: 'device_removed', message: 'bulb_color'}), + {qos: 0, retain: false}, + expect.any(Function) + ); + expect(controller.state.state).toStrictEqual({}) + }); + + it('Should allow to ban device', async () => { + // TODO: Ban doesn't ban at the moment + const device = zigbeeHerdsman.devices.bulb_color; + device.removeFromNetwork.mockClear(); + await flushPromises(); + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/ban', 'bulb_color'); + await flushPromises(); + expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(controller.state[device.ieeeAddr]).toBeUndefined(); + expect(settings.getDevice('bulb_color')).toBeNull(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bridge/log', + JSON.stringify({type: 'device_banned', message: 'bulb_color'}), + {qos: 0, retain: false}, + expect.any(Function) + ); + }); + + it('Should handle when remove fails', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + device.removeFromNetwork.mockClear(); + device.removeFromNetwork.mockImplementationOnce(() => {throw new Error('')}) + await flushPromises(); + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/remove', 'bulb_color'); + await flushPromises(); + expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(settings.getDevice('bulb_color')).toStrictEqual({"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": false}) + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); + + it('Should handle when ban fails', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + device.removeFromNetwork.mockClear(); + device.removeFromNetwork.mockImplementationOnce(() => {throw new Error('')}) + await flushPromises(); + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/config/ban', 'bulb_color'); + await flushPromises(); + expect(device.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(settings.getDevice('bulb_color')).toStrictEqual({"ID": "0x000b57fffec6a5b3", "friendlyName": "bulb_color", "friendly_name": "bulb_color", "retain": false}) + expect(MQTT.publish).toHaveBeenCalledTimes(0); }); }); diff --git a/test/controller.test.js b/test/controller.test.js index 4f792c82b..3d15e4d2c 100644 --- a/test/controller.test.js +++ b/test/controller.test.js @@ -1,183 +1,331 @@ -const Controller = require('../lib/controller'); +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +const MQTT = require('./stub/mqtt'); +const path = require('path'); +const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); const settings = require('../lib/util/settings'); -const mqtt = require('../lib/mqtt'); -const utils = require('./utils'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); +const tmp = require('tmp'); +const mocksClear = [ + zigbeeHerdsman.permitJoin, mockExit, MQTT.end, zigbeeHerdsman.stop, logger.debug, + MQTT.publish, MQTT.connect, zigbeeHerdsman.devices.bulb_color.removeFromNetwork, + zigbeeHerdsman.devices.bulb.removeFromNetwork +]; + +const fs = require('fs'); describe('Controller', () => { let controller; - let mqttPublish; beforeEach(() => { - utils.stubLogger(jest); - jest.spyOn(settings, 'getDevice').mockReturnValue({friendly_name: 'test'}); - mqttPublish = jest.spyOn(mqtt.prototype, 'publish').mockReturnValue(undefined); controller = new Controller(); - controller.zigbee = { - getDevice: () => { - return { - modelId: 'TRADFRI bulb E27 CWS opal 600lm', - manufName: 'IKEA', - hwVersion: '1.1', - swBuildId: '2.0', - dateCode: '2018-01-01', - }; - }, - }; + mocksClear.forEach((m) => m.mockClear()); + data.writeDefaultConfiguration(); + settings._reRead(); + data.writeDefaultState(); }); - afterEach(() => { - jest.restoreAllMocks(); + it('Start controller', async () => { + await controller.start(); + expect(logger.cleanup).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.constructor).toHaveBeenCalledWith({"network":{"panID":6754,"extenedPanID":[221,221,221,221,221,221,221,221],"channelList":[11],"networkKey":[1,3,5,7,9,11,13,15,0,2,4,6,8,10,12,13]},"databasePath":path.join(data.mockDir, "database.db"),"backupPath":path.join(data.mockDir, "coordinator_backup.json"),"serialPort":{"baudRate":115200,"rtscts":true,"path":"/dev/dummy"}}); + expect(zigbeeHerdsman.start).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.disableLED).toHaveBeenCalledTimes(0); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(true); + expect(logger.info).toHaveBeenCalledWith(`Currently ${Object.values(zigbeeHerdsman.devices).length - 1} devices are joined:`) + expect(logger.info).toHaveBeenCalledWith('bulb (0x000b57fffec6a5b2): LED1545G12 - IKEA TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (Router)'); + expect(logger.info).toHaveBeenCalledWith('remote (0x0017880104e45517): 324131092621 - Philips Hue dimmer switch (EndDevice)'); + expect(logger.info).toHaveBeenCalledWith('0x0017880104e45518 (0x0017880104e45518): Not supported (EndDevice)'); + expect(MQTT.connect).toHaveBeenCalledTimes(1); + expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", {"will": {"payload": "offline", "retain": true, "topic": "zigbee2mqtt/bridge/state"}}); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '{"state":"ON","brightness":50,"color_temp":370,"linkquality":99}',{ retain: true, qos: 0 }, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/remote', '{"brightness":255}', { retain: true, qos: 0 }, expect.any(Function)); }); - describe('Handling zigbee messages', () => { - it('Should handle a zigbee message', () => { - const device = {ieeeAddr: '0x12345678', modelId: 'TRADFRI bulb E27 CWS opal 600lm'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'devChange', {onOff: 1}, 1); - controller.onZigbeeMessage(message); - expect(mqttPublish).toHaveBeenCalledTimes(1); - expect(mqttPublish.mock.calls[0][1]).toBe(JSON.stringify({state: 'ON'})); - }); + it('Start controller with specific MQTT settings', async () => { + const ca = tmp.fileSync().name; + fs.writeFileSync(ca, "ca"); + const key = tmp.fileSync().name; + fs.writeFileSync(key, "key"); + const cert = tmp.fileSync().name; + fs.writeFileSync(cert, "cert"); - it('Should handle a zigbee message when include_device_information is set', () => { - jest.spyOn(settings, 'get').mockReturnValue({ - mqtt: { - include_device_information: true, - }, - advanced: { - cache_state: false, - last_seen: 'disable', - }, - experimental: { - output: 'json', - }, - }); + const configuration = { + base_topic: "zigbee2mqtt", + server: "mqtt://localhost", + ca, cert, key, + password: 'pass', + user: 'user1', + client_id: 'my_client_id', + reject_unauthorized: false, + } + settings.set(['mqtt'], configuration) + await controller.start(); + await flushPromises(); + expect(MQTT.connect).toHaveBeenCalledTimes(1); + const expected = { + "will": {"payload": "offline", "retain": true, "topic": "zigbee2mqtt/bridge/state"}, + ca: Buffer.from([99, 97]), + key: Buffer.from([107, 101, 121]), + cert: Buffer.from([99, 101, 114, 116]), + password: 'pass', + username: 'user1', + clientId: 'my_client_id', + rejectUnauthorized: false, - const payload = { - 'state': 'ON', - 'device': { - 'ieeeAddr': '0x12345678', - 'friendlyName': 'test', - 'manufName': 'IKEA', - 'modelId': 'TRADFRI bulb E27 CWS opal 600lm', - 'hwVersion': '1.1', - 'swBuildId': '2.0', - 'dateCode': '2018-01-01', - }, - }; + } + expect(MQTT.connect).toHaveBeenCalledWith("mqtt://localhost", expected); + }); - const device = {ieeeAddr: '0x12345678', modelId: 'TRADFRI bulb E27 CWS opal 600lm'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'devChange', {onOff: 1}, 1); - controller.onZigbeeMessage(message); - expect(mqttPublish).toHaveBeenCalledTimes(1); - expect(JSON.parse(mqttPublish.mock.calls[0][1])).toStrictEqual(payload); - }); + it('Log when MQTT client is unavailable', async () => { + jest.useFakeTimers(); + await controller.start(); + await flushPromises(); + logger.error.mockClear(); + controller.mqtt.client.reconnecting = true; + jest.advanceTimersByTime(11 * 1000); + await flushPromises(); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!"); + controller.mqtt.client.reconnecting = false; + }); - it('Should output to json by default', () => { - const payload = {temperature: 1, humidity: 2}; - controller.publishEntityState('0x12345678', payload); - expect(mqttPublish).toHaveBeenCalledTimes(1); - expect(JSON.parse(mqttPublish.mock.calls[0][1])).toStrictEqual(payload); - }); + it('Dont publish to mqtt when client is unavailable', async () => { + await controller.start(); + await flushPromises(); + logger.error.mockClear(); + controller.mqtt.client.reconnecting = true; + await controller.publishEntityState('bulb', {state: 'ON', brightness: 50, color_temp: 370, color: {r: 100, g: 50, b: 10}, dummy: {1: 'yes', 2: 'no'}}); + await flushPromises(); + expect(logger.error).toHaveBeenCalledTimes(2); + expect(logger.error).toHaveBeenCalledWith("Not connected to MQTT server!"); + expect(logger.error).toHaveBeenCalledWith("Cannot send message: topic: 'zigbee2mqtt/bulb', payload: '{\"state\":\"ON\",\"brightness\":50,\"color_temp\":370,\"linkquality\":99,\"color\":{\"r\":100,\"g\":50,\"b\":10},\"dummy\":{\"1\":\"yes\",\"2\":\"no\"}}"); + controller.mqtt.client.reconnecting = false; + }); - it('Should output to attribute', () => { - jest.spyOn(settings, 'get').mockReturnValue({ - mqtt: { - include_device_information: false, - }, - advanced: { - cache_state: false, - }, - experimental: { - output: 'attribute', - }, - }); + it('Load empty state when state file does not exist', async () => { + data.removeState(); + await controller.start(); + await flushPromises(); + expect(controller.state.state).toStrictEqual({}); + }); - const payload = { - temperature: 1, - humidity: 2, - state: 'ON', - allowedStates: ['ON', 'OFF'], - color: {r: 100, g: 0, b: 102, a: 0}, - nested: { - state: 'OFF', - color: {r: 1, g: 0, b: 2}, - }, - }; - controller.publishEntityState('0x12345678', payload); - expect(mqttPublish.mock.calls.map((x) => [x[0], x[1]])).toEqual([ - ['test/temperature', '1'], - ['test/humidity', '2'], - ['test/state', 'ON'], - ['test/allowedStates', 'ON,OFF'], - ['test/color', '100,0,102'], - ['test/nested-state', 'OFF'], - ['test/nested-color', '1,0,2'], - ]); - }); + it('Should remove non whitelisted devices on startup', async () => { + settings.set(['whitelist'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(0); + expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(1); + }); - it('Should cache state', () => { - jest.spyOn(settings, 'get').mockReturnValue({ - mqtt: { - include_device_information: false, - }, - advanced: { - cache_state: true, - }, - experimental: { - output: 'json', - }, - }); + it('Should remove banned devices on startup', async () => { + settings.set(['ban'], [zigbeeHerdsman.devices.bulb_color.ieeeAddr]); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsman.devices.bulb_color.removeFromNetwork).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.devices.bulb.removeFromNetwork).toHaveBeenCalledTimes(0); + }); - controller.publishEntityState('0x12345678', {temperature: 1}); - controller.publishEntityState('0x12345678', {humidity: 2}); - controller.publishEntityState('0x12345678', {humidity: 5, temperature: 4}); - controller.publishEntityState('0x12345678', {temperature: 9}); - controller.publishEntityState('0x12345678', {temperature: 3}); - expect(mqttPublish).toHaveBeenCalledTimes(5); - expect(JSON.parse(mqttPublish.mock.calls[0][1])).toStrictEqual({temperature: 1}); - expect(JSON.parse(mqttPublish.mock.calls[1][1])).toStrictEqual({temperature: 1, humidity: 2}); - expect(JSON.parse(mqttPublish.mock.calls[2][1])).toStrictEqual({temperature: 4, humidity: 5}); - expect(JSON.parse(mqttPublish.mock.calls[3][1])).toStrictEqual({temperature: 9, humidity: 5}); - expect(JSON.parse(mqttPublish.mock.calls[4][1])).toStrictEqual({temperature: 3, humidity: 5}); - }); + it('Start controller fails', async () => { + zigbeeHerdsman.start.mockImplementationOnce(() => {throw new Error('failed')}); + await controller.start(); + expect(mockExit).toHaveBeenCalledTimes(1); + }); - it('Shouldnt cache state', () => { - jest.spyOn(settings, 'get').mockReturnValue({ - mqtt: { - include_device_information: false, - }, - advanced: { - cache_state: false, - }, - experimental: { - output: 'json', - }, - }); + it('Start controller with permit join true', async () => { + settings.set(['permit_join'], false); + await controller.start(); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.permitJoin).toHaveBeenCalledWith(false); + }); - controller.publishEntityState('0x12345678', {temperature: 1}); - controller.publishEntityState('0x12345678', {humidity: 2}); - controller.publishEntityState('0x12345678', {temperature: 3}); - expect(mqttPublish).toHaveBeenCalledTimes(3); - expect(JSON.parse(mqttPublish.mock.calls[0][1])).toStrictEqual({temperature: 1}); - expect(JSON.parse(mqttPublish.mock.calls[1][1])).toStrictEqual({humidity: 2}); - expect(JSON.parse(mqttPublish.mock.calls[2][1])).toStrictEqual({temperature: 3}); - }); + it('Start controller with permit join true', async () => { + settings.set(['serial', 'disable_led'], true); + await controller.start(); + expect(zigbeeHerdsman.disableLED).toHaveBeenCalledTimes(1); + }); - it('Should not send empty messages', () => { - jest.spyOn(settings, 'get').mockReturnValue({ - mqtt: { - include_device_information: false, - }, - advanced: { - cache_state: true, - }, - experimental: { - output: 'json', - }, - }); + it('Start controller and stop', async () => { + await controller.start(); + await controller.stop(); + expect(MQTT.end).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledWith(0); + }); - controller.publishEntityState('0x12345678', {}); - expect(mqttPublish).toHaveBeenCalledTimes(0); - }); + it('Start controller and stop', async () => { + zigbeeHerdsman.stop.mockImplementationOnce(() => {throw new Error('failed')}) + await controller.start(); + await controller.stop(); + expect(MQTT.end).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('Start controller adapter disconnects', async () => { + zigbeeHerdsman.stop.mockImplementationOnce(() => {throw new Error('failed')}) + await controller.start(); + await zigbeeHerdsman.events.adapterDisconnected(); + await flushPromises(); + expect(MQTT.end).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.stop).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledTimes(1); + expect(mockExit).toHaveBeenCalledWith(1); + }); + + it('Handle mqtt message', async () => { + await controller.start(); + logger.debug.mockClear(); + await MQTT.events.message('dummytopic', 'dummymessage'); + expect(logger.debug).toHaveBeenCalledWith("Received MQTT message on 'dummytopic' with data 'dummymessage'") + }); + + it('On zigbee event message', async () => { + await controller.start(); + const device = zigbeeHerdsman.devices.bulb; + const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, cluster: 'genBasic', data: {modelId: device.modelID}}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'bulb' of type 'attributeReport' with data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1`); + }); + + it('On zigbee event message with group ID', async () => { + await controller.start(); + const device = zigbeeHerdsman.devices.bulb; + const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, groupID: 0, cluster: 'genBasic', data: {modelId: device.modelID}}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(logger.debug).toHaveBeenCalledWith(`Received Zigbee message from 'bulb' of type 'attributeReport' with data '{"modelId":"TRADFRI bulb E27 WS opal 980lm"}' from endpoint 1 with groupID 0`); + }); + + it('On zigbee event message from unkown device should create it', async () => { + await controller.start(); + const device = zigbeeHerdsman.devices.notInSettings; + expect(settings.getDevice(device.ieeeAddr)).toBeNull(); + const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, groupID: 0, cluster: 'genBasic', data: {modelId: device.modelID}}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(settings.getDevice(device.ieeeAddr)).toStrictEqual({"ID": "0x0017880104e45519", "friendlyName": "0x0017880104e45519", "friendly_name": "0x0017880104e45519", "retain": false}); + }); + + it('On zigbee deviceJoined', async () => { + await controller.start(); + const device = zigbeeHerdsman.devices.bulb; + const payload = {device}; + await zigbeeHerdsman.events.deviceJoined(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bridge/log", `{"type":"device_connected","message":"bulb"}`, {"retain": false, qos: 0}, expect.any(Function)); + }); + + it('On zigbee deviceInterview started', async () => { + await controller.start(); + const device = zigbeeHerdsman.devices.bulb; + const payload = {device, status: 'started'}; + await zigbeeHerdsman.events.deviceInterview(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', '{"type":"pairing","message":"interview_started","meta":{"friendlyName":"bulb"}}', { retain: false, qos: 0 }, expect.any(Function)); + }); + + it('On zigbee deviceInterview failed', async () => { + await controller.start(); + const device = zigbeeHerdsman.devices.bulb; + const payload = {device, status: 'failed'}; + await zigbeeHerdsman.events.deviceInterview(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', '{"type":"pairing","message":"interview_failed","meta":{"friendlyName":"bulb"}}', { retain: false, qos: 0 }, expect.any(Function)); + }); + + it('On zigbee deviceInterview successful supported', async () => { + await controller.start(); + const device = zigbeeHerdsman.devices.bulb; + const payload = {device, status: 'successful'}; + await zigbeeHerdsman.events.deviceInterview(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', '{"type":"pairing","message":"interview_successful","meta":{"friendlyName":"bulb","model":"LED1545G12","vendor":"IKEA","description":"TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white","supported":true}}', { retain: false, qos: 0 }, expect.any(Function)); + }); + + it('On zigbee deviceInterview successful not supported', async () => { + await controller.start(); + const device = zigbeeHerdsman.devices.unsupported; + const payload = {device, status: 'successful'}; + await zigbeeHerdsman.events.deviceInterview(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', '{"type":"pairing","message":"interview_successful","meta":{"friendlyName":"0x0017880104e45518","supported":false}}', { retain: false, qos: 0 }, expect.any(Function)); + }); + + it('On zigbee event device announce', async () => { + await controller.start(); + const device = zigbeeHerdsman.devices.bulb; + const payload = {device}; + await zigbeeHerdsman.events.deviceAnnounce(payload); + await flushPromises(); + expect(logger.debug).toHaveBeenCalledWith(`Device 'bulb' announced itself`); + }); + + it('On zigbee event device leave', async () => { + await controller.start(); + const device = zigbeeHerdsman.devices.bulb; + const payload = {ieeeAddr: device.ieeeAddr}; + await zigbeeHerdsman.events.deviceLeave(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bridge/log', '{"type":"device_removed","message":"left_network","meta":{"friendlyName":"bulb"}}', { retain: false, qos: 0}, expect.any(Function)); + }); + + it('Publish entity state attribute output', async () => { + await controller.start(); + settings.set(['experimental', 'output'], 'attribute'); + MQTT.publish.mockClear(); + await controller.publishEntityState('bulb', {state: 'ON', brightness: 50, color_temp: 370, color: {r: 100, g: 50, b: 10}, dummy: {1: 'yes', 2: 'no'}}); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/state", "ON", {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/brightness", "50", {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color_temp", "370", {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/color", '100,50,10', {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/dummy-1", 'yes', {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb/dummy-2", 'no', {"qos": 0, "retain": true}, expect.any(Function)); + }); + + it('Publish entity state with device information', async () => { + await controller.start(); + settings.set(['mqtt', 'include_device_information'], true); + MQTT.publish.mockClear(); + await controller.publishEntityState('bulb', {state: 'ON'}); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith('zigbee2mqtt/bulb', '{"state":"ON","brightness":50,"color_temp":370,"linkquality":99,"device":{"friendlyName":"bulb","ieeeAddr":"0x000b57fffec6a5b2","networkAddress":40369,"type":"Router","manufacturerID":4476,"powerSource":"Mains (single phase)"}}', {"qos": 0, "retain": true}, expect.any(Function)); + }); + + it('Publish entity state no empty messages', async () => { + data.writeEmptyState(); + await controller.start(); + MQTT.publish.mockClear(); + await controller.publishEntityState('bulb', {}); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); + + it('Publish should not cache when set', async () => { + settings.set(['advanced', 'cache_state'], false); + data.writeEmptyState(); + await controller.start(); + MQTT.publish.mockClear(); + await controller.publishEntityState('bulb', {state: 'ON'}); + await controller.publishEntityState('bulb', {brightness: 200}); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", "{\"state\":\"ON\"}", {"qos": 0, "retain": true}, expect.any(Function)); + expect(MQTT.publish).toHaveBeenCalledWith("zigbee2mqtt/bulb", "{\"brightness\":200}", {"qos": 0, "retain": true}, expect.any(Function)); + }); + + it('Publish should not do anything for unknown entity', async () => { + await controller.start(); + MQTT.publish.mockClear(); + await controller.publishEntityState('bulb-unknown', {brightness: 200}); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); }); }); diff --git a/test/data.test.js b/test/data.test.js index b3558453b..f9b05d530 100644 --- a/test/data.test.js +++ b/test/data.test.js @@ -1,13 +1,11 @@ +const logger = require('./stub/logger'); const data = require('../lib/util/data.js'); const path = require('path'); -const utils = require('./utils'); +const tmp = require('tmp'); +const fs = require('fs'); describe('Data', () => { describe('Get path', () => { - beforeEach(() => { - utils.stubLogger(jest); - }); - it('Should return correct path', () => { const expected = path.normalize(path.join(__dirname, '..', 'data')); const actual = data.getPath(); @@ -15,11 +13,12 @@ describe('Data', () => { }); it('Should return correct path when ZIGBEE2MQTT_DATA set', () => { - const expected = path.join('var', 'zigbee2mqtt'); + const expected = tmp.dirSync().name; process.env.ZIGBEE2MQTT_DATA = expected; data._reload(); const actual = data.getPath(); expect(actual).toBe(expected); + expect(data.joinPath('test')).toStrictEqual(path.join(expected, 'test')); delete process.env.ZIGBEE2MQTT_DATA; data._reload(); }); diff --git a/test/deviceAvailability.test.js b/test/deviceAvailability.test.js index 824cb6e19..b0e613e66 100644 --- a/test/deviceAvailability.test.js +++ b/test/deviceAvailability.test.js @@ -1,46 +1,235 @@ -const Availability = require('../lib/extension/deviceAvailability'); -const utils = require('./utils'); +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3') +zigbeeHerdsman.returnDevices.push('0x00124b00120144ae') +const MQTT = require('./stub/mqtt'); +const settings = require('../lib/util/settings'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +const mockExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); -describe('Availability', () => { - let availability; +const mocksClear = [MQTT.publish, logger.warn, logger.debug]; - beforeEach(() => { - utils.stubLogger(jest); - availability = new Availability(null, null, null, () => {}); +describe('Device availability', () => { + let controller; + + function getExtension() { + return controller.extensions.find((e) => e.constructor.name === 'DeviceAvailability'); + } + + beforeEach(async () => { + data.writeDefaultConfiguration(); + settings._reRead(); + data.writeEmptyState(); + jest.useFakeTimers(); + settings.set(['advanced', 'availability_timeout'], 10) + controller = new Controller(); + mocksClear.forEach((m) => m.mockClear()); + await controller.start(); + await flushPromises(); }); - afterEach(() => { - jest.restoreAllMocks(); + afterEach(async () => { + await controller.stop(); + await flushPromises(); + }) + + it('Should publish availabilty on startup', async () => { + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color/availability', + 'online', + { retain: true, qos: 0 }, + expect.any(Function) + ); }); - describe('Determine pingable devices', () => { - it('Router device should be a pingable device', () => { - const device = { - powerSource: 'Mains (single phase)', - type: 'Router', - }; + it('Should publish availabilty offline when ping fails', async () => { + MQTT.publish.mockClear(); + logger.error.mockClear(); + logger.debug.mockClear(); + const device = zigbeeHerdsman.devices.bulb_color; + device.ping.mockImplementationOnce(() => {throw new Error('failed')}); + jest.advanceTimersByTime(11 * 1000); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish).toHaveBeenNthCalledWith(1, + 'zigbee2mqtt/bulb_color/availability', + 'offline', + { retain: true, qos: 0 }, + expect.any(Function) + ); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith("Failed to ping '0x000b57fffec6a5b3'"); + device.ping.mockImplementationOnce(() => {throw new Error('failed')}); + jest.advanceTimersByTime(11 * 1000); + await flushPromises(); + expect(logger.debug).toHaveBeenCalledTimes(1); + expect(logger.debug).toHaveBeenCalledWith("Failed to ping '0x000b57fffec6a5b3'"); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish).toHaveBeenNthCalledWith(2, + 'zigbee2mqtt/bulb_color/availability', + 'offline', + { retain: true, qos: 0 }, + expect.any(Function) + ); + }); - expect(availability.isPingable(device)).toBe(true); - }); + it('Should publish availabilty online and query state on reconnect', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + device.ping.mockImplementationOnce(() => {throw new Error('failed')}); + jest.advanceTimersByTime(11 * 1000); + await flushPromises(); + MQTT.publish.mockClear(); + jest.advanceTimersByTime(11 * 1000); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color/availability', + 'online', + { retain: true, qos: 0 }, + expect.any(Function) + ); - it('Battery device should not be a pingable device', () => { - const device = { - powerSource: 'Battery', - type: 'EndDevice', - }; + expect(endpoint.read).toHaveBeenCalledTimes(3); + expect(endpoint.read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); + expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); + expect(endpoint.read).toHaveBeenCalledWith('lightingColorCtrl', ['currentX', 'currentY', 'colorTemperature']); + }); - expect(availability.isPingable(device)).toBe(false); - }); + it('Shouldnt ping again when still pinging', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + device.ping.mockClear(); + device.ping.mockImplementationOnce(async () => {await wait(100000000)}); + jest.advanceTimersByTime(11 * 1000); + await flushPromises(); + expect(device.ping).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(11 * 1000); + await flushPromises(); + expect(device.ping).toHaveBeenCalledTimes(1); + }); - it('E11-G13 should be a pingable device', () => { - const device = { - powerSource: 'Mains (single phase)', - type: 'EndDevice', - modelId: 'E11-G13', - manufId: 4448, - }; + it('Should mark device online when receiving message while offline', async () => { + MQTT.publish.mockClear(); + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + device.ping.mockImplementationOnce(() => {throw new Error('failed')}); + jest.advanceTimersByTime(11 * 1000); + await flushPromises(); - expect(availability.isPingable(device)).toBe(true); - }); + MQTT.publish.mockClear(); + const data = {modelID: 'test'} + const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish).toHaveBeenNthCalledWith(1, + 'zigbee2mqtt/bulb_color/availability', + 'online', + { retain: true, qos: 0 }, + expect.any(Function) + ); + }); + + it('Should retrieve the state when device is turned on/off within availability timeout', async () => { + MQTT.publish.mockClear(); + getExtension().state = {}; + const payload = {device: zigbeeHerdsman.devices.bulb_color}; + await zigbeeHerdsman.events.deviceJoined(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb_color/availability', + 'online', + { retain: true, qos: 0 }, + expect.any(Function) + ); + }); + + it('Should retrieve the state when device is turned on/off within availability timeout on deviceAnnounce', async () => { + MQTT.publish.mockClear(); + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + endpoint.read.mockClear(); + await zigbeeHerdsman.events.deviceAnnounce({device}); + await flushPromises(); + expect(endpoint.read).toHaveBeenCalledTimes(3); + expect(endpoint.read).toHaveBeenCalledWith('genLevelCtrl', ['currentLevel']); + expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); + expect(endpoint.read).toHaveBeenCalledWith('lightingColorCtrl', ['currentX', 'currentY', 'colorTemperature']); + }); + + it('Should not retrieve the state when device is turned on/off within availability timeout on deviceJoined', async () => { + MQTT.publish.mockClear(); + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + endpoint.read.mockClear(); + await zigbeeHerdsman.events.deviceJoined({device}); + await flushPromises(); + expect(endpoint.read).toHaveBeenCalledTimes(0); + }); + + it('Should do nothing when receiving message from non-pingable device', async () => { + MQTT.publish.mockClear(); + const device = zigbeeHerdsman.devices.WXKG11LM; + const payload = {device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, cluster: 'genBasic', data: {modelId: device.modelID}}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); + + it('Should not do anything when message has no device', async () => { + MQTT.publish.mockClear(); + const device = zigbeeHerdsman.devices.bulb_color; + const payload = {ieeeAddr: device.ieeeAddr}; + await zigbeeHerdsman.events.deviceLeave(payload); + await flushPromises(); + expect(MQTT.publish.mock.calls.find((c) => c[0].includes('availability'))).toBeUndefined(); + }); + + it('Should not ping blacklisted devices', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + settings.set(['advanced', 'availability_blacklist'], [device.ieeeAddr]) + await controller.stop(); + await flushPromises(); + controller = new Controller(); + await controller.start(); + await flushPromises(); + device.ping.mockClear(); + jest.advanceTimersByTime(11 * 1000); + await flushPromises(); + expect(device.ping).toHaveBeenCalledTimes(0); + }); + + it('Should ping forced pingable devices', async () => { + const device = zigbeeHerdsman.devices.E11_G13; + const endpoint = device.getEndpoint(1); + const data = {modelID: 'test'} + const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + device.ping.mockClear(); + jest.advanceTimersByTime(11 * 1000); + await flushPromises(); + expect(device.ping).toHaveBeenCalledTimes(1); + }); + + it('Should not read when device has no modelID and reconnects', async () => { + const device = zigbeeHerdsman.devices.nomodel; + getExtension().state[device.ieeeAddr] = true; + const endpoint = device.getEndpoint(1); + await zigbeeHerdsman.events.deviceAnnounce({device}); + await flushPromises(); + expect(endpoint.read).toHaveBeenCalledTimes(0); + }); + + it('Should not read when device has is unsupported', async () => { + const device = zigbeeHerdsman.devices.unsupported_router; + getExtension().state[device.ieeeAddr] = true; + const endpoint = device.getEndpoint(1); + await zigbeeHerdsman.events.deviceAnnounce({device}); + await flushPromises(); + expect(endpoint.read).toHaveBeenCalledTimes(0); }); }); diff --git a/test/deviceBind.test.js b/test/deviceBind.test.js index 67dab763e..0ae9a662b 100644 --- a/test/deviceBind.test.js +++ b/test/deviceBind.test.js @@ -1,196 +1,167 @@ -const DeviceBind = require('../lib/extension/deviceBind'); -const utils = require('./utils'); +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +const MQTT = require('./stub/mqtt'); const settings = require('../lib/util/settings'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); -const mqtt = { - subscribe: () => {}, - log: () => {}, -}; +describe('Device bind', () => { + let controller; -const devices = { - bulb: { - getSimpleDesc: () => { - return {inClusterList: [6, 8]}; - }, - }, - remote: { - getSimpleDesc: () => { - return {outClusterList: [5, 6, 8]}; - }, - }, - switch_ep2: { - getSimpleDesc: () => { - return {outClusterList: [5, 6]}; - }, - }, - switch_ep3: { - getSimpleDesc: () => { - return {inClusterList: [5, 6]}; - }, - }, - occupancy_sensor: { - getSimpleDesc: () => { - return {outClusterList: [5, 6]}; - }, - }, -}; - -const zigbee = { - bind: jest.fn((ep, cluster, target, callback) => { - callback(false, null); - }), - unbind: jest.fn((ep, cluster, target, callback) => { - callback(false, null); - }), - getEndpoint: (ID, ep) => { - if (ID === 'bulb' || ID === '0x002') { - return devices.bulb; - } else if (ID === 'remote' || ID === '0x001') { - return devices.remote; - } else if (ep == 2 && ID === 'switch_ep2') { - return devices.switch_ep2; - } else if (ep == 3 && ID === 'switch_ep3') { - return devices.switch_ep3; - } else if (ep == 2 && ID == 'occupancy_sensor') { - return devices.occupancy_sensor; + mockClear = (device) => { + for (const endpoint of device.endpoints) { + endpoint.read.mockClear(); + endpoint.write.mockClear(); + endpoint.configureReporting.mockClear(); + endpoint.bind.mockClear(); } + } - throw new Error(`No mock for ${ID} and ep ${ep}`); - }, - getDevice: (ID) => { - const lookup = { - 'occupancy_sensor': 'SML002', - 'switch_ep2': 'lumi.sensor_86sw2.es1', - 'switch_ep3': 'DNCKAT_S003', - 'bulb': 'TRADFRI bulb E27 WS opal 980lm', - 'remote': 'TRADFRI remote control', - '0x002': 'TRADFRI bulb E27 WS opal 980lm', - '0x001': 'TRADFRI remote control', - }; - - if (lookup.hasOwnProperty(ID)) { - return {modelId: lookup[ID]}; - } - - throw new Error(`No mock for ${ID}`); - }, -}; - -describe('DeviceBind', () => { - let deviceBind; - - beforeEach(() => { - utils.stubLogger(jest); - deviceBind = new DeviceBind(zigbee, mqtt, null, null); + beforeEach(async () => { + data.writeDefaultConfiguration(); + settings._reRead(); + data.writeEmptyState(); + controller = new Controller(); + await controller.start(); + await flushPromises(); + this.coordinatorEndoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); + MQTT.publish.mockClear(); }); - afterEach(() => { - zigbee.bind.mockClear(); - zigbee.unbind.mockClear(); - jest.restoreAllMocks(); + it('Should bind', async () => { + const device = zigbeeHerdsman.devices.remote; + const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); + const endpoint = device.getEndpoint(1); + mockClear(device); + MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'bulb_color'); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(3); + expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); + expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); + expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); + expect(MQTT.publish).toHaveBeenCalledTimes(3); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'bulb_color', cluster: 'genOnOff'}}); + expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'}}); }); - describe('Bind devices', () => { - it('Bind', async () => { - deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/remote', 'bulb'); - expect(zigbee.bind).toHaveBeenCalledTimes(2); - expect(zigbee.bind).toHaveBeenNthCalledWith(1, - devices.remote, - 6, - devices.bulb, - expect.any(Function) - ); - expect(zigbee.bind).toHaveBeenNthCalledWith(2, - devices.remote, - 8, - devices.bulb, - expect.any(Function) - ); - }); - - it('Bind by friendly name', async () => { - jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockImplementation((friendlyName) => { - const lookup = { - remote: '0x001', - bulb: '0x002', - }; - - return lookup[friendlyName]; - }); - - deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/remote', 'bulb'); - - expect(zigbee.bind).toHaveBeenCalledTimes(2); - expect(zigbee.bind).toHaveBeenNthCalledWith(1, - devices.remote, - 6, - devices.bulb, - expect.any(Function) - ); - expect(zigbee.bind).toHaveBeenNthCalledWith(2, - devices.remote, - 8, - devices.bulb, - expect.any(Function) - ); - }); - - it('Bind non default ep', async () => { - deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/switch_ep2/right', 'bulb'); - expect(zigbee.bind).toHaveBeenCalledTimes(1); - expect(zigbee.bind).toHaveBeenNthCalledWith(1, - devices.switch_ep2, - 6, - devices.bulb, - expect.any(Function) - ); - }); - - it('Bind non default ep to target with non default ep', async () => { - deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/switch_ep2/right', 'switch_ep3/right'); - expect(zigbee.bind).toHaveBeenCalledTimes(2); - expect(zigbee.bind).toHaveBeenNthCalledWith(1, - devices.switch_ep2, - 5, - devices.switch_ep3, - expect.any(Function) - ); - expect(zigbee.bind).toHaveBeenNthCalledWith(2, - devices.switch_ep2, - 6, - devices.switch_ep3, - expect.any(Function) - ); - }); - - it('Bind default ep when mapped', async () => { - deviceBind.onMQTTMessage('zigbee2mqtt/bridge/bind/occupancy_sensor', 'bulb'); - expect(zigbee.bind).toHaveBeenCalledTimes(1); - expect(zigbee.bind).toHaveBeenNthCalledWith(1, - devices.occupancy_sensor, - 6, - devices.bulb, - expect.any(Function) - ); - }); + it('Should unbind', async () => { + const device = zigbeeHerdsman.devices.remote; + const target = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); + const endpoint = device.getEndpoint(1); + mockClear(device); + MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', 'bulb_color'); + await flushPromises(); + expect(endpoint.unbind).toHaveBeenCalledTimes(3); + expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target); + expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target); + expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target); + expect(MQTT.publish).toHaveBeenCalledTimes(3); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'bulb_color', cluster: 'genScenes'}}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'bulb_color', cluster: 'genOnOff'}}); + expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'bulb_color', cluster: 'genLevelCtrl'}}); }); - describe('Unbind devices', () => { - it('Unbind', async () => { - deviceBind.onMQTTMessage('zigbee2mqtt/bridge/unbind/remote', 'bulb'); - expect(zigbee.unbind).toHaveBeenCalledTimes(2); - expect(zigbee.unbind).toHaveBeenNthCalledWith(1, - devices.remote, - 6, - devices.bulb, - expect.any(Function) - ); - expect(zigbee.unbind).toHaveBeenNthCalledWith(2, - devices.remote, - 8, - devices.bulb, - expect.any(Function) - ); - }); + it('Should unbind coordinator', async () => { + const device = zigbeeHerdsman.devices.remote; + const target = zigbeeHerdsman.devices.coordinator.getEndpoint(1); + const endpoint = device.getEndpoint(1); + mockClear(device); + endpoint.unbind.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/unbind/remote', 'coordinator'); + await flushPromises(); + expect(endpoint.unbind).toHaveBeenCalledTimes(3); + expect(endpoint.unbind).toHaveBeenCalledWith("genOnOff", target); + expect(endpoint.unbind).toHaveBeenCalledWith("genLevelCtrl", target); + expect(endpoint.unbind).toHaveBeenCalledWith("genScenes", target); + expect(MQTT.publish).toHaveBeenCalledTimes(3); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'Coordinator', cluster: 'genScenes'}}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'Coordinator', cluster: 'genOnOff'}}); + expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_unbind', message: {from: 'remote', to: 'Coordinator', cluster: 'genLevelCtrl'}}); + }); + + it('Should bind to groups', async () => { + const device = zigbeeHerdsman.devices.remote; + const target = zigbeeHerdsman.groups.group_1; + const endpoint = device.getEndpoint(1); + mockClear(device); + MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'group_1'); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(3); + expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); + expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); + expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); + expect(MQTT.publish).toHaveBeenCalledTimes(3); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genOnOff'}}); + expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genLevelCtrl'}}); + }); + + it('Should bind to group by number', async () => { + const device = zigbeeHerdsman.devices.remote; + const target = zigbeeHerdsman.groups.group_1; + const endpoint = device.getEndpoint(1); + mockClear(device); + MQTT.events.message('zigbee2mqtt/bridge/bind/remote', '1'); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(3); + expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); + expect(endpoint.bind).toHaveBeenCalledWith("genLevelCtrl", target); + expect(endpoint.bind).toHaveBeenCalledWith("genScenes", target); + expect(MQTT.publish).toHaveBeenCalledTimes(3); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genScenes'}}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genOnOff'}}); + expect(MQTT.publish.mock.calls[2][0]).toStrictEqual('zigbee2mqtt/bridge/log'); + expect(JSON.parse(MQTT.publish.mock.calls[2][1])).toStrictEqual({type: 'device_bind', message: {from: 'remote', to: 'group_1', cluster: 'genLevelCtrl'}}); + }); + + it('Should log when bind fails', async () => { + logger.error.mockClear(); + const device = zigbeeHerdsman.devices.remote; + const endpoint = device.getEndpoint(1); + mockClear(device); + endpoint.bind.mockImplementationOnce(() => {throw new Error('failed')}); + MQTT.events.message('zigbee2mqtt/bridge/bind/remote', 'bulb_color'); + await flushPromises(); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error).toHaveBeenCalledWith("Failed to bind cluster 'genScenes' from 'remote' to 'bulb_color' (Error: failed)"); + expect(endpoint.bind).toHaveBeenCalledTimes(3); + }); + + it('Should bind from non default endpoints', async () => { + const device = zigbeeHerdsman.devices.remote; + const target = zigbeeHerdsman.devices.QBKG03LM.getEndpoint(3); + const endpoint = device.getEndpoint(2); + mockClear(device); + MQTT.events.message('zigbee2mqtt/bridge/bind/remote/ep2', 'wall_switch_double/right'); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(1); + expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); + }); + + it('Should bind to default endpoint returned by endpoints()', async () => { + const device = zigbeeHerdsman.devices.remote; + const target = zigbeeHerdsman.devices.QBKG04LM.getEndpoint(2); + const endpoint = device.getEndpoint(2); + mockClear(device); + MQTT.events.message('zigbee2mqtt/bridge/bind/remote/ep2', 'wall_switch'); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(1); + expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target); }); }); diff --git a/test/deviceConfigure.test.js b/test/deviceConfigure.test.js new file mode 100644 index 000000000..778f7e59c --- /dev/null +++ b/test/deviceConfigure.test.js @@ -0,0 +1,144 @@ +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +const MQTT = require('./stub/mqtt'); +const settings = require('../lib/util/settings'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const mocksClear = [MQTT.publish, logger.warn, logger.debug]; + +describe('Device receive', () => { + let controller; + + expectRemoteConfigured = () => { + const device = zigbeeHerdsman.devices.remote; + const endpoint1 = device.getEndpoint(1); + expect(endpoint1.bind).toHaveBeenCalledTimes(2); + expect(endpoint1.bind).toHaveBeenCalledWith('genOnOff', this.coordinatorEndoint); + const endpoint2 = device.getEndpoint(2); + expect(endpoint2.write).toHaveBeenCalledTimes(1); + expect(endpoint2.write).toHaveBeenCalledWith("genBasic", {"49": {"type": 25, "value": 11}}, {"disableDefaultResponse": true, "manufacturerCode": 4107}); + expect(device.meta.configured).toBe(1); + } + + expectRemoteNotConfigured = () => { + const device = zigbeeHerdsman.devices.remote; + const endpoint1 = device.getEndpoint(1); + expect(endpoint1.bind).toHaveBeenCalledTimes(0); + } + + mockClear = (device) => { + for (const endpoint of device.endpoints) { + endpoint.read.mockClear(); + endpoint.write.mockClear(); + endpoint.configureReporting.mockClear(); + endpoint.bind.mockClear(); + } + } + + beforeEach(async () => { + jest.useRealTimers(); + data.writeDefaultConfiguration(); + settings._reRead(); + data.writeEmptyState(); + controller = new Controller(); + await controller.start(); + mocksClear.forEach((m) => m.mockClear()); + await flushPromises(); + this.coordinatorEndoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); + }); + + it('Should configure on startup', async () => { + expectRemoteConfigured(); + }); + + it('Should not configure twice', async () => { + expectRemoteConfigured(); + const device = zigbeeHerdsman.devices.remote; + const endpoint = device.getEndpoint(1); + mockClear(device); + const payload = {data: {zclVersion: 1}, cluster: 'genBasic', device, endpoint, type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(0); + }); + + it('Should configure on zigbee message when not configured yet', async () => { + const device = zigbeeHerdsman.devices.remote; + delete device.meta.configured; + const endpoint = device.getEndpoint(1); + mockClear(device); + const payload = {data: {zclVersion: 1}, cluster: 'genBasic', device, endpoint, type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expectRemoteConfigured(); + }); + + it('Should not configure when interviewing', async () => { + const device = zigbeeHerdsman.devices.remote; + delete device.meta.configured; + device.interviewing = true; + const endpoint = device.getEndpoint(1); + mockClear(device); + const payload = {data: {zclVersion: 1}, cluster: 'genBasic', device, endpoint, type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expectRemoteNotConfigured(); + device.interviewing = false; + }); + + it('Should not configure when not interviewCompleted', async () => { + const device = zigbeeHerdsman.devices.remote; + delete device.meta.configured; + device.interviewCompleted = false; + const endpoint = device.getEndpoint(1); + mockClear(device); + const payload = {data: {zclVersion: 1}, cluster: 'genBasic', device, endpoint, type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expectRemoteNotConfigured(); + device.interviewCompleted = true; + }); + + it('Should not configure when already configuring', async () => { + const device = zigbeeHerdsman.devices.remote; + delete device.meta.configured; + const endpoint = device.getEndpoint(1); + endpoint.bind.mockImplementationOnce(async () => await wait(500)); + mockClear(device); + const payload = {data: {zclVersion: 1}, cluster: 'genBasic', device, endpoint, type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(1); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(1); + }); + + it('Should configure max 3 times when fails', async () => { + controller.extensions.find((e) => e.constructor.name === 'DeviceConfigure').attempts = {}; + const device = zigbeeHerdsman.devices.remote; + delete device.meta.configured; + const endpoint = device.getEndpoint(1); + mockClear(device); + endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')}); + const payload = {data: {zclVersion: 1}, cluster: 'genBasic', device, endpoint, type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')}); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')}); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')}); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + endpoint.bind.mockImplementationOnce(async () => {throw new Error('BLA')}); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(3); + }); +}); diff --git a/test/devicePublish.test.js b/test/devicePublish.test.js deleted file mode 100644 index 5e59c64b6..000000000 --- a/test/devicePublish.test.js +++ /dev/null @@ -1,1376 +0,0 @@ -const DevicePublish = require('../lib/extension/devicePublish'); -const settings = require('../lib/util/settings'); -const utils = require('./utils'); - -const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - -const mqtt = { - subscribe: (topic) => {}, -}; - -const zigbee = { - getDevice: null, - publish: jest.fn((entityID, entityType, cid, cmd, cmdType, zclData, cfg, ep, callback) => { - callback(false, null); - }), -}; - -const state = { - get: jest.fn(), -}; - -const publishEntityState = jest.fn(); - -const cfg = { - default: { - manufSpec: 0, - disDefaultRsp: 0, - }, - disFeedbackRsp: { - manufSpec: 0, - disDefaultRsp: 0, - disFeedbackRsp: true, - }, -}; - -describe('DevicePublish', () => { - let devicePublish; - - beforeEach(() => { - utils.stubLogger(jest); - devicePublish = new DevicePublish(zigbee, mqtt, state, publishEntityState); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - /* eslint-disable jest/no-identical-title */ // TODO: FIXME - describe('Parse topic', () => { - it('Should publish messages to zigbee devices', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({brightness: '200'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000001', - 'device', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 200, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000001', - {state: 'ON', brightness: 200}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - }); - - it('Should publish messages to zigbee devices', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x00000002'); - zigbee.getDevice = () => ({modelId: 'LCT003'}); - devicePublish.onMQTTMessage('zigbee2mqtt/wohnzimmer.light.wall.right/set', JSON.stringify({state: 'ON'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000002', - 'device', - 'genOnOff', - 'on', - 'functional', - {}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000002', - {state: 'ON'}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - }); - - it('Should publish messages to zigbee devices when brightness is in %', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify({brightness_percent: '92'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000003', - 'device', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 235, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000003', - {state: 'ON', brightness: 235}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - } - ); - - it('Should publish messages to zigbee devices when brightness is in number', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000004/set', JSON.stringify({brightness: 230})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000004', - 'device', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 230, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000004', - {state: 'ON', brightness: 230}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - } - ); - - it('Should publish messages to zigbee devices with color_temp', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 WS opal 980lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000005/set', JSON.stringify({color_temp: '222'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000005', - 'device', - 'lightingColorCtrl', - 'moveToColorTemp', - 'functional', - {colortemp: 222, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000005', - {color_temp: 222}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - }); - - it('Should publish messages to zigbee devices with color_temp in %', async () => { - publishEntityState.mockClear(); - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 WS opal 980lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000006/set', JSON.stringify({color_temp_percent: '100'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000006', - 'device', - 'lightingColorCtrl', - 'moveToColorTemp', - 'functional', - {colortemp: 500, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000006', - {color_temp: 500}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - } - ); - - it('Should publish messages to zigbee devices with non-default ep', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral1'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000007/set', JSON.stringify({state: 'OFF'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000007', - 'device', - 'genOnOff', - 'off', - 'functional', - {}, - cfg.default, - 2, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000007', - {state: 'OFF'}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - } - ); - - it('Should publish messages to zigbee devices with non-default ep and postfix', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000008/right/set', JSON.stringify({state: 'OFF'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000008', - 'device', - 'genOnOff', - 'off', - 'functional', - {}, - cfg.default, - 3, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000008', - {state_right: 'OFF'}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - } - ); - - it('Should publish messages to zigbee gledopto with [11,13]', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'GLEDOPTO', epList: [11, 13]}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000009/set', JSON.stringify({state: 'OFF'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000009', - 'device', - 'genOnOff', - 'off', - 'functional', - {}, - cfg.default, - 11, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000009', - {state: 'OFF'}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - }); - - it('Should publish messages to zigbee gledopto with [11,12,13]', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'GLEDOPTO', epList: [11, 12, 13]}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000010/set', JSON.stringify({state: 'OFF', brightness: 50})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000010', - 'device', - 'genOnOff', - 'off', - 'functional', - {}, - cfg.default, - 12, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000010', - {state: 'OFF'}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - } - ); - - it('Should publish messages to zigbee devices with color xy', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000011/set', JSON.stringify({color: {x: 100, y: 50}})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000011', - 'device', - 'lightingColorCtrl', - 'moveToColor', - 'functional', - {colorx: 6553500, colory: 3276750, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000011', - {color: {x: 100, y: 50}}); - }); - - it('Should publish messages to zigbee devices with color xy and state', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - const payload = {color: {x: 100, y: 50}, state: 'ON'}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000012/set', JSON.stringify(payload)); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000012', - 'device', - 'genOnOff', - 'on', - 'functional', - {}, - cfg.default, - null, - expect.any(Function)); - expect(zigbee.publish).toHaveBeenNthCalledWith(2, - '0x00000012', - 'device', - 'lightingColorCtrl', - 'moveToColor', - 'functional', - {colorx: 6553500, colory: 3276750, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(2); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000012', - {state: 'ON'}); - expect(publishEntityState).toHaveBeenNthCalledWith(2, - '0x00000012', - {color: {x: 100, y: 50}}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - } - ); - - it('Should publish messages to zigbee devices with color xy and brightness', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - const payload = {color: {x: 100, y: 50}, brightness: 20}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000013/set', JSON.stringify(payload)); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000013', - 'device', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 20, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(zigbee.publish).toHaveBeenNthCalledWith(2, - '0x00000013', - 'device', - 'lightingColorCtrl', - 'moveToColor', - 'functional', - {colorx: 6553500, colory: 3276750, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(publishEntityState).toHaveBeenCalledTimes(2); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000013', - {state: 'ON', brightness: 20}); - expect(publishEntityState).toHaveBeenNthCalledWith(2, - '0x00000013', - {color: {x: 100, y: 50}}); - } - ); - - it('Should publish messages to zigbee devices with color xy, brightness and state on', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - const payload = {color: {x: 100, y: 50}, brightness: 20, state: 'oN'}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000014/set', JSON.stringify(payload)); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000014', - 'device', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 20, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(zigbee.publish).toHaveBeenNthCalledWith(2, - '0x00000014', - 'device', - 'lightingColorCtrl', - 'moveToColor', - 'functional', - {colorx: 6553500, colory: 3276750, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(publishEntityState).toHaveBeenCalledTimes(2); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000014', - {state: 'ON', brightness: 20}); - expect(publishEntityState).toHaveBeenNthCalledWith(2, - '0x00000014', - {color: {x: 100, y: 50}}); - } - ); - - it('Should publish messages to zigbee devices with color xy, brightness and state off', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - const payload = {color: {x: 100, y: 50}, brightness: 20, state: 'oFF'}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000015/set', JSON.stringify(payload)); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000015', - 'device', - 'genOnOff', - 'off', - 'functional', - {}, - cfg.default, - null, - expect.any(Function)); - expect(zigbee.publish).toHaveBeenNthCalledWith(2, - '0x00000015', - 'device', - 'lightingColorCtrl', - 'moveToColor', - 'functional', - {colorx: 6553500, colory: 3276750, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(2); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000015', - {state: 'OFF'}); - expect(publishEntityState).toHaveBeenNthCalledWith(2, - '0x00000015', - {color: {x: 100, y: 50}}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - } - ); - - it('Should publish messages to zigbee devices with color rgb', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000016/set', JSON.stringify({color: {r: 100, g: 200, b: 10}})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000016', - 'device', - 'lightingColorCtrl', - 'moveToColor', - 'functional', - {colorx: 17806, colory: 43155, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000016', - {color: {x: 0.2717, y: 0.6585}}); - }); - - it('Should publish messages to zigbee devices with color rgb string', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000017/set', JSON.stringify({color: {rgb: '100,200,10'}})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000017', - 'device', - 'lightingColorCtrl', - 'moveToColor', - 'functional', - {colorx: 17806, colory: 43155, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000017', - {color: {x: 0.2717, y: 0.6585}}); - } - ); - - it('Should publish 1 message when brightness with state is send', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000018/set', JSON.stringify({state: 'ON', brightness: '50'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000018', - 'device', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 50, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000018', - {state: 'ON', brightness: 50}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - } - ); - - it('Should publish messages to groups', async () => { - jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue('1'); - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({state: 'ON'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - 1, - 'group', - 'genOnOff', - 'on', - 'functional', - {}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - 1, - {state: 'ON'}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - }); - - it('Should publish messages to groups with brightness_percent', async () => { - jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue('1'); - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({brightness_percent: 50})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - 1, - 'group', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 127, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - 1, - {state: 'ON', brightness: 127}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - }); - - it('Should publish messages to groups with on and brightness', async () => { - jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue('1'); - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({state: 'ON', brightness: 50})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - 1, - 'group', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 50, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - 1, - {state: 'ON', brightness: 50}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - }); - - it('Should publish messages to groups with off and brightness', async () => { - jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue('1'); - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({state: 'OFF', brightness: 5})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - 1, - 'group', - 'genOnOff', - 'off', - 'functional', - {}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - 1, - {state: 'OFF'}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - }); - - it('Should publish messages to groups color', async () => { - jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue('1'); - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({color: {x: 0.37, y: 0.28}})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - 1, - 'group', - 'lightingColorCtrl', - 'moveToColor', - 'functional', - {colorx: 24248, colory: 18350, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - 1, - {color: {x: 0.37, y: 0.28}}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - }); - - it('Should publish messages to groups color temperature', async () => { - jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue('1'); - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - devicePublish.onMQTTMessage('zigbee2mqtt/group/group_1/set', JSON.stringify({color_temp: 100})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - 1, - 'group', - 'lightingColorCtrl', - 'moveToColorTemp', - 'functional', - {colortemp: 100, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - 1, - {color_temp: 100}); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - }); - }); - - describe('Parse topic', () => { - it('Should handle non-valid topics', () => { - const topic = 'zigbee2mqtt1/my_device_id/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed).toBeNull(); - }); - - it('Should handle non-valid topics', () => { - const topic = 'zigbee2mqtt1/my_device_id/sett'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed).toBeNull(); - }); - - it('Should handle non-valid topics', () => { - const topic = 'zigbee2mqtt/my_device_id/write'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed).toBeNull(); - }); - - it('Should handle non-valid topics', () => { - const topic = 'zigbee2mqtt/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed).toBeNull(); - }); - - it('Should handle non-valid topics', () => { - const topic = 'set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed).toBeNull(); - }); - - it('Should parse set topic', () => { - const topic = 'zigbee2mqtt/my_device_id/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('my_device_id'); - expect(parsed.postfix).toBe(''); - expect(parsed.attribute).toBeUndefined(); - }); - - it('Should parse get topic', () => { - const topic = 'zigbee2mqtt/my_device_id2/get'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('get'); - expect(parsed.ID).toBe('my_device_id2'); - expect(parsed.postfix).toBe(''); - expect(parsed.attribute).toBeUndefined(); - }); - - - it('Should not respond to bridge/config/devices/get', () => { - const topic = 'zigbee2mqtt/bridge/config/devices/get'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed).toBeNull(); - }); - - it('Should not respond to bridge/config/devices/set', () => { - const topic = 'zigbee2mqtt/bridge/config/devices/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed).toBeNull(); - }); - - - it('Should not respond to bridge/config/devices', () => { - const topic = 'zigbee2mqtt/bridge/config/devices'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed).toBeNull(); - }); - - - it('Should parse topic with when base topic has multiple slashes', () => { - jest.spyOn(settings, 'get').mockReturnValue({ - mqtt: { - base_topic: 'zigbee2mqtt/at/my/home', - }, - }); - - const topic = 'zigbee2mqtt/at/my/home/my_device_id2/get'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('get'); - expect(parsed.ID).toBe('my_device_id2'); - expect(parsed.postfix).toBe(''); - expect(parsed.attribute).toBeUndefined(); - }); - - it('Should parse topic with when deviceID has multiple slashes', () => { - const topic = 'zigbee2mqtt/floor0/basement/my_device_id2/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('floor0/basement/my_device_id2'); - expect(parsed.postfix).toBe(''); - expect(parsed.attribute).toBeUndefined(); - }); - - it('Should parse topic with when base and deviceID have multiple slashes', () => { - jest.spyOn(settings, 'get').mockReturnValue({ - mqtt: { - base_topic: 'zigbee2mqtt/at/my/basement', - }, - }); - - const topic = 'zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('floor0/basement/my_device_id2'); - expect(parsed.postfix).toBe(''); - expect(parsed.attribute).toBeUndefined(); - } - ); - - it('Should parse set with attribute topic', () => { - const topic = 'zigbee2mqtt/0x12345689/set/foobar'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('0x12345689'); - expect(parsed.postfix).toBe(''); - expect(parsed.attribute).toBe('foobar'); - }); - - it('Should parse set with ieeeAddr topic', () => { - const topic = 'zigbee2mqtt/0x12345689/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('0x12345689'); - expect(parsed.postfix).toBe(''); - expect(parsed.attribute).toBeUndefined(); - }); - - it('Should parse set with postfix topic', () => { - const topic = 'zigbee2mqtt/0x12345689/left/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('0x12345689'); - expect(parsed.postfix).toBe('left'); - expect(parsed.attribute).toBeUndefined(); - }); - - it('Should parse set with almost postfix topic', () => { - const topic = 'zigbee2mqtt/wohnzimmer.light.wall.right/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('wohnzimmer.light.wall.right'); - expect(parsed.postfix).toBe(''); - expect(parsed.attribute).toBeUndefined(); - }); - - it('Should parse set with postfix topic', () => { - const topic = 'zigbee2mqtt/0x12345689/right/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('0x12345689'); - expect(parsed.postfix).toBe('right'); - expect(parsed.attribute).toBeUndefined(); - }); - - it('Should parse set with postfix topic', () => { - const topic = 'zigbee2mqtt/0x12345689/bottom_left/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('0x12345689'); - expect(parsed.postfix).toBe('bottom_left'); - expect(parsed.attribute).toBeUndefined(); - }); - - it('Shouldnt parse set with invalid postfix topic', () => { - const topic = 'zigbee2mqtt/0x12345689/invalid/set'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('0x12345689/invalid'); - expect(parsed.postfix).toBe(''); - expect(parsed.attribute).toBeUndefined(); - }); - - it('Should parse set with postfix topic and attribute', () => { - const topic = 'zigbee2mqtt/0x12345689/bottom_left/set/foobar'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('set'); - expect(parsed.ID).toBe('0x12345689'); - expect(parsed.postfix).toBe('bottom_left'); - expect(parsed.attribute).toBe('foobar'); - }); - - it('Should parse set with and slashes in base and deviceID postfix topic', () => { - jest.spyOn(settings, 'get').mockReturnValue({ - mqtt: { - base_topic: 'zigbee2mqtt/at/my/home', - }, - }); - - const topic = 'zigbee2mqtt/at/my/home/my/device/in/basement/sensor/bottom_left/get'; - const parsed = devicePublish.parseTopic(topic); - expect(parsed.type).toBe('get'); - expect(parsed.ID).toBe('my/device/in/basement/sensor'); - expect(parsed.postfix).toBe('bottom_left'); - expect(parsed.attribute).toBeUndefined(); - } - ); - }); - - it('Should not publish messages to zigbee devices when payload is invalid', async () => { - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral1'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000019/set', JSON.stringify({state: true})); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(0); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000019/set', JSON.stringify({state: 1})); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(0); - } - ); - - it('Should set state before color', async () => { - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'LCT001'}); - const msg = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000020/set', JSON.stringify(msg)); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish.mock.calls[0][2]).toBe('genOnOff'); - expect(zigbee.publish.mock.calls[0][3]).toBe('on'); - expect(zigbee.publish.mock.calls[1][2]).toBe('lightingColorCtrl'); - expect(zigbee.publish.mock.calls[1][3]).toBe('moveToColor'); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - }); - - it('Should read after write when enabled', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'LCT001'}); - jest.spyOn(settings, 'getDevice').mockReturnValue({retrieve_state: true}); - const msg = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000020/set', JSON.stringify(msg)); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish.mock.calls[0][2]).toBe('genOnOff'); - expect(zigbee.publish.mock.calls[0][3]).toBe('on'); - expect(zigbee.publish.mock.calls[1][2]).toBe('lightingColorCtrl'); - expect(zigbee.publish.mock.calls[1][3]).toBe('moveToColor'); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(5); - expect(zigbee.publish.mock.calls[2][2]).toBe('genOnOff'); - expect(zigbee.publish.mock.calls[2][3]).toBe('read'); - expect(zigbee.publish.mock.calls[3][2]).toBe('genLevelCtrl'); - expect(zigbee.publish.mock.calls[3][3]).toBe('read'); - expect(zigbee.publish.mock.calls[4][2]).toBe('lightingColorCtrl'); - expect(zigbee.publish.mock.calls[4][3]).toBe('read'); - expect(publishEntityState).toHaveBeenCalledTimes(2); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000020', - {state: 'ON'}); - expect(publishEntityState).toHaveBeenNthCalledWith(2, - '0x00000020', - {color: {x: 0.701, y: 0.299}}); - }); - - it('Should use transition when enabled', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'LCT001'}); - jest.spyOn(settings, 'getDevice').mockReturnValue({transition: 20}); - const msg = {'brightness': 200}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000020/set', JSON.stringify(msg)); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish.mock.calls[0][2]).toBe('genLevelCtrl'); - expect(zigbee.publish.mock.calls[0][3]).toBe('moveToLevelWithOnOff'); - expect(zigbee.publish.mock.calls[0][5]).toStrictEqual({level: 200, transtime: 200}); - }); - - it('Should use transition when color temp', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'LCT001'}); - jest.spyOn(settings, 'getDevice').mockReturnValue({transition: 20}); - const msg = {'color_temp': 200}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000020/set', JSON.stringify(msg)); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish.mock.calls[0][2]).toBe('lightingColorCtrl'); - expect(zigbee.publish.mock.calls[0][3]).toBe('moveToColorTemp'); - expect(zigbee.publish.mock.calls[0][5]).toStrictEqual({colortemp: 200, transtime: 200}); - }); - - it('Message transition should overrule options transition', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'LCT001'}); - jest.spyOn(settings, 'getDevice').mockReturnValue({transition: 20}); - const msg = {'brightness': 200, 'transition': 10}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000020/set', JSON.stringify(msg)); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish.mock.calls[0][2]).toBe('genLevelCtrl'); - expect(zigbee.publish.mock.calls[0][3]).toBe('moveToLevelWithOnOff'); - expect(zigbee.publish.mock.calls[0][5]).toStrictEqual({level: 200, transtime: 100}); - }); - - it('Should set state with brightness before color', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'LCT001'}); - const msg = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}, 'transition': 3, 'brightness': 100}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000021/set', JSON.stringify(msg)); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish.mock.calls[0][2]).toBe('genLevelCtrl'); - expect(zigbee.publish.mock.calls[1][2]).toBe('lightingColorCtrl'); - await wait(10); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(publishEntityState).toHaveBeenCalledTimes(2); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000021', - {state: 'ON', brightness: 100}); - expect(publishEntityState).toHaveBeenNthCalledWith(2, - '0x00000021', - {color: {x: 0.701, y: 0.299}}); - }); - - it('Should turn device off when brightness 0 is send', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({brightness: 0})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000001', - 'device', - 'genOnOff', - 'off', - 'functional', - {}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000001', - {state: 'OFF', brightness: 0}); - }); - - it('Should turn device off when brightness 0 is send with light_brightness converter', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'FB56+ZSC05HG1.0'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({brightness: 0})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000001', - 'device', - 'genOnOff', - 'off', - 'functional', - {}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000001', - {state: 'OFF', brightness: 0}); - }); - - it('Specifc ZNCLDJ11LM test', async () => { - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.curtain'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({state: 'OPEN'})); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000001', 'device', 'genAnalogOutput', 'write', - 'foundation', [{attrId: 0x0055, dataType: 0x39, attrData: 100}], cfg.default, null, expect.any(Function) - ); - - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({position: 10})); - expect(zigbee.publish).toHaveBeenNthCalledWith(2, - '0x00000001', 'device', 'genAnalogOutput', 'write', - 'foundation', [{attrId: 0x0055, dataType: 0x39, attrData: 10}], cfg.default, null, expect.any(Function) - ); - - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({state: 'CLOSE'})); - expect(zigbee.publish).toHaveBeenNthCalledWith(3, - '0x00000001', 'device', 'genAnalogOutput', 'write', - 'foundation', [{attrId: 0x0055, dataType: 0x39, attrData: 0}], cfg.default, null, expect.any(Function) - ); - - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({state: 'STOP'})); - expect(zigbee.publish).toHaveBeenNthCalledWith(4, - '0x00000001', 'device', 'closuresWindowCovering', 'stop', - 'functional', {}, cfg.default, null, expect.any(Function) - ); - }); - - it('Should turn device off when brightness 0 is send with light_brightness converter', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({state: 'ON', transition: 1})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000001', - 'device', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 255, transtime: 10}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000001', - {state: 'ON', brightness: 255}); - }); - - it('Should turn device off when brightness 0 is send with light_brightness converter', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({state: 'OFF', transition: 2})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000001', - 'device', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 0, transtime: 20}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000001', - {state: 'OFF', brightness: 0}); - }); - - it('Home Assistant: should set state', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - jest.spyOn(settings, 'get').mockReturnValue({ - homeassistant: true, - mqtt: {base_topic: 'zigbee2mqtt'}, - advanced: {last_seen: 'disable'}, - }); - zigbee.getDevice = () => ({modelId: 'RB 185 C'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({state: 'ON'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000001', - 'device', - 'genOnOff', - 'on', - 'functional', - {}, - cfg.default, - null, - expect.any(Function)); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000001', - {state: 'ON'}); - }); - - it('Home Assistant: should not set state when color temperature is also set', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - jest.spyOn(state, 'get').mockReturnValue({state: 'ON'}); - jest.spyOn(settings, 'get').mockReturnValue({ - homeassistant: true, - mqtt: {base_topic: 'zigbee2mqtt'}, - advanced: {last_seen: 'disable'}, - }); - zigbee.getDevice = () => ({modelId: 'RB 185 C'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({state: 'ON', color_temp: 100})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000001', - 'device', - 'lightingColorCtrl', - 'moveToColorTemp', - 'functional', - {colortemp: 100, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000001', - {color_temp: 100}); - }); - - it('Home Assistant: should not set state when color is also set', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - jest.spyOn(state, 'get').mockReturnValue({state: 'ON'}); - jest.spyOn(settings, 'get').mockReturnValue({ - homeassistant: true, - mqtt: {base_topic: 'zigbee2mqtt'}, - advanced: {last_seen: 'disable'}, - }); - zigbee.getDevice = () => ({modelId: 'RB 185 C'}); - devicePublish.onMQTTMessage( - 'zigbee2mqtt/0x00000001/set', - JSON.stringify({state: 'ON', color: {x: 0.41, y: 0.25}}) - ); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000001', - 'device', - 'lightingColorCtrl', - 'moveToColor', - 'functional', - {colorx: 26869, colory: 16384, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000001', - {color: {x: 0.41, y: 0.25}}); - }); - - it('Home Assistant: should set state when color is also set and bulb is off', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - jest.spyOn(state, 'get').mockReturnValue({state: 'OFF'}); - jest.spyOn(settings, 'get').mockReturnValue({ - homeassistant: true, - mqtt: {base_topic: 'zigbee2mqtt'}, - advanced: {last_seen: 'disable'}, - }); - zigbee.getDevice = () => ({modelId: 'RB 185 C'}); - devicePublish.onMQTTMessage( - 'zigbee2mqtt/0x00000001/set', - JSON.stringify({state: 'ON', color: {x: 0.41, y: 0.25}}) - ); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000001', - 'device', - 'genOnOff', - 'on', - 'functional', - {}, - cfg.default, - null, - expect.any(Function)); - expect(zigbee.publish).toHaveBeenNthCalledWith(2, - '0x00000001', - 'device', - 'lightingColorCtrl', - 'moveToColor', - 'functional', - {colorx: 26869, colory: 16384, transtime: 0}, - cfg.default, - null, - expect.any(Function)); - - expect(publishEntityState).toHaveBeenCalledTimes(2); - expect(publishEntityState).toHaveBeenNthCalledWith(1, - '0x00000001', - {state: 'ON'}); - expect(publishEntityState).toHaveBeenNthCalledWith(2, - '0x00000001', - {color: {x: 0.41, y: 0.25}}); - }); - - it('Should publish message with disFeedbackRsp when set', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'HDC52EastwindFan'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify({brightness: '92'})); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000003', - 'device', - 'genLevelCtrl', - 'moveToLevelWithOnOff', - 'functional', - {level: 92, transtime: 0}, - cfg.disFeedbackRsp, - null, - expect.any(Function)); - - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify({state: 'OFF'})); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenNthCalledWith(2, - '0x00000003', - 'device', - 'genOnOff', - 'off', - 'functional', - {}, - cfg.disFeedbackRsp, - null, - expect.any(Function)); - }); - - it('Should publish messages to zigbee devices', async () => { - jest.spyOn(settings, 'get').mockReturnValue({ - mqtt: {base_topic: 'zigbee2mqtt'}, - advanced: {last_seen: 'ISO_8601'}, - }); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'TRADFRI bulb E27 CWS opal 600lm'}); - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000001/set', JSON.stringify({brightness: '200'})); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(typeof publishEntityState.mock.calls[0][1].last_seen).toBe('string'); - }); - - it('HS2WD-E burglar warning', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'WarningDevice'}); - const payload = {warning: {duration: 100, mode: 'burglar', strobe: true, level: 'high'}}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify(payload)); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000003', - 'device', - 'ssIasWd', - 'startWarning', - 'functional', - {startwarninginfo: 22, warningduration: 100}, - cfg.default, - null, - expect.any(Function)); - }); - - it('HS2WD-E emergency warning', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'WarningDevice'}); - const payload = {warning: {duration: 10, mode: 'emergency', strobe: false, level: 'very_high'}}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify(payload)); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000003', - 'device', - 'ssIasWd', - 'startWarning', - 'functional', - {startwarninginfo: 51, warningduration: 10}, - cfg.default, - null, - expect.any(Function)); - }); - - it('HS2WD-E emergency without level', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'WarningDevice'}); - const payload = {warning: {duration: 10, mode: 'emergency', strobe: false}}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify(payload)); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000003', - 'device', - 'ssIasWd', - 'startWarning', - 'functional', - {startwarninginfo: 49, warningduration: 10}, - cfg.default, - null, - expect.any(Function)); - }); - - it('HS2WD-E wrong payload (should use defaults)', async () => { - zigbee.publish.mockClear(); - publishEntityState.mockClear(); - zigbee.getDevice = () => ({modelId: 'WarningDevice'}); - const payload = {warning: 'wrong'}; - devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify(payload)); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenNthCalledWith(1, - '0x00000003', - 'device', - 'ssIasWd', - 'startWarning', - 'functional', - {startwarninginfo: 53, warningduration: 10}, - cfg.default, - null, - expect.any(Function)); - }); -}); diff --git a/test/deviceReceive.test.js b/test/deviceReceive.test.js index 8465fdeb0..c2a1e2836 100644 --- a/test/deviceReceive.test.js +++ b/test/deviceReceive.test.js @@ -1,278 +1,377 @@ -const DeviceReceive = require('../lib/extension/deviceReceive'); +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +const MQTT = require('./stub/mqtt'); const settings = require('../lib/util/settings'); -const devices = require('zigbee-shepherd-converters').devices; -const utils = require('./utils'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); -const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); +const mocksClear = [MQTT.publish, logger.warn, logger.debug]; -// Devices -const WXKG11LM = devices.find((d) => d.model === 'WXKG11LM'); -const WXKG02LM = devices.find((d) => d.model === 'WXKG02LM'); -const WSDCGQ11LM = devices.find((d) => d.model === 'WSDCGQ11LM'); -const RTCGQ11LM = devices.find((d) => d.model === 'RTCGQ11LM'); -const ZNCZ02LM = devices.find((d) => d.model === 'ZNCZ02LM'); +describe('Device receive', () => { + let controller; -const mqtt = { - log: () => {}, -}; - -describe('DeviceReceive', () => { - let deviceReceive; - let publishEntityState; - - beforeEach(() => { - utils.stubLogger(jest); - jest.spyOn(settings, 'addDevice').mockReturnValue(undefined); - publishEntityState = jest.fn(); - deviceReceive = new DeviceReceive(null, mqtt, null, publishEntityState); + beforeEach(async () => { + jest.useRealTimers(); + data.writeDefaultConfiguration(); + settings._reRead(); + data.writeEmptyState(); + controller = new Controller(); + await controller.start(); + mocksClear.forEach((m) => m.mockClear()); }); - afterEach(() => { - jest.restoreAllMocks(); + it('Should handle a zigbee message', async () => { + const device = zigbeeHerdsman.devices.WXKG11LM; + const data = {onOff: 1} + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'single', linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); }); - describe('Handling zigbee messages', () => { - it('Should handle a zigbee message', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1); - deviceReceive.onZigbeeMessage(message, device, WXKG11LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({click: 'single'}); - }); - it('Should handle a zigbee message and counter it when Home Assistant integration is enabled', () => { - jest.spyOn(settings, 'get').mockReturnValue({homeassistant: true, advanced: {last_seen: 'disable'}}); - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1); - deviceReceive.onZigbeeMessage(message, device, WXKG11LM); - expect(publishEntityState).toHaveBeenCalledTimes(2); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({click: 'single'}); - expect(publishEntityState.mock.calls[1][1]).toStrictEqual({click: ''}); - }); + it('Should handle a zigbee message and counter it when Home Assistant integration is enabled', async () => { + settings.set(['homeassistant'], true); + const device = zigbeeHerdsman.devices.WXKG11LM; + const data = {onOff: 1} + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'single', linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/button'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({click: '', linkquality: 10}); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should handle a zigbee message which uses ep (left)', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1); - deviceReceive.onZigbeeMessage(message, device, WXKG02LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({click: 'left'}); - }); + it('Should handle a zigbee message which uses ep (left)', async () => { + const device = zigbeeHerdsman.devices.WXKG02LM; + const data = {onOff: 1} + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'left', linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should handle a zigbee message which uses ep (right)', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 2); - deviceReceive.onZigbeeMessage(message, device, WXKG02LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({click: 'right'}); - }); + it('Should handle a zigbee message which uses ep (right)', async () => { + const device = zigbeeHerdsman.devices.WXKG02LM; + const data = {onOff: 1} + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(2), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({click: 'right', linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should handle a zigbee message with default precision', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage( - device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1 - ); - deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: -0.85}); - }); + it('Should handle a zigbee message with default precision', async () => { + const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const data = {measuredValue: -85} + const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.85, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + }); - it('Should debounce messages', async () => { - const device = {ieeeAddr: '0x12345678'}; - jest.spyOn(settings, 'getDevice').mockReturnValue({debounce: 0.1}); - const message1 = utils.zigbeeMessage( - device, 'msTemperatureMeasurement', 'attReport', {measuredValue: 8}, 1 - ); - const message2 = utils.zigbeeMessage( - device, 'msRelativeHumidity', 'attReport', {measuredValue: 1}, 1 - ); - const message3 = utils.zigbeeMessage( - device, 'msPressureMeasurement', 'attReport', {measuredValue: 2}, 1 - ); - deviceReceive.onZigbeeMessage(message1, device, WSDCGQ11LM); - deviceReceive.onZigbeeMessage(message2, device, WSDCGQ11LM); - deviceReceive.onZigbeeMessage(message3, device, WSDCGQ11LM); - await wait(200); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2}); + it('Should debounce messages', async () => { + jest.useFakeTimers(); + const device = zigbeeHerdsman.devices.WSDCGQ11LM; + settings.set(['devices', device.ieeeAddr, 'debounce'], 0.1); + const data1 = {measuredValue: 8} + const payload1 = {data: data1, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload1); + const data2 = {measuredValue: 1} + const payload2 = {data: data2, cluster: 'msRelativeHumidity', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload2); + const data3 = {measuredValue: 2} + const payload3 = {data: data3, cluster: 'msPressureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload3); + await flushPromises(); + jest.advanceTimersByTime(50); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + jest.runAllTimers(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + }); - deviceReceive.onZigbeeMessage(message1, device, WSDCGQ11LM); - await wait(200); - expect(publishEntityState).toHaveBeenCalledTimes(2); - expect(publishEntityState.mock.calls[1][1]).toStrictEqual({temperature: 0.08}); - }); + it('Should handle a zigbee message with 1 precision', async () => { + const device = zigbeeHerdsman.devices.WSDCGQ11LM; + settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 1); + const data = {measuredValue: -85} + const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + }); - it('Should handle a zigbee message with 1 precision', () => { - const device = {ieeeAddr: '0x12345678'}; - jest.spyOn(settings, 'getDevice').mockReturnValue({temperature_precision: 1}); - const message = utils.zigbeeMessage( - device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1 - ); - deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: -0.8}); - }); + it('Should handle a zigbee message with 0 precision', async () => { + const device = zigbeeHerdsman.devices.WSDCGQ11LM; + settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 0); + const data = {measuredValue: -85} + const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + }); - it('Should handle a zigbee message with 0 precision', () => { - const device = {ieeeAddr: '0x12345678'}; - jest.spyOn(settings, 'getDevice').mockReturnValue({temperature_precision: 0}); - const message = utils.zigbeeMessage( - device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1 - ); - deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: -1}); - }); + it('Should handle a zigbee message with 1 precision when set via device_options', async () => { + const device = zigbeeHerdsman.devices.WSDCGQ11LM; + settings.set(['device_options', 'temperature_precision'], 1); + const data = {measuredValue: -85} + const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -0.8, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + }); - it('Should handle a zigbee message with 1 precision when set via device_options', () => { - const device = {ieeeAddr: '0x12345678'}; - jest.spyOn(settings, 'get').mockReturnValue({ - device_options: { - temperature_precision: 1, - }, - advanced: { - last_seen: 'disable', - }, - }); - jest.spyOn(settings, 'getDevice').mockReturnValue({}); - const message = utils.zigbeeMessage( - device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1 - ); - deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: -0.8}); - } - ); + it('Should handle a zigbee message with 2 precision when overrides device_options', async () => { + const device = zigbeeHerdsman.devices.WSDCGQ11LM; + settings.set(['device_options', 'temperature_precision'], 1); + settings.set(['devices', device.ieeeAddr, 'temperature_precision'], 0); + const data = {measuredValue: -85} + const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/weather_sensor'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({temperature: -1, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 1, "retain": false}); + }); - it('Should handle a zigbee message with 2 precision when overrides device_options', () => { - const device = {ieeeAddr: '0x12345678'}; - jest.spyOn(settings, 'get').mockReturnValue({ - device_options: { - temperature_precision: 1, - }, - advanced: { - last_seen: 'disable', - }, - }); - jest.spyOn(settings, 'getDevice').mockReturnValue({ - temperature_precision: 2, - }); - const message = utils.zigbeeMessage( - device, 'msTemperatureMeasurement', 'attReport', {measuredValue: -85}, 1 - ); - deviceReceive.onZigbeeMessage(message, device, WSDCGQ11LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({temperature: -0.85}); - } - ); + it('Should handle a zigbee message with voltage 3010', async () => { + const device = zigbeeHerdsman.devices.WXKG02LM; + const data = {'65281': {'1': 3010}} + const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 100, voltage: 3010, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should handle a zigbee message with voltage 3010', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 3010}}, 1); - deviceReceive.onZigbeeMessage(message, device, WXKG02LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - const expected = {battery: 100, voltage: 3010}; - expect(publishEntityState.mock.calls[0][1]).toStrictEqual(expected); - }); + it('Should handle a zigbee message with voltage 2850', async () => { + const device = zigbeeHerdsman.devices.WXKG02LM; + const data = {'65281': {'1': 2850}} + const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 35, voltage: 2850, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should handle a zigbee message with voltage 2850', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 2850}}, 1); - deviceReceive.onZigbeeMessage(message, device, WXKG02LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - const expected = {battery: 35, voltage: 2850}; - expect(publishEntityState.mock.calls[0][1]).toStrictEqual(expected); - }); + it('Should handle a zigbee message with voltage 2650', async () => { + const device = zigbeeHerdsman.devices.WXKG02LM; + const data = {'65281': {'1': 2650}} + const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 14, voltage: 2650, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should handle a zigbee message with voltage 2650', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 2650}}, 1); - deviceReceive.onZigbeeMessage(message, device, WXKG02LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - const expected = {battery: 14, voltage: 2650}; - expect(publishEntityState.mock.calls[0][1]).toStrictEqual(expected); - }); + it('Should handle a zigbee message with voltage 2000', async () => { + const device = zigbeeHerdsman.devices.WXKG02LM; + const data = {'65281': {'1': 2000}} + const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({battery: 0, voltage: 2000, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should handle a zigbee message with voltage 2000', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', {'65281': {'1': 2000}}, 1); - deviceReceive.onZigbeeMessage(message, device, WXKG02LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - const expected = {battery: 0, voltage: 2000}; - expect(publishEntityState.mock.calls[0][1]).toStrictEqual(expected); - }); + it('Should publish 1 message when converted twice', async () => { + const device = zigbeeHerdsman.devices.RTCGQ11LM; + const data = {'65281': {'1': 3045, '3': 19, '4': 17320, '5': 35, '6': [0, 3], '10': 51107, '11': 381, '100': 0}} + const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/occupancy_sensor'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'battery': 100, 'illuminance': 381, 'voltage': 3045, linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should publish 1 message when converted twice', () => { - const device = {ieeeAddr: '0x12345678'}; - const payload = { - '65281': {'1': 3045, '3': 19, '4': 17320, '5': 35, '6': [0, 3], '10': 51107, '11': 381, '100': 0}, - }; - const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', payload, 1); - deviceReceive.onZigbeeMessage(message, device, RTCGQ11LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - const expected = {'battery': 100, 'illuminance': 381, 'voltage': 3045}; - expect(publishEntityState.mock.calls[0][1]).toStrictEqual(expected); - }); + it('Should publish 1 message when converted twice', async () => { + const device = zigbeeHerdsman.devices.RTCGQ11LM; + const data = {'9999': {'1': 3045}}; + const payload = {data, cluster: 'genBasic', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); - it('Should publish no message when converted without result', () => { - const device = {ieeeAddr: '0x12345678'}; - const payload = {'9999': {'1': 3045}}; - const message = utils.zigbeeMessage(device, 'genBasic', 'attReport', payload, 1); - deviceReceive.onZigbeeMessage(message, device, RTCGQ11LM); - expect(publishEntityState).toHaveBeenCalledTimes(0); - }); + it('Should publish last_seen epoch', async () => { + const device = zigbeeHerdsman.devices.WXKG02LM; + settings.set(['advanced', 'last_seen'], 'epoch'); + const data = {onOff: 1}; + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('number') + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should publish last_seen epoch', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1); - jest.spyOn(settings, 'get').mockReturnValue({ - advanced: { - last_seen: 'epoch', - }, - }); - deviceReceive.onZigbeeMessage(message, device, WXKG02LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(typeof publishEntityState.mock.calls[0][1].last_seen).toBe('number'); - }); + it('Should publish last_seen ISO_8601', async () => { + const device = zigbeeHerdsman.devices.WXKG02LM; + settings.set(['advanced', 'last_seen'], 'ISO_8601'); + const data = {onOff: 1}; + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string') + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should publish last_seen ISO_8601', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1); - jest.spyOn(settings, 'get').mockReturnValue({ - advanced: { - last_seen: 'ISO_8601', - }, - }); - deviceReceive.onZigbeeMessage(message, device, WXKG02LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(typeof publishEntityState.mock.calls[0][1].last_seen).toBe('string'); - }); + it('Should publish last_seen ISO_8601_local', async () => { + const device = zigbeeHerdsman.devices.WXKG02LM; + settings.set(['advanced', 'last_seen'], 'ISO_8601_local'); + const data = {onOff: 1}; + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/button_double_key'); + expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toBe('string') + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should publish last_seen ISO_8601_local', () => { - const device = {ieeeAddr: '0x12345678'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1); - jest.spyOn(settings, 'get').mockReturnValue({ - advanced: { - last_seen: 'ISO_8601_local', - }, - }); - deviceReceive.onZigbeeMessage(message, device, WXKG02LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(typeof publishEntityState.mock.calls[0][1].last_seen).toBe('string'); - }); + it('Should not handle messages forwarded Xiaomi messages', async () => { + const device = zigbeeHerdsman.devices.ZNCZ02LM; + const data = {onOff: 1}; + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10, groupID: 599}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); - it('Should not handle messages forwarded Xiaomi messages', () => { - const device = {ieeeAddr: '0x12345678', manufId: 4151, type: 'Router'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}, 1, 599); - deviceReceive.onZigbeeMessage(message, device, ZNCZ02LM); - expect(publishEntityState).toHaveBeenCalledTimes(0); - }); + it('Should handle messages from Xiaomi router devices', async () => { + const device = zigbeeHerdsman.devices.ZNCZ02LM; + const data = {onOff: 1}; + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 20}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/power_plug'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'state': 'ON', linkquality: 20}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); - it('Should handle messages from Xiaomi router devices', () => { - const device = {ieeeAddr: '0x12345678', manufId: 4151, type: 'Router'}; - const message = utils.zigbeeMessage(device, 'genOnOff', 'attReport', {onOff: 1}); - deviceReceive.onZigbeeMessage(message, device, ZNCZ02LM); - expect(publishEntityState).toHaveBeenCalledTimes(1); - expect(publishEntityState.mock.calls[0][1]).toStrictEqual({state: 'ON'}); - }); + it('Should not handle messages from coordinator', async () => { + const device = zigbeeHerdsman.devices.coordinator; + const data = {onOff: 1}; + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); + + it('Should not handle messages from unsupported devices', async () => { + const device = zigbeeHerdsman.devices.unsupported; + const data = {onOff: 1}; + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); + + it('Should not handle messages from still interviewing devices with unknown modelID', async () => { + const device = zigbeeHerdsman.devices.interviewing; + const data = {onOff: 1}; + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(logger.debug).toHaveBeenCalledWith(`Skipping message, modelID is undefined and still interviewing`); + }); + + it('Should handle a command', async () => { + const device = zigbeeHerdsman.devices.E1743; + const data = {}; + const payload = {data, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'click': 'brightness_stop', linkquality: 10}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should add elapsed', async () => { + settings.set(['advanced', 'elapsed'], true); + const device = zigbeeHerdsman.devices.E1743; + const payload = {data: {}, cluster: 'genLevelCtrl', device, endpoint: device.getEndpoint(1), type: 'commandStopWithOnOff'}; + const oldNow = Date.now; + Date.now = jest.fn() + Date.now.mockReturnValue(new Date(150)); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + Date.now.mockReturnValue(new Date(200)); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({'click': 'brightness_stop'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/ikea_onoff'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toMatchObject({'click': 'brightness_stop'}); + expect(JSON.parse(MQTT.publish.mock.calls[1][1]).elapsed).toBe(50); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false}); + Date.now = oldNow; + }); + + it('Should log when message is from supported device but has no converters', async () => { + const device = zigbeeHerdsman.devices.ZNCZ02LM; + const data = {inactiveText: 'hello'}; + const payload = {data, cluster: 'genBinaryOutput', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 20}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(logger.warn).toHaveBeenCalledWith('No converter available for \'ZNCZ02LM\' with cluster \'genBinaryOutput\' and type \'attributeReport\' and data \'{"inactiveText":"hello"}\''); + expect(logger.warn).toHaveBeenCalledWith(`Please see: https://www.zigbee2mqtt.io/how_tos/how_to_support_new_devices.html.`) + }); + + it('Should not log when message is from supported device but has no converters is readResponse', async () => { + const device = zigbeeHerdsman.devices.ZNCZ02LM; + const data = {inactiveText: 'hello'}; + const payload = {data, cluster: 'genBinaryOutput', device, endpoint: device.getEndpoint(1), type: 'readResponse', linkquality: 20}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + expect(logger.warn).toBeCalledTimes(0); }); }); diff --git a/test/deviceReport.test.js b/test/deviceReport.test.js new file mode 100644 index 000000000..e7869ed60 --- /dev/null +++ b/test/deviceReport.test.js @@ -0,0 +1,152 @@ +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3'); +zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); +zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b2'); +zigbeeHerdsman.returnDevices.push('0x0017880104e45553'); +zigbeeHerdsman.returnDevices.push('0x0017880104e45559'); +const MQTT = require('./stub/mqtt'); +const settings = require('../lib/util/settings'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const mocksClear = [MQTT.publish, logger.warn, logger.debug]; + +describe('Device report', () => { + let controller; + + function expectOnOffBrightnessColorReport(endpoint) { + const coordinatorEndpoint = zigbeeHerdsman.devices.coordinator.getEndpoint(1); + expect(endpoint.bind).toHaveBeenCalledTimes(3); + expect(endpoint.bind).toHaveBeenCalledWith('genOnOff', coordinatorEndpoint); + expect(endpoint.bind).toHaveBeenCalledWith('genLevelCtrl', coordinatorEndpoint); + expect(endpoint.bind).toHaveBeenCalledWith('lightingColorCtrl', coordinatorEndpoint); + expect(endpoint.configureReporting).toHaveBeenCalledTimes(3); + expect(endpoint.configureReporting).toHaveBeenCalledWith('genOnOff', [{"attribute": "onOff", "maximumReportInterval": 300, "minimumReportInterval": 0, "reportableChange": 0}]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('genLevelCtrl', [{"attribute": "currentLevel", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}]); + expect(endpoint.configureReporting).toHaveBeenCalledWith('lightingColorCtrl', [{"attribute": "colorTemperature", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}, {"attribute": "currentX", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}, {"attribute": "currentY", "maximumReportInterval": 300, "minimumReportInterval": 3, "reportableChange": 0}]); + } + + mockClear = (device) => { + for (const endpoint of device.endpoints) { + endpoint.read.mockClear(); + endpoint.write.mockClear(); + endpoint.configureReporting.mockClear(); + endpoint.bind.mockClear(); + } + } + + beforeEach(async () => { + data.writeDefaultConfiguration(); + settings._reRead(); + data.writeEmptyState(); + settings.set(['advanced', 'report'], true); + controller = new Controller(); + await controller.start(); + mocksClear.forEach((m) => m.mockClear()); + await flushPromises(); + }); + + it('Should configure reporting on startup', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + expectOnOffBrightnessColorReport(endpoint); + }); + + it('Should configure reporting when receicing message from device which has not been setup yet', async () => { + const device = zigbeeHerdsman.devices.bulb; + const endpoint = device.getEndpoint(1); + device.save.mockClear(); + mockClear(device); + delete device.meta.reporting; + const data = {onOff: 1} + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expectOnOffBrightnessColorReport(endpoint); + expect(device.save).toHaveBeenCalledTimes(1); + }); + + it('Should not configure reporting when still configuring', async () => { + jest.useFakeTimers(); + const device = zigbeeHerdsman.devices.bulb; + const endpoint = device.getEndpoint(1); + endpoint.bind.mockImplementationOnce(async () => await wait(1000)); + delete device.meta.reporting; + mockClear(device); + const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(1); + jest.runAllTimers(); + jest.useRealTimers(); + }); + + it('Should not mark as configured when reporting setup fails', async () => { + const device = zigbeeHerdsman.devices.bulb; + const endpoint = device.getEndpoint(1); + endpoint.bind.mockImplementationOnce(async () => {throw new Error('failed')}); + delete device.meta.reporting; + mockClear(device); + const payload = {data: {onOff: 1}, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(1); + expect(device.meta.reporting).toBeUndefined(); + }); + + it('Should not configure reporting when receicing message from device which has already been setup yet', async () => { + const device = zigbeeHerdsman.devices.bulb; + const endpoint = device.getEndpoint(1); + mockClear(device); + const data = {onOff: 1} + const payload = {data, cluster: 'genOnOff', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(0); + expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); + }); + + it('Should not configure reporting for end devices', async () => { + const device = zigbeeHerdsman.devices.E11_G13; + const endpoint = device.getEndpoint(1); + expect(endpoint.bind).toHaveBeenCalledTimes(0); + expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); + }); + + it('Should configure reporting when deviceAnnounce message from IKEA device', async () => { + const device = zigbeeHerdsman.devices.bulb; + const endpoint = device.getEndpoint(1); + mockClear(device); + const payload = {device}; + await zigbeeHerdsman.events.deviceAnnounce(payload); + await flushPromises(); + expectOnOffBrightnessColorReport(endpoint); + }); + + it('Should not configure reporting on device leave', async () => { + const device = zigbeeHerdsman.devices.bulb; + const endpoint = device.getEndpoint(1); + delete device.meta.reporting; + mockClear(device); + await zigbeeHerdsman.events.deviceLeave({ieeeAddr: device.ieeeAddr}); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(0); + expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); + }); + + it('Should not configure reporting for the CC2530 router', async () => { + const device = zigbeeHerdsman.devices.CC2530_ROUTER; + const endpoint = device.getEndpoint(1); + delete device.meta.reporting; + mockClear(device); + await zigbeeHerdsman.events.deviceLeave({ieeeAddr: device.ieeeAddr}); + await flushPromises(); + expect(endpoint.bind).toHaveBeenCalledTimes(0); + expect(endpoint.configureReporting).toHaveBeenCalledTimes(0); + }); +}); diff --git a/test/entityPublish.test.js b/test/entityPublish.test.js new file mode 100644 index 000000000..d9bc40b2d --- /dev/null +++ b/test/entityPublish.test.js @@ -0,0 +1,827 @@ +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +const MQTT = require('./stub/mqtt'); +const settings = require('../lib/util/settings'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const mocksClear = [MQTT.publish, logger.warn, logger.debug]; + +const expectNothingPublished = () => { + Object.values(zigbeeHerdsman.devices).forEach((d) => { + d.endpoints.forEach((e) => { + expect(e.command).toHaveBeenCalledTimes(0); + expect(e.read).toHaveBeenCalledTimes(0); + expect(e.write).toHaveBeenCalledTimes(0); + }); + }) + Object.values(zigbeeHerdsman.groups).forEach((g) => { + expect(g.command).toHaveBeenCalledTimes(0); + }) +} + +describe('Entity publish', () => { + let controller; + + beforeAll(async () => { + data.writeEmptyState(); + controller = new Controller(); + await controller.start(); + await flushPromises(); + await wait(50); + }); + + beforeEach(async () => { + jest.useRealTimers(); + await flushPromises(); + data.writeDefaultConfiguration(); + controller.state.state = {}; + settings._reRead(); + mocksClear.forEach((m) => m.mockClear()); + Object.values(zigbeeHerdsman.devices).forEach((d) => { + d.endpoints.forEach((e) => { + e.command.mockClear(); + e.read.mockClear(); + e.write.mockClear(); + }) + }) + Object.values(zigbeeHerdsman.groups).forEach((g) => { + g.command.mockClear(); + }); + }); + + it('Should publish messages to zigbee devices', async () => { + const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness: '200'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {"level": 200, "transtime": 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({brightness: 200, state: 'ON'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices when there is no converters', async () => { + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness_no: '200'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should publish messages to zigbee devices when there is a get converter but no set', async () => { + await MQTT.events.message('zigbee2mqtt/thermostat/set', JSON.stringify({relay_status_log_rsp: '200'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should publish messages to zigbee devices with complicated topic', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'wohnzimmer.light.wall.right') + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/wohnzimmer.light.wall.right/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wohnzimmer.light.wall.right'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices when brightness is in %', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness_percent: '92'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 235, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 235}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices when brightness is in number', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness: 230})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 230, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 230}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices with color_temp', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color_temp: '222'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 222, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 222}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices with color_temp in %', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color_temp_percent: '100'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 500, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 500}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices with non-default ep', async () => { + const device = zigbeeHerdsman.devices.QBKG04LM; + const endpoint = device.getEndpoint(2); + await MQTT.events.message('zigbee2mqtt/wall_switch/set', JSON.stringify({state: 'OFF'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices with non-default ep and postfix', async () => { + const device = zigbeeHerdsman.devices.QBKG03LM; + const endpoint = device.getEndpoint(3); + await MQTT.events.message('zigbee2mqtt/wall_switch_double/right/set', JSON.stringify({state: 'OFF'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/wall_switch_double'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state_right: 'OFF'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee gledopto with [11,13]', async () => { + const device = zigbeeHerdsman.devices.GLEDOPTO1112; + const endpoint = device.getEndpoint(11); + await MQTT.events.message('zigbee2mqtt/led_controller_1/set', JSON.stringify({state: 'OFF'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/led_controller_1'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee gledopto with [11,12,13]', async () => { + const device = zigbeeHerdsman.devices.GLEDOPTO111213; + const endpoint = device.getEndpoint(12); + await MQTT.events.message('zigbee2mqtt/led_controller_2/set', JSON.stringify({state: 'OFF'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/led_controller_2'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices with color xy', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color: {x: 100, y: 50}})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 100, y: 50}}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices with color xy and sate', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color: {x: 100, y: 50}, state: 'ON'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(2); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}); + expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, state: 'ON'}); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices with color xy and brightness', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color: {x: 100, y: 50}, brightness: 20})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(2); + expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}); + expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 20}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, state: 'ON', brightness: 20}); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices with color xy, brightness and state on', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color: {x: 100, y: 50}, brightness: 20, state: 'ON'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(2); + expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}); + expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 20}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, state: 'ON', brightness: 20}); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices with color xy, brightness and state off', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color: {x: 100, y: 50}, brightness: 20, state: 'OFF'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(2); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "off", {}); + expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 6553500, colory: 3276750, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({color: {x: 100, y: 50}, state: 'OFF'}); + expect(MQTT.publish.mock.calls[1][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('Should publish messages to zigbee devices with color rgb', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color: {r: 100, g: 200, b: 10}})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}}); + }); + + it('Should publish messages to zigbee devices with color rgb', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({color: {rgb: '100,200,10'}})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 17806, colory: 43155, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.2717, y: 0.6585}}); + }); + + it('Should publish messages to zigbee devices with color rgb', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({state: 'ON', brightness: '50'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50}); + }); + + it('Should publish messages groups', async () => { + const group = zigbeeHerdsman.groups.group_1; + await MQTT.events.message('zigbee2mqtt/group_1/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expect(group.command).toHaveBeenCalledTimes(1); + expect(group.command).toHaveBeenCalledWith("genOnOff", "on", {}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + }); + + it('Should publish messages to groups with brightness_percent', async () => { + const group = zigbeeHerdsman.groups.group_1; + await MQTT.events.message('zigbee2mqtt/group_1/set', JSON.stringify({brightness_percent: 50})); + await flushPromises(); + expect(group.command).toHaveBeenCalledTimes(1); + expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 127, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 127}); + }); + + it('Should publish messages to groups with on and brightness', async () => { + const group = zigbeeHerdsman.groups.group_1; + await MQTT.events.message('zigbee2mqtt/group_1/set', JSON.stringify({state: 'ON', brightness: 50})); + await flushPromises(); + expect(group.command).toHaveBeenCalledTimes(1); + expect(group.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 50, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', brightness: 50}); + }); + + it('Should publish messages to groups with off and brightness', async () => { + const group = zigbeeHerdsman.groups.group_1; + await MQTT.events.message('zigbee2mqtt/group_1/set', JSON.stringify({state: 'OFF', brightness: 50})); + await flushPromises(); + expect(group.command).toHaveBeenCalledTimes(1); + expect(group.command).toHaveBeenCalledWith("genOnOff", "off", {}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF'}); + }); + + it('Should publish messages to groups color', async () => { + const group = zigbeeHerdsman.groups.group_1; + await MQTT.events.message('zigbee2mqtt/group_1/set', JSON.stringify({color: {x: 0.37, y: 0.28}})); + await flushPromises(); + expect(group.command).toHaveBeenCalledTimes(1); + expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColor", {colorx: 24248, colory: 18350, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.37, y: 0.28}}); + }); + + it('Should publish messages to groups color temperature', async () => { + const group = zigbeeHerdsman.groups.group_1; + await MQTT.events.message('zigbee2mqtt/group_1/set', JSON.stringify({color_temp: 100})); + await flushPromises(); + expect(group.command).toHaveBeenCalledTimes(1); + expect(group.command).toHaveBeenCalledWith("lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/group_1'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color_temp: 100}); + }); + + it('Should create and publish to group which is in configuration.yaml but not in zigbee-herdsman', async () => { + delete zigbeeHerdsman.groups.group_2; + expect(Object.values(zigbeeHerdsman.groups).length).toBe(1); + await MQTT.events.message('zigbee2mqtt/group_2/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expect(Object.values(zigbeeHerdsman.groups).length).toBe(2); + expect(zigbeeHerdsman.groups.group_2.command).toHaveBeenCalledTimes(1); + expect(zigbeeHerdsman.groups.group_2.command).toHaveBeenCalledWith("genOnOff", "on", {}); + }); + + it('Should handle non-valid topics', async () => { + await MQTT.events.message('zigbee2mqtt1/bulb_color/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should handle non-valid topics', async () => { + await MQTT.events.message('zigbee2mqtt1/bulb_color/sett', JSON.stringify({state: 'ON'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should handle non-valid topics', async () => { + await MQTT.events.message('zigbee2mqtt/bulb_color/write', JSON.stringify({state: 'ON'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should handle non-valid topics', async () => { + await MQTT.events.message('zigbee2mqtt/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should handle non-valid topics', async () => { + await MQTT.events.message('set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should handle get', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/get', JSON.stringify({state: ''})); + await flushPromises(); + expect(endpoint.read).toHaveBeenCalledTimes(1); + expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); + }); + + it('Should not respond to bridge/config/devices/get', async () => { + await MQTT.events.message('zigbee2mqtt/bridge/config/devices/get', JSON.stringify({state: 'ON'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should not respond to bridge/config/devices/set', async () => { + await MQTT.events.message('zigbee2mqtt/bridge/config/devices/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should not respond to bridge/config/devices', async () => { + await MQTT.events.message('zigbee2mqtt/bridge/config/devices', JSON.stringify({state: 'ON'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should parse topic with when base topic has multiple slashes', async () => { + settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/home'); + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/at/my/home/bulb_color/get', JSON.stringify({state: ''})); + await flushPromises(); + expect(endpoint.read).toHaveBeenCalledTimes(1); + expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); + }); + + it('Should parse topic with when deviceID has multiple slashes', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'floor0/basement/my_device_id2'); + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/floor0/basement/my_device_id2/get', JSON.stringify({state: ''})); + await flushPromises(); + expect(endpoint.read).toHaveBeenCalledTimes(1); + expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); + }); + + it('Should parse topic with when base and deviceID have multiple slashes', async () => { + settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/basement'); + const device = zigbeeHerdsman.devices.bulb_color; + settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'floor0/basement/my_device_id2'); + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/at/my/basement/floor0/basement/my_device_id2/get', JSON.stringify({state: ''})); + await flushPromises(); + expect(endpoint.read).toHaveBeenCalledTimes(1); + expect(endpoint.read).toHaveBeenCalledWith('genOnOff', ['onOff']); + }); + + it('Should parse set with attribute topic', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set/state', 'ON'); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}); + }); + + it('Should parse set with ieeeAddr topic', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/0x000b57fffec6a5b3/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}); + }); + + it('Should parse set with non-existing postfix', async () => { + await MQTT.events.message('zigbee2mqtt/wall_switch_double/invalid/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should parse set with postfix topic and attribute', async () => { + const device = zigbeeHerdsman.devices.QBKG03LM; + const endpoint = device.getEndpoint(2); + await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', 'ON'); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}); + }); + + it('Should parse set with and slashes in base and deviceID postfix topic', async () => { + settings.set(['mqtt', 'base_topic'], 'zigbee2mqtt/at/my/home') + const device = zigbeeHerdsman.devices.QBKG03LM; + settings.set(['devices', device.ieeeAddr, 'friendly_name'], 'in/basement/wall_switch_double'); + const endpoint = device.getEndpoint(2); + await MQTT.events.message('zigbee2mqtt/at/my/home/in/basement/wall_switch_double/left/set', JSON.stringify({state: 'ON'})); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("genOnOff", "on", {}); + }); + + it('Should not publish messages to zigbee devices when payload is invalid', async () => { + const device = zigbeeHerdsman.devices.QBKG03LM; + const endpoint = device.getEndpoint(2); + await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', JSON.stringify({state: true})); + await MQTT.events.message('zigbee2mqtt/wall_switch_double/left/set', JSON.stringify({state: 1})); + await flushPromises(); + expectNothingPublished(); + }); + + it('Should set state before color', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const payload = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(2); + expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}]); + expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {"colorx": 45940, "colory": 19595, "transtime": 0}]); + }); + + it('Should read after write when enabled', async () => { + jest.useFakeTimers(); + const device = zigbeeHerdsman.devices.bulb_color; + settings.set(['devices', device.ieeeAddr, 'retrieve_state'], true); + const endpoint = device.getEndpoint(1); + const payload = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + jest.runAllTimers(); + expect(endpoint.command).toHaveBeenCalledTimes(2); + expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}]); + expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {"colorx": 45940, "colory": 19595, "transtime": 0}]); + expect(endpoint.read).toHaveBeenCalledTimes(2); + expect(endpoint.read.mock.calls[0]).toEqual(["genOnOff", ["onOff"]]); + expect(endpoint.read.mock.calls[1]).toEqual(["lightingColorCtrl", ["currentX", "currentY", "colorTemperature"]]); + }); + + it('Should use transition when brightness', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + settings.set(['devices', device.ieeeAddr, 'transition'], 20); + const endpoint = device.getEndpoint(1); + const payload = {brightness: 20}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 200}]); + }); + + it('Should use transition when color temp', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + settings.set(['devices', device.ieeeAddr, 'transition'], 20); + const endpoint = device.getEndpoint(1); + const payload = {color_temp: 200}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 200, transtime: 200}]); + }); + + it('Message transition should overrule options transition', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + settings.set(['devices', device.ieeeAddr, 'transition'], 20); + const endpoint = device.getEndpoint(1); + const payload = {brightness: 200, transition: 10}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 200, transtime: 100}]); + }); + + it('Should set state with brightness before color', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const payload = {'state': 'ON', 'color': {'x': 0.701, 'y': 0.299}, 'transition': 3, 'brightness': 100}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(2); + expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 100, transtime: 30}]); + expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColor", {"colorx": 45940, "colory": 19595, "transtime": 30}]); + }); + + it('Should turn device off when brightness 0 is send', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const payload = {'brightness': 0}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "off", {}]); + }); + + it('Should turn device off when brightness 0 is send with light_brightness converter', async () => { + const device = zigbeeHerdsman.devices.HGZB04D; + const endpoint = device.getEndpoint(1); + const payload = {'brightness': 0}; + await MQTT.events.message('zigbee2mqtt/dimmer_wall_switch/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "off", {}]); + }); + + it('ZNCLDJ11LM open', async () => { + const device = zigbeeHerdsman.devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1); + const payload = {'state': 'OPEN'}; + await MQTT.events.message('zigbee2mqtt/curtain/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.write).toHaveBeenCalledTimes(1); + expect(endpoint.write).toHaveBeenCalledWith("genAnalogOutput", {"85": {"type": 57, "value": 100}}); + }); + + it('ZNCLDJ11LM position', async () => { + const device = zigbeeHerdsman.devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1); + const payload = {'position': 10}; + await MQTT.events.message('zigbee2mqtt/curtain/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.write).toHaveBeenCalledTimes(1); + expect(endpoint.write).toHaveBeenCalledWith("genAnalogOutput", {"85": {"type": 57, "value": 10}}); + }); + + it('ZNCLDJ11LM position', async () => { + const device = zigbeeHerdsman.devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1); + const payload = {'state': 'CLOSE'}; + await MQTT.events.message('zigbee2mqtt/curtain/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.write).toHaveBeenCalledTimes(1); + expect(endpoint.write).toHaveBeenCalledWith("genAnalogOutput", {"85": {"type": 57, "value": 0}}); + }); + + it('ZNCLDJ11LM position', async () => { + const device = zigbeeHerdsman.devices.ZNCLDJ11LM; + const endpoint = device.getEndpoint(1); + const payload = {'state': 'STOP'}; + await MQTT.events.message('zigbee2mqtt/curtain/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("closuresWindowCovering", "stop", {}); + }); + + + it('Should turn device off when brightness 0 is send with light_brightness converter ON', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const payload = {state: 'ON', transition: 1}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 255, transtime: 10}]); + }); + + it('Should turn device off when brightness 0 is send with light_brightness converter OFF', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const payload = {state: 'OFF', transition: 1}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 0, transtime: 10}]); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'OFF', brightness: 0}); + }); + + it('Home Assistant: should set state', async () => { + settings.set(['homeassistant'], true); + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const payload = {state: 'ON'}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}]); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + }); + + it('Home Assistant: should not set state when color temperature is also set and device is already on', async () => { + settings.set(['homeassistant'], true); + const device = zigbeeHerdsman.devices.bulb_color; + controller.state.set(device.ieeeAddr, {state: 'ON'}) + const endpoint = device.getEndpoint(1); + const payload = {state: 'ON', color_temp: 100}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}]); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON', color_temp: 100}); + }); + + it('Home Assistant: should set state when color temperature is also set and device is off', async () => { + settings.set(['homeassistant'], true); + const device = zigbeeHerdsman.devices.bulb_color; + controller.state.set(device.ieeeAddr, {state: 'OFF'}) + const endpoint = device.getEndpoint(1); + const payload = {state: 'ON', color_temp: 100}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(2); + expect(endpoint.command.mock.calls[0]).toEqual(["genOnOff", "on", {}]); + expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 100, transtime: 0}]); + expect(MQTT.publish).toHaveBeenCalledTimes(2); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({state: 'ON'}); + expect(MQTT.publish.mock.calls[1][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[1][1])).toStrictEqual({state: 'ON', color_temp: 100}); + }); + + it('Home Assistant: should not set state when color is also set', async () => { + settings.set(['homeassistant'], true); + const device = zigbeeHerdsman.devices.bulb_color; + controller.state.set(device.ieeeAddr, {state: 'ON'}) + const endpoint = device.getEndpoint(1); + const payload = {state: 'ON', color: {x: 0.41, y: 0.25}}; + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command.mock.calls[0]).toEqual(["lightingColorCtrl", "moveToColor", {colorx: 26869, colory: 16384, transtime: 0}]); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(JSON.parse(MQTT.publish.mock.calls[0][1])).toStrictEqual({color: {x: 0.41, y: 0.25}, state: 'ON'}); + }); + + // TODO + // it('Should publish message with disFeedbackRsp when set', async () => { + // const device = zigbeeHerdsman.devices.HAMPTON99432; + // controller.state.set(device.ieeeAddr, {state: 'ON'}) + // const endpoint = device.getEndpoint(1); + // const payload = {brightness: '92'}; + // await MQTT.events.message('zigbee2mqtt/fan/set', JSON.stringify(payload)); + // await flushPromises(); + // expect(endpoint.command).toHaveBeenCalledTimes(1); + // expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 92}]); + // }); + // devicePublish.onMQTTMessage('zigbee2mqtt/0x00000003/set', JSON.stringify({state: 'OFF'})); + // expect(zigbee.publish).toHaveBeenCalledTimes(2); + // expect(zigbee.publish).toHaveBeenNthCalledWith(2, + // '0x00000003', + // 'device', + // 'genOnOff', + // 'off', + // 'functional', + // {}, + // cfg.disFeedbackRsp, + // null, + // expect.any(Function)); + // }); + + it('Should publish messages to zigbee devices', async () => { + settings.set(['advanced', 'last_seen'], 'ISO_8601') + const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); + await MQTT.events.message('zigbee2mqtt/bulb_color/set', JSON.stringify({brightness: '200'})); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(MQTT.publish.mock.calls[0][0]).toStrictEqual('zigbee2mqtt/bulb_color'); + expect(typeof JSON.parse(MQTT.publish.mock.calls[0][1]).last_seen).toStrictEqual('string'); + expect(MQTT.publish.mock.calls[0][2]).toStrictEqual({"qos": 0, "retain": false}); + }); + + it('HS2WD-E burglar warning', async () => { + const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const payload = {warning: {duration: 100, mode: 'burglar', strobe: true, level: 'high'}}; + await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 22, "warningduration": 100}); + }); + + it('HS2WD-E emergency warning', async () => { + const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const payload = {warning: {duration: 10, mode: 'emergency', strobe: false, level: 'very_high'}}; + await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 51, "warningduration": 10}); + }); + + it('HS2WD-E emergency without level', async () => { + const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const payload = {warning: {duration: 10, mode: 'emergency', strobe: false}}; + await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 49, "warningduration": 10}); + }); + + it('HS2WD-E wrong payload (should use defaults)', async () => { + const endpoint = zigbeeHerdsman.devices.HS2WD.getEndpoint(1); + const payload = {warning: 'wrong'}; + await MQTT.events.message('zigbee2mqtt/siren/set', JSON.stringify(payload)); + await flushPromises(); + expect(endpoint.command).toHaveBeenCalledTimes(1); + expect(endpoint.command).toHaveBeenCalledWith("ssIasWd", "startWarning", {"startwarninginfo": 53, "warningduration": 10}); + }); + + it('Shouldnt do anythign when device is not supported', async () => { + const payload = {state: 'ON'}; + await MQTT.events.message('zigbee2mqtt/unsupported2/set', JSON.stringify(payload)); + await flushPromises(); + expectNothingPublished(); + }); +}); diff --git a/test/group.test.js b/test/group.test.js index f5ae0960f..f54d003b8 100644 --- a/test/group.test.js +++ b/test/group.test.js @@ -1,287 +1,237 @@ -const Groups = require('../lib/extension/groups'); +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +zigbeeHerdsman.returnDevices.push('0x00124b00120144ae'); +zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b3'); +zigbeeHerdsman.returnDevices.push('0x000b57fffec6a5b2'); +zigbeeHerdsman.returnDevices.push('0x0017880104e45542'); +const MQTT = require('./stub/mqtt'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); const settings = require('../lib/util/settings'); -const utils = require('./utils'); - -let groupExtension = null; -let zigbee = null; describe('Groups', () => { + let controller; + beforeEach(() => { - utils.stubLogger(jest); - - zigbee = { - publish: jest.fn(), - }; - - groupExtension = new Groups(zigbee, null, null, null); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); + controller = new Controller(); + Object.values(zigbeeHerdsman.groups).forEach((g) => g.members = []); + data.writeDefaultConfiguration(); + settings._reRead(); + }) it('Apply group updates add', async () => { - zigbee.publish.mockClear(); - const from = {}; - const to = {'1': ['1', '2']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenCalledWith( - '1', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '2', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: ['bulb', 'bulb_color']}}); + zigbeeHerdsman.groups.group_1.members.push(zigbeeHerdsman.devices.bulb.getEndpoint(1)) + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([ + zigbeeHerdsman.devices.bulb.getEndpoint(1), + zigbeeHerdsman.devices.bulb_color.getEndpoint(1) + ]); }); it('Apply group updates remove', async () => { - zigbee.publish.mockClear(); - const from = {'1': ['1', '2', '3']}; - const to = {'1': ['1']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenCalledWith( - '3', 'device', 'genGroups', 'remove', 'functional', - {groupid: '1'}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '2', 'device', 'genGroups', 'remove', 'functional', - {groupid: '1'}, null, null, expect.any(Function) - ); + const endpoint = zigbeeHerdsman.devices.bulb_color.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(endpoint); + settings.set(['groups'], {'1': {friendly_name: 'group_1'}}); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); }); - it('Apply group updates add 1', async () => { - zigbee.publish.mockClear(); - const from = {'1': ['1']}; - const to = {'1': ['1', '2', '3']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenCalledWith( - '3', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '2', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); + it('Move to non existing group', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(endpoint); + settings.set(['groups'], {'3': {friendly_name: 'group_3', devices: [device.ieeeAddr]}}); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); }); - it('Apply group updates add and remove group', async () => { - zigbee.publish.mockClear(); - const from = {'1': ['1', '2']}; - const to = {'2': ['1', '2']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(4); - expect(zigbee.publish).toHaveBeenCalledWith( - '1', 'device', 'genGroups', 'remove', 'functional', - {groupid: '1'}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '2', 'device', 'genGroups', 'remove', 'functional', - {groupid: '1'}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '1', 'device', 'genGroups', 'add', 'functional', - {groupid: '2', groupname: ''}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '2', 'device', 'genGroups', 'add', 'functional', - {groupid: '2', groupname: ''}, null, null, expect.any(Function) - ); + it('Add non standard endpoint to group with name', async () => { + const QBKG03LM = zigbeeHerdsman.devices.QBKG03LM; + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: ['0x0017880104e45542/right']}}); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(3)]); }); - it('Apply group updates change 1', async () => { - zigbee.publish.mockClear(); - const from = {'1': ['1', '4', '2']}; - const to = {'1': ['1', '2', '3']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenCalledWith( - '3', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '4', 'device', 'genGroups', 'remove', 'functional', - {groupid: '1'}, null, null, expect.any(Function) - ); + it('Add non standard endpoint to group with number', async () => { + const QBKG03LM = zigbeeHerdsman.devices.QBKG03LM; + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: ['wall_switch_double/2']}}); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([QBKG03LM.getEndpoint(2)]); }); - it('Apply group updates change 2', async () => { - zigbee.publish.mockClear(); - const from = {'1': ['1', '2', '3']}; - const to = {'1': ['3', '1', '2', '4']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenCalledWith( - '4', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); - }); - - it('Apply group updates change 3', async () => { - zigbee.publish.mockClear(); - const from = {'1': ['1', '2']}; - const to = {'1': ['3', '1', '4', '2']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(2); - expect(zigbee.publish).toHaveBeenCalledWith( - '3', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '4', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); - }); - - it('Apply group updates change 4', async () => { - zigbee.publish.mockClear(); - const from = {'1': ['1']}; - const to = {'2': ['3', '1']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(3); - expect(zigbee.publish).toHaveBeenCalledWith( - '1', 'device', 'genGroups', 'remove', 'functional', - {groupid: '1'}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '1', 'device', 'genGroups', 'add', 'functional', - {groupid: '2', groupname: ''}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '3', 'device', 'genGroups', 'add', 'functional', - {groupid: '2', groupname: ''}, null, null, expect.any(Function) - ); - }); - - it('Apply group updates change 5', async () => { - zigbee.publish.mockClear(); - const from = {'1': ['1']}; - const to = {'1': ['3'], '2': ['3', '1']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(4); - expect(zigbee.publish).toHaveBeenCalledWith( - '1', 'device', 'genGroups', 'remove', 'functional', - {groupid: '1'}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '3', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '1', 'device', 'genGroups', 'add', 'functional', - {groupid: '2', groupname: ''}, null, null, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '3', 'device', 'genGroups', 'add', 'functional', - {groupid: '2', groupname: ''}, null, null, expect.any(Function) - ); - }); - - it('Apply group updates add with postfix', async () => { - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'}); - zigbee.getEndpoint = (entityID, ep) => ({epId: ep}); - const from = {}; - const to = {'1': ['0x12345689/right']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenCalledWith( - '0x12345689', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, 3, expect.any(Function) - ); - }); - - it('Apply group updates add and remove with postfix', async () => { - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'}); - zigbee.getEndpoint = (entityID, ep) => ({epId: ep}); - const from = {'1': ['0x12345689/right']}; - const to = {'1': ['0x12345689'], '2': ['0x12345689/left']}; - groupExtension.apply(from, to); - expect(zigbee.publish).toHaveBeenCalledTimes(3); - expect(zigbee.publish).toHaveBeenCalledWith( - '0x12345689', 'device', 'genGroups', 'add', 'functional', - {groupid: '2', groupname: ''}, null, 2, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '0x12345689', 'device', 'genGroups', 'remove', 'functional', - {groupid: '1'}, null, 3, expect.any(Function) - ); - expect(zigbee.publish).toHaveBeenCalledWith( - '0x12345689', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); + it('Shouldnt crash on non-existing devices', async () => { + logger.error.mockClear(); + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: ['not_existing_bla']}}); + await controller.start(); + await flushPromises(); + expect(zigbeeHerdsman.groups.group_1.members).toStrictEqual([]); + expect(logger.error).toHaveBeenCalledWith("Cannot find 'not_existing_bla' of group 'group_1'"); }); it('Add to group via MQTT', async () => { - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'}); - zigbee.getEndpoint = (entityID, ep) => ({epId: ep}); - jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue(1); - jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x12345689'); - groupExtension.onMQTTMessage('zigbee2mqtt/bridge/group/my_group/add', 'my_switch'); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenCalledWith( - '0x12345689', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, null, expect.any(Function) - ); + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: []}}); + expect(group.members.length).toBe(0); + await controller.start(); + await flushPromises(); + MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb_color'); + await flushPromises(); + expect(group.members).toStrictEqual([endpoint]); + expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/1`]); }); it('Add to group via MQTT with postfix', async () => { - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'}); - zigbee.getEndpoint = (entityID, ep) => ({epId: ep}); - jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue(1); - jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x12345689'); - groupExtension.onMQTTMessage('zigbee2mqtt/bridge/group/my_group/add', 'my_switch/right'); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenCalledWith( - '0x12345689', 'device', 'genGroups', 'add', 'functional', - {groupid: '1', groupname: ''}, null, 3, expect.any(Function) - ); + const device = zigbeeHerdsman.devices.QBKG03LM; + const endpoint = device.getEndpoint(3); + const group = zigbeeHerdsman.groups.group_1; + expect(group.members.length).toBe(0); + await controller.start(); + await flushPromises(); + await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'wall_switch_double/right'); + await flushPromises(); + expect(group.members).toStrictEqual([endpoint]); + expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]); }); - it('Remove from group via MQTT with postfix', async () => { - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'}); - zigbee.getEndpoint = (entityID, ep) => ({epId: ep}); - jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue(1); - jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x12345689'); - groupExtension.onMQTTMessage('zigbee2mqtt/bridge/group/my_group/remove', 'my_switch/left'); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenCalledWith( - '0x12345689', 'device', 'genGroups', 'remove', 'functional', - {groupid: '1'}, null, 2, expect.any(Function) - ); + it('Add to group via MQTT with postfix shouldnt add it twice', async () => { + const device = zigbeeHerdsman.devices.QBKG03LM; + const endpoint = device.getEndpoint(3); + const group = zigbeeHerdsman.groups.group_1; + expect(group.members.length).toBe(0); + await controller.start(); + await flushPromises(); + await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'wall_switch_double/right'); + await flushPromises(); + await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', '0x0017880104e45542/3'); + await flushPromises(); + expect(group.members).toStrictEqual([endpoint]); + expect(settings.getGroup('group_1').devices).toStrictEqual([`${device.ieeeAddr}/${endpoint.ID}`]); }); - it('Remove all group via MQTT', async () => { - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'}); - zigbee.getEndpoint = (entityID, ep) => ({epId: ep}); - jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x12345689'); - groupExtension.onMQTTMessage('zigbee2mqtt/bridge/group/remove_all', 'my_switch'); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenCalledWith( - '0x12345689', 'device', 'genGroups', 'removeAll', 'functional', - {}, null, null, expect.any(Function) - ); + it('Remove from group via MQTT', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(endpoint); + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: [device.ieeeAddr]}}); + await controller.start(); + await flushPromises(); + await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'bulb_color'); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(settings.getGroup('group_1').devices).toStrictEqual([]); }); - it('Remove all group via MQTT deprecated', async () => { - zigbee.publish.mockClear(); - zigbee.getDevice = () => ({modelId: 'lumi.ctrl_neutral2'}); - zigbee.getEndpoint = (entityID, ep) => ({epId: ep}); - jest.spyOn(settings, 'getGroupIDByFriendlyName').mockReturnValue(1); - jest.spyOn(settings, 'getIeeeAddrByFriendlyName').mockReturnValue('0x12345689'); - groupExtension.onMQTTMessage('zigbee2mqtt/bridge/group/my_group/remove_all', 'my_switch'); - expect(zigbee.publish).toHaveBeenCalledTimes(1); - expect(zigbee.publish).toHaveBeenCalledWith( - '0x12345689', 'device', 'genGroups', 'removeAll', 'functional', - {}, null, null, expect.any(Function) - ); + it('Remove from group via MQTT when in zigbee but not in settings', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(endpoint); + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: ['dummy']}}); + await controller.start(); + await flushPromises(); + await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'bulb_color'); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(settings.getGroup('group_1').devices).toStrictEqual(['dummy']); + }); + + it('Remove from group via MQTT with postfix variant 1', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(endpoint); + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: [`wall_switch_double/right`]}}); + await controller.start(); + await flushPromises(); + await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', '0x0017880104e45542/3'); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(settings.getGroup('group_1').devices).toStrictEqual([]); + }); + + it('Remove from group via MQTT with postfix variant 2', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(endpoint); + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: [`0x0017880104e45542/right`]}}); + await controller.start(); + await flushPromises(); + await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', 'wall_switch_double/3'); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(settings.getGroup('group_1').devices).toStrictEqual([]); + }); + + it('Remove from group via MQTT with postfix variant 3', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(endpoint); + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: [`wall_switch_double/3`]}}); + await controller.start(); + await flushPromises(); + await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove', '0x0017880104e45542/right'); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(settings.getGroup('group_1').devices).toStrictEqual([]); + }); + + it('Remove from group all', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(endpoint); + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: [`wall_switch_double/3`]}}); + await controller.start(); + await flushPromises(); + await MQTT.events.message('zigbee2mqtt/bridge/group/remove_all', '0x0017880104e45542/right'); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(settings.getGroup('group_1').devices).toStrictEqual([]); + }); + + it('Remove from group all deprecated', async () => { + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const group = zigbeeHerdsman.groups.group_1; + group.members.push(endpoint); + settings.set(['groups'], {'1': {friendly_name: 'group_1', devices: [`wall_switch_double/3`]}}); + await controller.start(); + await flushPromises(); + await MQTT.events.message('zigbee2mqtt/bridge/group/group_1/remove_all', '0x0017880104e45542/right'); + await flushPromises(); + expect(group.members).toStrictEqual([]); + expect(settings.getGroup('group_1').devices).toStrictEqual([]); + }); + + it('Log when adding to non-existing group', async () => { + await controller.start(); + await flushPromises(); + logger.error.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/group/group_1_not_existing/add', 'bulb_color'); + await flushPromises(); + expect(logger.error).toHaveBeenCalledWith("Group 'group_1_not_existing' does not exist"); + }); + + it('Log when adding to non-existing device', async () => { + await controller.start(); + await flushPromises(); + logger.error.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/group/group_1/add', 'bulb_color_not_existing'); + await flushPromises(); + expect(logger.error).toHaveBeenCalledWith("Device 'bulb_color_not_existing' does not exist"); }); }); diff --git a/test/homeassistant.test.js b/test/homeassistant.test.js index bd52d6d7b..868ba3a1d 100644 --- a/test/homeassistant.test.js +++ b/test/homeassistant.test.js @@ -1,659 +1,620 @@ -const devices = require('zigbee-shepherd-converters').devices; -const HomeassistantExtension = require('../lib/extension/homeassistant'); +const data = require('./stub/data'); const settings = require('../lib/util/settings'); -const utils = require('./utils'); - -const WSDCGQ11LM = devices.find((d) => d.model === 'WSDCGQ11LM'); -const SV01 = devices.find((d) => d.model === 'SV01'); -const FAN99432 = devices.find((d) => d.model === '99432'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); +const flushPromises = () => new Promise(setImmediate); +const MQTT = require('./stub/mqtt'); +const Controller = require('../lib/controller'); +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); describe('HomeAssistant extension', () => { - let homeassistant = null; - let mqtt = null; - - beforeEach(() => { - utils.stubLogger(jest); - - mqtt = { - publish: jest.fn(), - }; - - homeassistant = new HomeassistantExtension(null, mqtt, null, null); - homeassistant.zigbee2mqttVersion = 'test'; + beforeEach(async () => { + this.version = await require('../lib/util/utils').getZigbee2mqttVersion(); + this.version = `Zigbee2mqtt ${this.version.version}`; + jest.useRealTimers(); + data.writeDefaultConfiguration(); + settings._reRead(); + data.writeEmptyState(); + MQTT.publish.mockClear(); + settings.set(['homeassistant'], true); }); - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('Should have mapping for all devices supported by zigbee-shepherd-converters', () => { + it('Should have mapping for all devices supported by zigbee-herdsman-converters', () => { const missing = []; + const HomeAssistant = require('../lib/extension/homeassistant'); + const ha = new HomeAssistant(null, null, null, null); - devices.forEach((d) => { - if (!homeassistant._getMapping()[d.model]) { + require('zigbee-herdsman-converters').devices.forEach((d) => { + if (!ha._getMapping()[d.model]) { missing.push(d.model); } }); expect(missing).toHaveLength(0); - } - ); + }); - it('Should discover devices', () => { - let payload = null; - jest.spyOn(settings, 'getDevice').mockReturnValue({friendly_name: 'my_device'}); + it('Should discover devices', async () => { + controller = new Controller(false); + await controller.start(); - homeassistant.discover('0x12345678', WSDCGQ11LM, false); - expect(mqtt.publish).toHaveBeenCalledTimes(5); + let payload; + await flushPromises(); - // 1 payload = { 'unit_of_measurement': '°C', 'device_class': 'temperature', 'value_template': '{{ value_json.temperature }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_temperature', - 'unique_id': '0x12345678_temperature_zigbee2mqtt', + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_temperature', + 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', 'manufacturer': 'Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[0][3]).toBeNull(); - expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/temperature/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); - // 2 payload = { 'unit_of_measurement': '%', 'device_class': 'humidity', 'value_template': '{{ value_json.humidity }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_humidity', - 'unique_id': '0x12345678_humidity_zigbee2mqtt', + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_humidity', + 'unique_id': '0x0017880104e45522_humidity_zigbee2mqtt', 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', 'manufacturer': 'Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[1][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[1][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[1][3]).toBeNull(); - expect(mqtt.publish.mock.calls[1][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/humidity/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); - // 3 payload = { 'unit_of_measurement': 'hPa', 'device_class': 'pressure', 'value_template': '{{ value_json.pressure }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_pressure', - 'unique_id': '0x12345678_pressure_zigbee2mqtt', + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_pressure', + 'unique_id': '0x0017880104e45522_pressure_zigbee2mqtt', 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', 'manufacturer': 'Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[2][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[2][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[2][3]).toBeNull(); - expect(mqtt.publish.mock.calls[2][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/pressure/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); - // 4 payload = { 'unit_of_measurement': '%', 'device_class': 'battery', 'value_template': '{{ value_json.battery }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_battery', - 'unique_id': '0x12345678_battery_zigbee2mqtt', + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_battery', + 'unique_id': '0x0017880104e45522_battery_zigbee2mqtt', 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', 'manufacturer': 'Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[3][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[3][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[3][3]).toBeNull(); - expect(mqtt.publish.mock.calls[3][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/battery/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); - // 5 payload = { 'unit_of_measurement': '-', 'value_template': '{{ value_json.linkquality }}', - 'state_topic': 'zigbee2mqtt/my_device', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_linkquality', - 'unique_id': '0x12345678_linkquality_zigbee2mqtt', + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_linkquality', + 'unique_id': '0x0017880104e45522_linkquality_zigbee2mqtt', 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', 'manufacturer': 'Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[4][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[4][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[4][3]).toBeNull(); - expect(mqtt.publish.mock.calls[4][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/linkquality/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); }); - it('Should discover devices with precision', () => { - let payload = null; - jest.spyOn(settings, 'getDevice').mockReturnValue({ - friendly_name: 'my_device', + it('Should discover devices with precision', async () => { + settings.set(['devices', '0x0017880104e45522'], { humidity_precision: 0, temperature_precision: 1, pressure_precision: 2, - }); + friendly_name: 'weather_sensor', + }) - homeassistant.discover('0x12345678', WSDCGQ11LM, false); - expect(mqtt.publish).toHaveBeenCalledTimes(5); + controller = new Controller(false); + await controller.start(); + + let payload; + await flushPromises(); - // 1 payload = { 'unit_of_measurement': '°C', 'device_class': 'temperature', - 'value_template': '{{ (value_json.temperature | float) | round(1) }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_temperature', - 'unique_id': '0x12345678_temperature_zigbee2mqtt', + 'value_template': "{{ (value_json.temperature | float) | round(1) }}", + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_temperature', + 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', 'manufacturer': 'Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[0][3]).toBeNull(); - expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/temperature/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); - // 2 payload = { 'unit_of_measurement': '%', 'device_class': 'humidity', 'value_template': '{{ (value_json.humidity | float) | round(0) }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_humidity', - 'unique_id': '0x12345678_humidity_zigbee2mqtt', + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_humidity', + 'unique_id': '0x0017880104e45522_humidity_zigbee2mqtt', 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', 'manufacturer': 'Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[1][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[1][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[1][3]).toBeNull(); - expect(mqtt.publish.mock.calls[1][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/humidity/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); - // 3 payload = { 'unit_of_measurement': 'hPa', 'device_class': 'pressure', 'value_template': '{{ (value_json.pressure | float) | round(2) }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_pressure', - 'unique_id': '0x12345678_pressure_zigbee2mqtt', + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_pressure', + 'unique_id': '0x0017880104e45522_pressure_zigbee2mqtt', 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', 'manufacturer': 'Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[2][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[2][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[2][3]).toBeNull(); - expect(mqtt.publish.mock.calls[2][4]).toBe('homeassistant'); - - // 4 - payload = { - 'unit_of_measurement': '%', - 'device_class': 'battery', - 'value_template': '{{ value_json.battery }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_battery', - 'unique_id': '0x12345678_battery_zigbee2mqtt', - 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', - 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', - 'manufacturer': 'Xiaomi', - }, - 'availability_topic': 'zigbee2mqtt/bridge/state', - }; - - expect(JSON.parse(mqtt.publish.mock.calls[3][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[3][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[3][3]).toBeNull(); - expect(mqtt.publish.mock.calls[3][4]).toBe('homeassistant'); - - // 5 - payload = { - 'unit_of_measurement': '-', - 'value_template': '{{ value_json.linkquality }}', - 'state_topic': 'zigbee2mqtt/my_device', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_linkquality', - 'unique_id': '0x12345678_linkquality_zigbee2mqtt', - 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', - 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', - 'manufacturer': 'Xiaomi', - }, - 'availability_topic': 'zigbee2mqtt/bridge/state', - }; - - expect(JSON.parse(mqtt.publish.mock.calls[4][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[4][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[4][3]).toBeNull(); - expect(mqtt.publish.mock.calls[4][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/pressure/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); }); - it('Should discover devices with overriden user configuration', () => { - let payload = null; - jest.spyOn(settings, 'getDevice').mockReturnValue({ - friendly_name: 'my_device', + it('Should discover devices with overriden user configuration', async () => { + settings.set(['devices', '0x0017880104e45522'], { homeassistant: { expire_after: 30, icon: 'mdi:test', temperature: { expire_after: 90, + device: { + manufacturer: 'From Xiaomi', + sw_version: 'test' + } }, + humidity: { + unique_id: null, + }, + device: { + manufacturer: 'Not from Xiaomi', + model: 'custom model', + } }, - }); + friendly_name: 'weather_sensor', + }) - homeassistant.discover('0x12345678', WSDCGQ11LM, false); - expect(mqtt.publish).toHaveBeenCalledTimes(5); + controller = new Controller(false); + await controller.start(); + + let payload; + await flushPromises(); - // 1 payload = { 'unit_of_measurement': '°C', 'device_class': 'temperature', 'value_template': '{{ value_json.temperature }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_temperature', - 'unique_id': '0x12345678_temperature_zigbee2mqtt', - 'expire_after': 90, - 'icon': 'mdi:test', + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_temperature', + 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', - 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', - 'manufacturer': 'Xiaomi', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': 'test', + 'model': 'custom model', + 'manufacturer': 'From Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', + 'expire_after': 90, + 'icon': 'mdi:test', }; - expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[0][3]).toBeNull(); - expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/temperature/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); - // 2 payload = { 'unit_of_measurement': '%', 'device_class': 'humidity', 'value_template': '{{ value_json.humidity }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_humidity', - 'unique_id': '0x12345678_humidity_zigbee2mqtt', - 'expire_after': 30, - 'icon': 'mdi:test', + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_humidity', 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', - 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', - 'manufacturer': 'Xiaomi', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, + 'model': 'custom model', + 'manufacturer': 'Not from Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', - }; - - expect(JSON.parse(mqtt.publish.mock.calls[1][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[1][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[1][3]).toBeNull(); - expect(mqtt.publish.mock.calls[1][4]).toBe('homeassistant'); - - // 3 - payload = { - 'unit_of_measurement': 'hPa', - 'device_class': 'pressure', - 'value_template': '{{ value_json.pressure }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_pressure', - 'unique_id': '0x12345678_pressure_zigbee2mqtt', 'expire_after': 30, 'icon': 'mdi:test', - 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', - 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', - 'manufacturer': 'Xiaomi', - }, - 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[2][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[2][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[2][3]).toBeNull(); - expect(mqtt.publish.mock.calls[2][4]).toBe('homeassistant'); - - // 4 - payload = { - 'unit_of_measurement': '%', - 'device_class': 'battery', - 'value_template': '{{ value_json.battery }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_battery', - 'unique_id': '0x12345678_battery_zigbee2mqtt', - 'expire_after': 30, - 'icon': 'mdi:test', - 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', - 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', - 'manufacturer': 'Xiaomi', - }, - 'availability_topic': 'zigbee2mqtt/bridge/state', - }; - - expect(JSON.parse(mqtt.publish.mock.calls[3][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[3][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[3][3]).toBeNull(); - expect(mqtt.publish.mock.calls[3][4]).toBe('homeassistant'); - - // 5 - payload = { - 'unit_of_measurement': '-', - 'value_template': '{{ value_json.linkquality }}', - 'state_topic': 'zigbee2mqtt/my_device', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_linkquality', - 'unique_id': '0x12345678_linkquality_zigbee2mqtt', - 'expire_after': 30, - 'icon': 'mdi:test', - 'device': { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', - 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', - 'manufacturer': 'Xiaomi', - }, - 'availability_topic': 'zigbee2mqtt/bridge/state', - }; - - expect(JSON.parse(mqtt.publish.mock.calls[4][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[4][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[4][3]).toBeNull(); - expect(mqtt.publish.mock.calls[4][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/humidity/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); }); - it('Should discover devices with fan', () => { - let payload = null; - jest.spyOn(settings, 'getDevice').mockReturnValue({friendly_name: 'my_device'}); + it('Should discover devices with fan', async () => { + controller = new Controller(false); + await controller.start(); - homeassistant.discover('0x12345678', FAN99432, false); - expect(mqtt.publish).toHaveBeenCalledTimes(3); + let payload; + await flushPromises(); - // 1 payload = { - name: 'my_device_fan', - state_topic: 'zigbee2mqtt/my_device', - state_value_template: '{{ value_json.fan_state }}', - command_topic: 'zigbee2mqtt/my_device/set/fan_state', - speed_state_topic: 'zigbee2mqtt/my_device', - speed_value_template: '{{ value_json.fan_mode }}', - speed_command_topic: 'zigbee2mqtt/my_device/set/fan_mode', - unique_id: '0x12345678_fan_zigbee2mqtt', - speeds: ['off', 'low', 'medium', 'high', 'on', 'auto', 'smart'], - device: { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', - 'manufacturer': 'Hampton Bay', - 'model': 'Universal wink enabled white ceiling fan premier remote control (99432)', + "state_topic":"zigbee2mqtt/fan", + "state_value_template":"{{ value_json.fan_state }}", + "command_topic":"zigbee2mqtt/fan/set/fan_state", + "speed_state_topic":"zigbee2mqtt/fan", + "speed_command_topic":"zigbee2mqtt/fan/set/fan_mode", + "speed_value_template":"{{ value_json.fan_mode }}", + "speeds":[ + "off", + "low", + "medium", + "high", + "on", + "auto", + "smart" + ], + "json_attributes_topic":"zigbee2mqtt/fan", + "name":"fan_fan", + "unique_id":"0x0017880104e45548_fan_zigbee2mqtt", + "device":{ + "identifiers":[ + "zigbee2mqtt_0x0017880104e45548" + ], + "name":"fan", + "sw_version":this.version, + "model":"Universal wink enabled white ceiling fan premier remote control (99432)", + "manufacturer":"Hampton Bay" }, - availability_topic: 'zigbee2mqtt/bridge/state', - json_attributes_topic: 'zigbee2mqtt/my_device', - }; + "availability_topic":"zigbee2mqtt/bridge/state" + }; - expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[0][3]).toBeNull(); - expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/fan/0x0017880104e45548/fan/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); }); - it('Should discover devices with cover_position', () => { - let payload = null; - jest.spyOn(settings, 'getDevice').mockReturnValue({friendly_name: 'my_device'}); + it('Should discover devices with cover_position', async () => { + controller = new Controller(false); + await controller.start(); - homeassistant.discover('0x12345678', SV01, false); - expect(mqtt.publish).toHaveBeenCalledTimes(5); + let payload; + await flushPromises(); - // 1 payload = { - name: 'my_device_cover', - command_topic: 'zigbee2mqtt/my_device/set', - position_topic: 'zigbee2mqtt/my_device', - set_position_topic: 'zigbee2mqtt/my_device/set', + command_topic: 'zigbee2mqtt/smart_vent/set', + position_topic: 'zigbee2mqtt/smart_vent', + set_position_topic: 'zigbee2mqtt/smart_vent/set', set_position_template: '{ "position": {{ position }} }', value_template: '{{ value_json.position }}', - unique_id: '0x12345678_cover_zigbee2mqtt', - device: { - 'identifiers': ['zigbee2mqtt_0x12345678'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', - 'model': 'Smart vent (SV01)', - 'manufacturer': 'Keen Home', + name: 'smart_vent_cover', + unique_id: '0x0017880104e45551_cover_zigbee2mqtt', + device: + { + identifiers: [ 'zigbee2mqtt_0x0017880104e45551' ], + name: 'smart_vent', + sw_version: this.version, + model: 'Smart vent (SV01)', + manufacturer: 'Keen Home' }, - availability_topic: 'zigbee2mqtt/bridge/state', + availability_topic: 'zigbee2mqtt/bridge/state' }; - expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[0][3]).toBeNull(); - expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/cover/0x0017880104e45551/cover/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); }); - it('Should discover devices with overriden user configuration in device', () => { - let payload = null; - jest.spyOn(settings, 'getDevice').mockReturnValue({ - friendly_name: 'my_device', - homeassistant: { - device: { - identifiers: ['test123'], - }, - temperature: { - expire_after: 90, - }, - }, - }); + it('Should discover devices with custom homeassistant_discovery_topic', async () => { + settings.set(['advanced', 'homeassistant_discovery_topic'], 'my_custom_discovery_topic') + controller = new Controller(false); + await controller.start(); - homeassistant.discover('0x12345678', WSDCGQ11LM, false); - expect(mqtt.publish).toHaveBeenCalledTimes(5); + let payload; + await flushPromises(); - // 1 payload = { 'unit_of_measurement': '°C', 'device_class': 'temperature', 'value_template': '{{ value_json.temperature }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_temperature', - 'unique_id': '0x12345678_temperature_zigbee2mqtt', - 'expire_after': 90, + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_temperature', + 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', 'device': { - 'identifiers': ['test123'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', 'manufacturer': 'Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[0][3]).toBeNull(); - expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'my_custom_discovery_topic/sensor/0x0017880104e45522/temperature/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); }); - it('Should discover devices with overriden user configuration in device in temperature', () => { - let payload = null; - jest.spyOn(settings, 'getDevice').mockReturnValue({ - friendly_name: 'my_device', - homeassistant: { - temperature: { - expire_after: 90, - device: { - identifiers: ['test'], - }, - }, - }, - }); + it('Should throw error when starting with attributes output', async () => { + settings.set(['experimental', 'output'], 'attribute') + expect(() => { + controller = new Controller(false); + }).toThrowError('Home Assitant integration is not possible with attribute output!'); + }); - homeassistant.discover('0x12345678', WSDCGQ11LM, false); - expect(mqtt.publish).toHaveBeenCalledTimes(5); + it('Should warn when starting with cache_state false', async () => { + settings.set(['advanced', 'cache_state'], false); + logger.warn.mockClear(); + controller = new Controller(false); + expect(logger.warn).toHaveBeenCalledWith("In order for HomeAssistant integration to work properly set `cache_state: true"); + }); - // 1 - payload = { + it('Shouldt discover when already discovered', async () => { + controller = new Controller(false); + await controller.start(); + await flushPromises(); + const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const data = {measuredValue: -85} + const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + MQTT.publish.mockClear(); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + // 1 publish is the publish from deviceReceive + expect(MQTT.publish).toHaveBeenCalledTimes(1); + }); + + it('Should discover when not discovered yet', async () => { + controller = new Controller(false); + await controller.start(); + await flushPromises(); + controller.extensions.find((e) => e.constructor.name === 'HomeAssistant').discovered = {}; + const device = zigbeeHerdsman.devices.WSDCGQ11LM; + const data = {measuredValue: -85} + const payload = {data, cluster: 'msTemperatureMeasurement', device, endpoint: device.getEndpoint(1), type: 'attributeReport', linkquality: 10}; + MQTT.publish.mockClear(); + await zigbeeHerdsman.events.message(payload); + await flushPromises(); + const payloadHA = { 'unit_of_measurement': '°C', 'device_class': 'temperature', 'value_template': '{{ value_json.temperature }}', - 'json_attributes_topic': 'zigbee2mqtt/my_device', - 'state_topic': 'zigbee2mqtt/my_device', - 'name': 'my_device_temperature', - 'unique_id': '0x12345678_temperature_zigbee2mqtt', - 'expire_after': 90, + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_temperature', + 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', 'device': { - 'identifiers': ['test'], - 'name': 'my_device', - 'sw_version': 'Zigbee2mqtt test', + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', 'manufacturer': 'Xiaomi', }, 'availability_topic': 'zigbee2mqtt/bridge/state', }; - expect(JSON.parse(mqtt.publish.mock.calls[0][1])).toStrictEqual(payload); - expect(mqtt.publish.mock.calls[0][2]).toStrictEqual({retain: true, qos: 0}); - expect(mqtt.publish.mock.calls[0][3]).toBeNull(); - expect(mqtt.publish.mock.calls[0][4]).toBe('homeassistant'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/temperature/config', + JSON.stringify(payloadHA), + { retain: true, qos: 0 }, + expect.any(Function), + ); }); - it('Should discover devices with a custom discovery topic', () => { - jest.spyOn(settings, 'get').mockReturnValue({ - mqtt: { - base_topic: 'zigbee2mqtt', - }, - experimental: { - output: 'json', - }, - advanced: { - homeassistant_discovery_topic: 'my_custom_discovery_topic', - }, - }); - - homeassistant = new HomeassistantExtension(null, mqtt, null, null); - - jest.spyOn(settings, 'getDevice').mockReturnValue({ - friendly_name: 'my_device', - homeassistant: { - temperature: { - expire_after: 90, - device: { - identifiers: ['test'], - }, - }, - }, - }); - - homeassistant.discover('0x12345678', WSDCGQ11LM, false); - - expect(mqtt.publish).toHaveBeenCalledTimes(5); - expect(mqtt.publish.mock.calls[0][4]).toBe('my_custom_discovery_topic'); + it('Shouldnt discover when message has no device yet', async () => { + controller = new Controller(); + await controller.start(); + await flushPromises(); + controller.extensions.find((e) => e.constructor.name === 'HomeAssistant').discovered = {}; + const device = zigbeeHerdsman.devices.bulb; + const payload = {ieeeAddr: device.ieeeAddr}; + MQTT.publish.mockClear(); + await zigbeeHerdsman.events.deviceLeave(payload); + await flushPromises(); + // 1 publish is from device_removed + expect(MQTT.publish).toHaveBeenCalledTimes(1); }); - it('Should subscribe to custom status topic', () => { - jest.spyOn(settings, 'get').mockReturnValue({ - experimental: { - output: 'json', - }, - advanced: { - homeassistant_status_topic: 'my_custom_status_topic', - }, - }); + it('Should send all status when home assistant comes online', async () => { + jest.useFakeTimers(); + data.writeDefaultState(); + controller = new Controller(); + await controller.start(); + await flushPromises(); + MQTT.publish.mockClear(); + await MQTT.events.message('hass/status', 'online'); + await flushPromises(); + jest.runOnlyPendingTimers(); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/bulb', + '{"state":"ON","brightness":50,"color_temp":370,"linkquality":99}', + { retain: true, qos: 0 }, + expect.any(Function) + ); + expect(MQTT.publish).toHaveBeenCalledWith( + 'zigbee2mqtt/remote', + '{"brightness":255}', + { retain: true, qos: 0 }, + expect.any(Function) + ); + }); - const zigbee = { - getAllClients: jest.fn().mockReturnValue([]), + it('Shouldnt send all status when home assistant comes offline', async () => { + jest.useFakeTimers(); + data.writeDefaultState(); + controller = new Controller(); + await controller.start(); + await flushPromises(); + MQTT.publish.mockClear(); + await MQTT.events.message('hass/status', 'offline'); + await flushPromises(); + jest.runOnlyPendingTimers(); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); + + it('Shouldnt send all status when home assistant comes online with different topic', async () => { + jest.useFakeTimers(); + data.writeDefaultState(); + controller = new Controller(); + await controller.start(); + await flushPromises(); + MQTT.publish.mockClear(); + await MQTT.events.message('hass/status_different', 'offline'); + await flushPromises(); + jest.runOnlyPendingTimers(); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(0); + }); + + it('Should discover devices with availability', async () => { + settings.set(['advanced', 'availability_timeout'], 1) + controller = new Controller(false); + await controller.start(); + + let payload; + await flushPromises(); + + payload = { + 'unit_of_measurement': '°C', + 'device_class': 'temperature', + 'value_template': '{{ value_json.temperature }}', + 'state_topic': 'zigbee2mqtt/weather_sensor', + 'json_attributes_topic': 'zigbee2mqtt/weather_sensor', + 'name': 'weather_sensor_temperature', + 'unique_id': '0x0017880104e45522_temperature_zigbee2mqtt', + 'device': { + 'identifiers': ['zigbee2mqtt_0x0017880104e45522'], + 'name': 'weather_sensor', + 'sw_version': this.version, + 'model': 'Aqara temperature, humidity and pressure sensor (WSDCGQ11LM)', + 'manufacturer': 'Xiaomi', + }, + 'availability_topic': 'zigbee2mqtt/weather_sensor/availability', }; - mqtt = { - subscribe: jest.fn(), - }; - - - homeassistant = new HomeassistantExtension(zigbee, mqtt, null, null); - - homeassistant.onMQTTConnected(); - - expect(mqtt.subscribe).toHaveBeenCalledTimes(1); - expect(mqtt.subscribe.mock.calls[0][0]).toBe('my_custom_status_topic'); + expect(MQTT.publish).toHaveBeenCalledWith( + 'homeassistant/sensor/0x0017880104e45522/temperature/config', + JSON.stringify(payload), + { retain: true, qos: 0 }, + expect.any(Function), + ); }); }); diff --git a/test/logger.test.js b/test/logger.test.js new file mode 100644 index 000000000..cee7a66dc --- /dev/null +++ b/test/logger.test.js @@ -0,0 +1,36 @@ +const tmp = require('tmp'); +const dir = tmp.dirSync(); +const data = require('./stub/data'); +const settings = require('../lib/util/settings'); +settings.set(['advanced', 'log_directory'], dir.name + '/%TIMESTAMP%'); +const logger = require('../lib/util/logger.js'); +const fs = require('fs'); +const path = require('path'); + +describe('Logger', () => { + it('Create log directory', () => { + const dirs = fs.readdirSync(dir.name); + expect(dirs.length).toBe(1); + }); + + it('Should cleanup', () => { + for (let i = 0; i < 20; i++) { + fs.mkdirSync(path.join(dir.name, `log_${i}`)); + } + + expect(fs.readdirSync(dir.name).length).toBe(21); + logger.cleanup(); + expect(fs.readdirSync(dir.name).length).toBe(10); + }) + + it('Should not cleanup when there is no timestamp set', () => { + for (let i = 30; i < 40; i++) { + fs.mkdirSync(path.join(dir.name, `log_${i}`)); + } + + settings.set(['advanced', 'log_directory'], dir.name + '/bla'); + expect(fs.readdirSync(dir.name).length).toBe(20); + logger.cleanup(); + expect(fs.readdirSync(dir.name).length).toBe(20); + }) +}); diff --git a/test/networkmap.test.js b/test/networkmap.test.js new file mode 100644 index 000000000..406cd6ea7 --- /dev/null +++ b/test/networkmap.test.js @@ -0,0 +1,163 @@ +const data = require('./stub/data'); +const logger = require('./stub/logger'); +const zigbeeHerdsman = require('./stub/zigbeeHerdsman'); + +const {coordinator, bulb, bulb_color, WXKG02LM, CC2530_ROUTER, unsupported_router} = zigbeeHerdsman.devices; + +zigbeeHerdsman.returnDevices.push(coordinator.ieeeAddr); +zigbeeHerdsman.returnDevices.push(bulb.ieeeAddr); +zigbeeHerdsman.returnDevices.push(bulb_color.ieeeAddr); +zigbeeHerdsman.returnDevices.push(WXKG02LM.ieeeAddr); +zigbeeHerdsman.returnDevices.push(CC2530_ROUTER.ieeeAddr); +zigbeeHerdsman.returnDevices.push(unsupported_router.ieeeAddr); +const MQTT = require('./stub/mqtt'); +const settings = require('../lib/util/settings'); +const Controller = require('../lib/controller'); +const flushPromises = () => new Promise(setImmediate); +const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + +const mocksClear = [MQTT.publish, logger.warn, logger.debug]; + +describe('Networkmap', () => { + let controller; + + beforeAll(async () => { + data.writeDefaultConfiguration(); + settings._reRead(); + data.writeEmptyState(); + controller = new Controller(); + await controller.start(); + mocksClear.forEach((m) => m.mockClear()); + await flushPromises(); + }); + + beforeEach(async () => { + mocksClear.forEach((m) => m.mockClear()); + await flushPromises(); + }); + + function mock() { + /** + * Topology + * + * | -> bulb_color -> unsupported_router (offline) + * coordinator | ^ ^ + * | | | (not valid) + * | -> bulb | + * | -> CC2530_ROUTER -> WXKG02LM + * + */ + coordinator.lqi = () => {return {neighbors: [ + {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 2, depth: 1, linkquality: 120}, + {ieeeAddr: bulb.ieeeAddr, networkAddress: bulb.networkAddress, relationship: 2, depth: 1, linkquality: 92} + ]}}; + coordinator.routingTable = () => {return {table: [ + {destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}, + ]}}; + + bulb.lqi = () => {return {neighbors: [ + {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 1, depth: 2, linkquality: 110}, + {ieeeAddr: CC2530_ROUTER.ieeeAddr, networkAddress: CC2530_ROUTER.networkAddress, relationship: 1, depth: 2, linkquality: 100} + ]}}; + bulb.routingTable = () => {return {table: []}}; + + bulb_color.lqi = () => {return {neighbors: []}} + bulb_color.routingTable = () => {return {table: []}}; + + CC2530_ROUTER.lqi = () => {return {neighbors: [ + {ieeeAddr: WXKG02LM.ieeeAddr, networkAddress: WXKG02LM.networkAddress, relationship: 1, depth: 2, linkquality: 130}, + {ieeeAddr: bulb_color.ieeeAddr, networkAddress: bulb_color.networkAddress, relationship: 4, depth: 2, linkquality: 130}, + ]}}; + CC2530_ROUTER.routingTable = () => {return {table: []}}; + + unsupported_router.lqi = () => {throw new Error('failed')}; + unsupported_router.routingTable = () => {throw new Error('failed')}; + } + + it('Output raw networkmap', async () => { + mock(); + MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'raw'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + let call = MQTT.publish.mock.calls[0]; + expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw'); + + const conv = (device) => {return {ieeeAddr: device.ieeeAddr, networkAddress: device.networkAddress}}; + + const expected = { + "nodes":[ + {"ieeeAddr":coordinator.ieeeAddr,"friendlyName":"Coordinator","type":"Coordinator","networkAddress":0,"modelID":null,"failed":[]}, + {"ieeeAddr":bulb.ieeeAddr,"friendlyName":"bulb","type":"Router","networkAddress":40369,"modelID":"TRADFRI bulb E27 WS opal 980lm","failed":[]}, + {"ieeeAddr":bulb_color.ieeeAddr,"friendlyName":"bulb_color","type":"Router","networkAddress":40399,"modelID":"LLC020","failed":[]}, + {"ieeeAddr":WXKG02LM.ieeeAddr,"friendlyName":"button_double_key","type":"EndDevice","networkAddress":6538,"modelID":"lumi.sensor_86sw2.es1"}, + {"ieeeAddr":unsupported_router.ieeeAddr,"friendlyName":"0x0017880104e45525","type":"Router","networkAddress":6536,"modelID":"notSupportedModelID","manufacturerName": "Boef","failed":['lqi', 'routingTable']}, + {"ieeeAddr":CC2530_ROUTER.ieeeAddr,"friendlyName":"cc2530_router","type":"Router","networkAddress":6540,"modelID":"lumi.router","failed":[]}, + ], + "links":[ + {depth: 1, linkquality: 120, routes: [], source: conv(bulb_color), target: conv(coordinator)}, + {depth: 1, linkquality: 92, routes: [{destinationAddress: CC2530_ROUTER.networkAddress, status: 'ACTIVE', nextHop: bulb.networkAddress}], source: conv(bulb), target: conv(coordinator)}, + {depth: 2, linkquality: 110, routes: [], source: conv(bulb_color), target: conv(bulb)}, + {depth: 2, linkquality: 100, routes: [], source: conv(CC2530_ROUTER), target: conv(bulb)}, + {depth: 2, linkquality: 130, routes: [], source: conv(WXKG02LM), target: conv(CC2530_ROUTER)} + ] + }; + + expect(JSON.parse(call[1])).toStrictEqual(expected); + + /** + * Check again without routes + */ + MQTT.publish.mockClear(); + MQTT.events.message('zigbee2mqtt/bridge/networkmap', 'raw'); + await flushPromises(); + call = MQTT.publish.mock.calls[0]; + expect(MQTT.publish).toHaveBeenCalledTimes(1); + expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/raw'); + + // Remove routing information + expected.nodes.forEach((n) => { + if (n.failed && n.failed.includes('routingTable')) { + n.failed.splice(n.failed.indexOf('routingTable'), 1); + } + }); + + expected.links.forEach((l) => l.routes = []) + expect(JSON.parse(call[1])).toStrictEqual(expected); + }); + + it('Output graphviz networkmap', async () => { + mock(); + const device = zigbeeHerdsman.devices.bulb_color; + const endpoint = device.getEndpoint(1); + const data = {modelID: 'test'} + const payload = {data, cluster: 'genOnOff', device, endpoint, type: 'readResponse', linkquality: 10}; + await zigbeeHerdsman.events.message(payload); + MQTT.events.message('zigbee2mqtt/bridge/networkmap/routes', 'graphviz'); + await flushPromises(); + expect(MQTT.publish).toHaveBeenCalledTimes(1); + let call = MQTT.publish.mock.calls[0]; + expect(call[0]).toStrictEqual('zigbee2mqtt/bridge/networkmap/graphviz'); + + const expected = `digraph G { + node[shape=record]; + "0x00124b00120144ae" [style="bold, filled", fillcolor="#e04e5d", fontcolor="#ffffff", label="{Coordinator|0x00124b00120144ae (0)|Last seen: 00:00:00s ago}"]; + "0x000b57fffec6a5b2" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb|0x000b57fffec6a5b2 (40369)|IKEA TRADFRI LED bulb E26/E27 980 lumen, dimmable, white spectrum, opal white (LED1545G12)|Last seen: unknown}"]; + "0x000b57fffec6a5b2" -> "0x00124b00120144ae" [penwidth=2, weight=1, color="#009900", label="92 (routes: 6540)"] + "0x000b57fffec6a5b3" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{bulb_color|0x000b57fffec6a5b3 (40399)|Philips Hue Go (7146060PH)|Last seen: 00:00:00s ago}"]; + "0x000b57fffec6a5b3" -> "0x00124b00120144ae" [penwidth=0.5, weight=0, color="#994444", label="120"] + "0x000b57fffec6a5b3" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="110"] + "0x0017880104e45521" [style="rounded, dashed, filled", fillcolor="#fff8ce", fontcolor="#000000", label="{button_double_key|0x0017880104e45521 (6538)|Xiaomi Aqara double key wireless wall switch (WXKG02LM)|Last seen: unknown}"]; + "0x0017880104e45521" -> "0x0017880104e45559" [penwidth=1, weight=0, color="#994444", label="130"] + "0x0017880104e45525" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{0x0017880104e45525|0x0017880104e45525 (6536)failed: lqi,routingTable|Boef notSupportedModelID|Last seen: unknown}"]; + "0x0017880104e45559" [style="rounded, filled", fillcolor="#4ea3e0", fontcolor="#ffffff", label="{cc2530_router|0x0017880104e45559 (6540)|Custom devices (DiY) [CC2530 router](http://ptvo.info/cc2530-based-zigbee-coordinator-and-router-112/) (CC2530.ROUTER)|Last seen: unknown}"]; + "0x0017880104e45559" -> "0x000b57fffec6a5b2" [penwidth=0.5, weight=0, color="#994444", label="100"] + }`; + + const expectedLines = expected.split('\n'); + const actualLines = call[1].split('\n'); + + for (let i = 0; i < expectedLines.length; i++) { + expect(actualLines[i].trim()).toStrictEqual(expectedLines[i].trim()); + } + }); +}); diff --git a/test/settings.test.js b/test/settings.test.js index 740ad2e6d..088d78106 100644 --- a/test/settings.test.js +++ b/test/settings.test.js @@ -1,322 +1,397 @@ -const objectAssignDeep = require('object-assign-deep'); - -const data = require('../lib/util/data'); +require('./stub/logger'); +require('./stub/data'); +const data = require('../lib/util/data.js'); const settings = require('../lib/util/settings.js'); -const fs = require('../lib/util/fs'); -const utils = require('./utils'); - +const fs = require('fs'); const configurationFile = data.joinPath('configuration.yaml'); const devicesFile = data.joinPath('devices.yaml'); const groupsFile = data.joinPath('groups.yaml'); +const yaml = require('js-yaml'); describe('Settings', () => { - const write = (file, json) => fs.writeYaml(file, json); - - const read = (file) => fs.readYaml(file); - - const files = new Map(); - - beforeAll(() => { - utils.stubLogger(jest); - - jest.spyOn(fs, 'readYaml').mockImplementation((file) => { - if (files.has(file)) return objectAssignDeep.noMutate(files.get(file)); - throw new Error(`Fake file not found: ${file}`); - }); - jest.spyOn(fs, 'readYamlIfExists').mockImplementation((file) => { - if (files.has(file)) return objectAssignDeep.noMutate(files.get(file)); - return null; - }); - jest.spyOn(fs, 'writeYaml').mockImplementation((file, content) => { - files.set(file, objectAssignDeep.noMutate(content)); - }); - }); + const write = (file, json) => { + fs.writeFileSync(file, yaml.safeDump(json)) + settings._reRead(); + }; + const read = (file) => yaml.safeLoad(fs.readFileSync(file, 'utf8')); + const remove = (file) => { + if (fs.existsSync(file)) fs.unlinkSync(file); + } beforeEach(() => { - settings._clear(); - files.clear(); + remove(configurationFile); + remove(devicesFile); + remove(groupsFile); }); - afterAll(() => { - fs.readYaml.mockRestore(); - fs.readYamlIfExists.mockRestore(); - fs.writeYaml.mockRestore(); + it('Should return default settings', () => { + write(configurationFile, {}); + const s = settings.get(); + expect(s).toStrictEqual(settings._getDefaults()); }); - describe('Settings', () => { - it('Should return default settings', () => { - write(configurationFile, {}); - const s = settings.get(); - expect(s).toStrictEqual(settings._getDefaults()); - }); + it('Should return settings', () => { + write(configurationFile, {permit_join: true}); + const s = settings.get(); + const expected = settings._getDefaults(); + expected.permit_join = true; + expect(s).toStrictEqual(expected); + }); - it('Should return settings', () => { - write(configurationFile, {permit_join: true}); - const s = settings.get(); - const expected = settings._getDefaults(); - expected.permit_join = true; - expect(s).toStrictEqual(expected); - }); + it('Should add devices', () => { + write(configurationFile, {}); + settings.addDevice('0x12345678'); - it('Should add devices', () => { - write(configurationFile, {}); - settings.addDevice('0x12345678'); - - const actual = read(configurationFile); - const expected = { - devices: { - '0x12345678': { - friendly_name: '0x12345678', - retain: false, - }, + const actual = read(configurationFile); + const expected = { + devices: { + '0x12345678': { + friendly_name: '0x12345678', + retain: false, }, - }; + }, + }; - expect(actual).toStrictEqual(expected); - }); + expect(actual).toStrictEqual(expected); + }); - it('Should read devices', () => { - const content = { - devices: { - '0x12345678': { - friendly_name: '0x12345678', - retain: false, - }, + it('Should read devices', () => { + const content = { + devices: { + '0x12345678': { + friendly_name: '0x12345678', + retain: false, }, - }; + }, + }; - write(configurationFile, content); + write(configurationFile, content); - const device = settings.getDevice('0x12345678'); - const expected = { + const device = settings.getDevice('0x12345678'); + const expected = { + ID: "0x12345678", + friendlyName: "0x12345678", + friendly_name: '0x12345678', + retain: false, + }; + + expect(device).toStrictEqual(expected); + }); + + it('Should read devices form a separate file', () => { + const contentConfiguration = { + devices: 'devices.yaml', + }; + + const contentDevices = { + '0x12345678': { friendly_name: '0x12345678', retain: false, - }; + }, + }; - expect(device).toStrictEqual(expected); - }); + write(configurationFile, contentConfiguration); + write(devicesFile, contentDevices); + const device = settings.getDevice('0x12345678'); + const expected = { + ID: "0x12345678", + friendlyName: "0x12345678", + friendly_name: '0x12345678', + retain: false, + }; - it('Should read devices form a separate file', () => { - const contentConfiguration = { - devices: 'devices.yaml', - }; + expect(device).toStrictEqual(expected); + }); - const contentDevices = { - '0x12345678': { - friendly_name: '0x12345678', - retain: false, - }, - }; + it('Should add devices to a separate file', () => { + const contentConfiguration = { + devices: 'devices.yaml', + }; - write(devicesFile, contentDevices); - write(configurationFile, contentConfiguration); - - const device = settings.getDevice('0x12345678'); - const expected = { + const contentDevices = { + '0x12345678': { friendly_name: '0x12345678', retain: false, - }; + }, + }; - expect(device).toStrictEqual(expected); - }); + write(configurationFile, contentConfiguration); + write(devicesFile, contentDevices); - it('Should add devices to a separate file', () => { - const contentConfiguration = { - devices: 'devices.yaml', - }; + settings.addDevice('0x1234'); - const contentDevices = { - '0x12345678': { - friendly_name: '0x12345678', - retain: false, - }, - }; + expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'}); - write(devicesFile, contentDevices); - write(configurationFile, contentConfiguration); - - settings.addDevice('0x1234'); - - expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'}); - - const expected = { - '0x12345678': { - friendly_name: '0x12345678', - retain: false, - }, - '0x1234': { - friendly_name: '0x1234', - retain: false, - }, - }; - - expect(read(devicesFile)).toStrictEqual(expected); - }); - - it('Should add devices to a separate file if devices.yaml doesnt exist', () => { - const contentConfiguration = { - devices: 'devices.yaml', - }; - - write(configurationFile, contentConfiguration); - - settings.addDevice('0x1234'); - - expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'}); - - const expected = { - '0x1234': { - friendly_name: '0x1234', - retain: false, - }, - }; - - expect(read(devicesFile)).toStrictEqual(expected); - } - ); - - it('Should add and remove devices to a separate file if devices.yaml doesnt exist', () => { - const contentConfiguration = { - devices: 'devices.yaml', - }; - - write(configurationFile, contentConfiguration); - - settings.addDevice('0x1234'); - expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'}); - - settings.removeDevice('0x1234'); - expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'}); - - expect(read(devicesFile)).toStrictEqual({}); - } - ); - - it('Should read groups', () => { - const content = { - groups: { - '1': { - friendly_name: '123', - }, - }, - }; - - write(configurationFile, content); - - const group = settings.getGroup('1'); - const expected = { - friendly_name: '123', - devices: [], - }; - - expect(group).toStrictEqual(expected); - }); - - it('Should read groups from a separate file', () => { - const contentConfiguration = { - groups: 'groups.yaml', - }; - - const contentGroups = { - '1': { - friendly_name: '123', - }, - }; - - write(groupsFile, contentGroups); - write(configurationFile, contentConfiguration); - - const group = settings.getGroup('1'); - const expected = { - friendly_name: '123', - devices: [], - }; - - expect(group).toStrictEqual(expected); - }); - - it('Combine everything! groups and devices from separate file :)', () => { - const contentConfiguration = { - devices: 'devices.yaml', - groups: 'groups.yaml', - }; - - const contentGroups = { - '1': { - friendly_name: '123', - devices: [], - }, - }; - - write(groupsFile, contentGroups); - write(configurationFile, contentConfiguration); - - const expectedConfiguration = { - devices: 'devices.yaml', - groups: 'groups.yaml', - }; - - expect(read(configurationFile)).toStrictEqual(expectedConfiguration); - - settings.addDevice('0x1234'); - - expect(read(configurationFile)).toStrictEqual(expectedConfiguration); - - const expectedDevice = { - '0x1234': { - friendly_name: '0x1234', - retain: false, - }, - }; - - expect(read(devicesFile)).toStrictEqual(expectedDevice); - - const group = settings.getGroup('1'); - const expectedGroup = { - friendly_name: '123', - devices: [], - }; - - expect(group).toStrictEqual(expectedGroup); - - expect(read(configurationFile)).toStrictEqual(expectedConfiguration); - - const expectedDevice2 = { + const expected = { + '0x12345678': { + friendly_name: '0x12345678', + retain: false, + }, + '0x1234': { friendly_name: '0x1234', retain: false, - }; + }, + }; - expect(settings.getDevice('0x1234')).toStrictEqual(expectedDevice2); + expect(read(devicesFile)).toStrictEqual(expected); + }); + + it('Should add devices to a separate file if devices.yaml doesnt exist', () => { + const contentConfiguration = { + devices: 'devices.yaml', + }; + + write(configurationFile, contentConfiguration); + + settings.addDevice('0x1234'); + + expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'}); + + const expected = { + '0x1234': { + friendly_name: '0x1234', + retain: false, + }, + }; + + expect(read(devicesFile)).toStrictEqual(expected); + } + ); + + it('Should add and remove devices to a separate file if devices.yaml doesnt exist', () => { + const contentConfiguration = { + devices: 'devices.yaml', + }; + + write(configurationFile, contentConfiguration); + + settings.addDevice('0x1234'); + expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'}); + + settings.removeDevice('0x1234'); + expect(read(configurationFile)).toStrictEqual({devices: 'devices.yaml'}); + + expect(read(devicesFile)).toStrictEqual({}); + } + ); + + it('Should read groups', () => { + const content = { + groups: { + '1': { + friendly_name: '123', + }, + }, + }; + + write(configurationFile, content); + + const group = settings.getGroup('1'); + const expected = { + ID: 1, + friendlyName: '123', + friendly_name: '123', + devices: [], + }; + + expect(group).toStrictEqual(expected); + }); + + it('Should read groups from a separate file', () => { + const contentConfiguration = { + groups: 'groups.yaml', + }; + + const contentGroups = { + '1': { + friendly_name: '123', + }, + }; + + write(configurationFile, contentConfiguration); + write(groupsFile, contentGroups); + + const group = settings.getGroup('1'); + const expected = { + ID: 1, + friendlyName: '123', + friendly_name: '123', + devices: [], + }; + + expect(group).toStrictEqual(expected); + }); + + it('Combine everything! groups and devices from separate file :)', () => { + const contentConfiguration = { + devices: 'devices.yaml', + groups: 'groups.yaml', + }; + + const contentGroups = { + '1': { + friendly_name: '123', + devices: [], + }, + }; + write(configurationFile, contentConfiguration); + write(groupsFile, contentGroups); + + const expectedConfiguration = { + devices: 'devices.yaml', + groups: 'groups.yaml', + }; + + expect(read(configurationFile)).toStrictEqual(expectedConfiguration); + + settings.addDevice('0x1234'); + + expect(read(configurationFile)).toStrictEqual(expectedConfiguration); + + const expectedDevice = { + '0x1234': { + friendly_name: '0x1234', + retain: false, + }, + }; + + expect(read(devicesFile)).toStrictEqual(expectedDevice); + + const group = settings.getGroup('1'); + const expectedGroup = { + ID: 1, + friendlyName: '123', + friendly_name: '123', + devices: [], + }; + + expect(group).toStrictEqual(expectedGroup); + + expect(read(configurationFile)).toStrictEqual(expectedConfiguration); + + const expectedDevice2 = { + ID: '0x1234', + friendlyName: '0x1234', + friendly_name: '0x1234', + retain: false, + }; + + expect(settings.getDevice('0x1234')).toStrictEqual(expectedDevice2); + }); + + it('Should add groups', () => { + write(configurationFile, {}); + + const added = settings.addGroup('test123'); + const expected = { + '1': { + friendly_name: 'test123', + }, + }; + + expect(settings.get().groups).toStrictEqual(expected); + }); + + it('Should not add duplicate groups', () => { + write(configurationFile, {}); + + settings.addGroup('test123'); + expect(() => { + settings.addGroup('test123'); + }).toThrow(new Error("Group 'test123' already exists")); + const expected = { + '1': { + friendly_name: 'test123', + }, + }; + + expect(settings.get().groups).toStrictEqual(expected); + }); + + it('Should add devices to groups', () => { + write(configurationFile, { + devices: { + '0x123': { + friendly_name: 'bulb', + retain: true, + } + } }); - it('Should add groups', () => { - write(configurationFile, {}); + settings.addGroup('test123'); + settings.addDeviceToGroup('test123', ['0x123']); + const expected = { + '1': { + friendly_name: 'test123', + devices: ['0x123'], + }, + }; - const added = settings.addGroup('test123'); - const expected = { + expect(settings.get().groups).toStrictEqual(expected); + }); + + it('Should remove devices from groups', () => { + write(configurationFile, { + devices: { + '0x123': { + friendly_name: 'bulb', + retain: true, + } + }, + groups: { '1': { friendly_name: 'test123', - }, - }; - - expect(added).toStrictEqual(true); - expect(settings.get().groups).toStrictEqual(expected); + devices: ['0x123'], + } + } }); - it('Should not add duplicate groups', () => { - write(configurationFile, {}); + settings.removeDeviceFromGroup('test123', ['0x123']); + const expected = { + '1': { + friendly_name: 'test123', + devices: [], + }, + }; - const added = settings.addGroup('test123'); - const added2 = settings.addGroup('test123'); - const expected = { - '1': { - friendly_name: 'test123', - }, - }; + expect(settings.get().groups).toStrictEqual(expected); + }); - expect(added).toStrictEqual(true); - expect(added2).toStrictEqual(false); - expect(settings.get().groups).toStrictEqual(expected); + it('Should throw when adding device to non-existing group', () => { + write(configurationFile, { + devices: { + '0x123': { + friendly_name: 'bulb', + retain: true, + } + }, }); + + expect(() => { + settings.removeDeviceFromGroup('test123', 'bulb') + }).toThrow(new Error("Group 'test123' does not exist")); + }); + + it('Should throw when adding device which already exists', () => { + write(configurationFile, { + devices: { + '0x123': { + friendly_name: 'bulb', + retain: true, + } + }, + }); + + expect(() => { + settings.addDevice('0x123') + }).toThrow(new Error("Device '0x123' already exists")); + }); + + it('Should ban devices', () => { + write(configurationFile, {}); + settings.banDevice('0x123'); + expect(settings.get().ban).toStrictEqual(['0x123']); + settings.banDevice('0x1234'); + expect(settings.get().ban).toStrictEqual(['0x123', '0x1234']); }); }); diff --git a/test/stub/data.js b/test/stub/data.js new file mode 100644 index 000000000..6df99aa55 --- /dev/null +++ b/test/stub/data.js @@ -0,0 +1,171 @@ +const tmp = require('tmp'); +const yaml = require('../../lib/util/yaml'); +const path = require('path'); +const fs = require('fs'); + +const mockDir = tmp.dirSync().name; +const mockDirStorage = tmp.dirSync().name; + +function writeDefaultConfiguration() { + const config = { + homeassistant: false, + permit_join: true, + mqtt: { + base_topic: "zigbee2mqtt", + server: "mqtt://localhost", + }, + serial: { + "port": "/dev/dummy", + }, + devices: { + "0x000b57fffec6a5b2": { + retain: true, + friendly_name: "bulb" + }, + "0x0017880104e45517": { + retain: true, + friendly_name: "remote" + }, + "0x0017880104e45520": { + retain: false, + friendly_name: "button" + }, + "0x0017880104e45521": { + retain: false, + friendly_name: "button_double_key" + }, + "0x0017880104e45522": { + qos: 1, + friendly_name: "weather_sensor" + }, + "0x0017880104e45523": { + retain: false, + friendly_name: "occupancy_sensor" + }, + "0x0017880104e45524": { + retain: false, + friendly_name: "power_plug" + }, + "0x0017880104e45530": { + retain: false, + friendly_name: "button_double_key_interviewing" + }, + "0x0017880104e45540": { + retain: false, + friendly_name: "ikea_onoff" + }, + "0x000b57fffec6a5b3": { + retain: false, + friendly_name: "bulb_color" + }, + "0x0017880104e45541": { + retain: false, + friendly_name: "wall_switch" + }, + "0x0017880104e45542": { + retain: false, + friendly_name: "wall_switch_double" + }, + "0x0017880104e45543": { + retain: false, + friendly_name: "led_controller_1" + }, + "0x0017880104e45544": { + retain: false, + friendly_name: "led_controller_2" + }, + '0x0017880104e45545': { + retain: false, + friendly_name: "dimmer_wall_switch" + }, + '0x0017880104e45547': { + retain: false, + friendly_name: "curtain" + }, + '0x0017880104e45548': { + retain: false, + friendly_name: 'fan' + }, + '0x0017880104e45549': { + retain: false, + friendly_name: 'siren' + }, + '0x0017880104e45529': { + retain: false, + friendly_name: 'unsupported2' + }, + '0x0017880104e45550': { + retain: false, + friendly_name: 'thermostat' + }, + '0x0017880104e45551': { + retain: false, + friendly_name: 'smart_vent' + }, + '0x0017880104e45552': { + retain: false, + friendly_name: 'j1' + }, + '0x0017880104e45553': { + retain: false, + friendly_name: 'bulb_enddevice' + }, + '0x0017880104e45559': { + retain: false, + friendly_name: 'cc2530_router' + } + }, + groups: { + '1': { + friendly_name: 'group_1', + retain: false, + }, + '2': { + friendly_name: 'group_2', + retain: false, + } + } + }; + + yaml.write(path.join(mockDir, 'configuration.yaml'), config); +} + +function writeEmptyState() { + yaml.write(path.join(mockDir, 'state.json'), JSON.stringify({})); +} + +function writeDefaultState() { + const state = { + "0x000b57fffec6a5b2": { + "state": "ON", + "brightness": 50, + "color_temp": 370, + "linkquality": 99, + }, + "0x0017880104e45517": { + "brightness": 255 + }, + } + + fs.writeFileSync(path.join(mockDir, 'state.json'), JSON.stringify(state)); +} + +function removeState() { + fs.unlinkSync(path.join(mockDir, 'state.json')) +} + +jest.mock('../../lib/util/data', () => ({ + joinPath: (file) => require('path').join(mockDir, file), + getPath: () => mockDir, +})); + +writeDefaultConfiguration(); +writeDefaultState(); + +module.exports = { + mockDir, + writeDefaultConfiguration, + writeDefaultState, + removeState, + writeEmptyState, +}; diff --git a/test/stub/logger.js b/test/stub/logger.js new file mode 100644 index 000000000..66acc2941 --- /dev/null +++ b/test/stub/logger.js @@ -0,0 +1,13 @@ +const mock = { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + cleanup: jest.fn(), + transports: {console: {level: 1}, file: {level: 1}} +}; + +jest.mock('../../lib/util/logger', () => (mock)); + +module.exports = {...mock}; diff --git a/test/stub/mqtt.js b/test/stub/mqtt.js new file mode 100644 index 000000000..7588c4646 --- /dev/null +++ b/test/stub/mqtt.js @@ -0,0 +1,24 @@ +const events = {}; + +const mock = { + publish: jest.fn().mockImplementation((topic, payload, options, cb) => cb()), + end: jest.fn(), + subscribe: jest.fn(), + on: (type, handler) => { + if (type === 'connect') { + handler(); + } + + events[type] = handler + }, +}; + +const mockConnect = jest.fn().mockReturnValue(mock); + +jest.mock('mqtt', () => { + return {connect: mockConnect}; +}); + +module.exports = { + events, ...mock, connect: mockConnect, +}; \ No newline at end of file diff --git a/test/stub/zigbeeHerdsman.js b/test/stub/zigbeeHerdsman.js new file mode 100644 index 000000000..9ab2a028e --- /dev/null +++ b/test/stub/zigbeeHerdsman.js @@ -0,0 +1,176 @@ +const events = {}; +const assert = require('assert'); + +class Group { + constructor(groupID) { + this.groupID = groupID; + this.command = jest.fn(); + this.meta = {}; + this.members = []; + this.hasMember = (endpoint) => this.members.includes(endpoint); + this.getMembers = () => this.members; + } +} + +const clusters = { + 'genScenes': 5, + 'genOnOff': 6, + 'genLevelCtrl': 8, + 'lightingColorCtrl': 768, + 'closuresWindowCovering': 258, +} + +class Endpoint { + constructor(ID, inputClusters, outputClusters, deviceIeeeAddress) { + this.deviceIeeeAddress = deviceIeeeAddress; + this.ID = ID; + this.inputClusters = inputClusters; + this.outputClusters = outputClusters; + this.command = jest.fn(); + this.read = jest.fn(); + this.write = jest.fn(); + this.bind = jest.fn(); + this.unbind = jest.fn(); + this.configureReporting = jest.fn(); + this.supportsInputCluster = (cluster) => { + assert(clusters[cluster], `Undefined '${cluster}'`); + return this.inputClusters.includes(clusters[cluster]); + } + + this.supportsOutputCluster = (cluster) => { + assert(clusters[cluster], `Undefined '${cluster}'`); + return this.outputClusters.includes(clusters[cluster]); + } + + this.addToGroup = (group) => { + if (!group.members.includes(this)) group.members.push(this); + } + + this.removeFromGroup = (group) => { + const index = group.members.indexOf(this); + if (index != -1) { + group.members.splice(index, 1); + } + } + + this.removeFromAllGroups = () => { + Object.values(groups).forEach((g) => this.removeFromGroup(g)) + } + } +} + +class Device { + constructor(type, ieeeAddr, networkAddress, manufacturerID, endpoints, interviewCompleted, powerSource = null, modelID = null, interviewing=false, manufacturerName) { + this.type = type; + this.ieeeAddr = ieeeAddr; + this.networkAddress = networkAddress; + this.manufacturerID = manufacturerID; + this.endpoints = endpoints; + this.powerSource = powerSource; + this.interviewCompleted = interviewCompleted; + this.modelID = modelID; + this.interviewing = interviewing; + this.meta = {}; + this.ping = jest.fn(); + this.removeFromNetwork = jest.fn(); + this.save = jest.fn(); + this.manufacturerName = manufacturerName; + } + + getEndpoint(ID) { + return this.endpoints.find((e) => e.ID === ID); + } + + getEndpoints() { + return this.endpoints; + } + + isType(type) { + return type === 'device'; + } +} + +const returnDevices = []; + +const devices = { + 'coordinator': new Device('Coordinator', '0x00124b00120144ae', 0, 0, [new Endpoint(1, [], [])], false), + 'bulb': new Device('Router', '0x000b57fffec6a5b2', 40369, 4476, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096])], true, "Mains (single phase)", "TRADFRI bulb E27 WS opal 980lm"), + 'bulb_color': new Device('Router', '0x000b57fffec6a5b3', 40399, 6535, [new Endpoint(1, [0,3,4,5,6,8,768,2821,4096], [5,25,32,4096], '0x000b57fffec6a5b3')], true, "Mains (single phase)", "LLC020"), + 'remote': new Device('EndDevice', '0x0017880104e45517', 6535, 4107, [new Endpoint(1, [0], [0,3,4,6,8,5]), new Endpoint(2, [0,1,3,15,64512], [25, 6])], true, "Battery", "RWL021"), + 'unsupported': new Device('EndDevice', '0x0017880104e45518', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID"), + 'unsupported2': new Device('EndDevice', '0x0017880104e45529', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "notSupportedModelID"), + 'interviewing': new Device('EndDevice', '0x0017880104e45530', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", undefined, true), + 'notInSettings': new Device('EndDevice', '0x0017880104e45519', 6537, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "lumi.sensor_switch.aq2"), + 'WXKG11LM': new Device('EndDevice', '0x0017880104e45520', 6537,4151, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Battery", "lumi.sensor_switch.aq2"), + 'WXKG02LM': new Device('EndDevice', '0x0017880104e45521', 6538,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Battery", "lumi.sensor_86sw2.es1"), + 'WSDCGQ11LM': new Device('EndDevice', '0x0017880104e45522', 6539,4151, [new Endpoint(1, [0], [])], true, "Battery", "lumi.weather"), + 'RTCGQ11LM': new Device('EndDevice', '0x0017880104e45523', 6540,4151, [new Endpoint(1, [0], [])], true, "Battery", "lumi.sensor_motion.aq2"), + 'ZNCZ02LM': new Device('Router', '0x0017880104e45524', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", "lumi.plug"), + 'E1743': new Device('Router', '0x0017880104e45540', 6540,4476, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'TRADFRI on/off switch'), + 'QBKG04LM': new Device('Router', '0x0017880104e45541', 6549,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0, 6], [])], true, "Mains (single phase)", 'lumi.ctrl_neutral1'), + 'QBKG03LM':new Device('Router', '0x0017880104e45542', 6540,4151, [new Endpoint(1, [0], [], '0x0017880104e45542'), new Endpoint(2, [0, 6], [], '0x0017880104e45542'), new Endpoint(3, [0, 6], [], '0x0017880104e45542')], true, "Mains (single phase)", 'lumi.ctrl_neutral2'), + 'GLEDOPTO1112': new Device('Router', '0x0017880104e45543', 6540,4151, [new Endpoint(11, [0], []), new Endpoint(13, [0], [])], true, "Mains (single phase)", 'GLEDOPTO'), + 'GLEDOPTO111213': new Device('Router', '0x0017880104e45544', 6540,4151, [new Endpoint(11, [0], []), new Endpoint(13, [0], []), new Endpoint(12, [0], [])], true, "Mains (single phase)", 'GLEDOPTO'), + 'HGZB04D': new Device('Router', '0x0017880104e45545', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'FB56+ZSC05HG1.0'), + 'ZNCLDJ11LM': new Device('Router', '0x0017880104e45547', 6540,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Mains (single phase)", 'lumi.curtain'), + 'HAMPTON99432': new Device('Router', '0x0017880104e45548', 6540,4151, [new Endpoint(1, [0], []), new Endpoint(2, [0], [])], true, "Mains (single phase)", 'HDC52EastwindFan'), + 'HS2WD': new Device('Router', '0x0017880104e45549', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'WarningDevice'), + '1TST_EU': new Device('Router', '0x0017880104e45550', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'Thermostat'), + 'SV01': new Device('Router', '0x0017880104e45551', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'SV01-410-MP-1.0'), + 'J1': new Device('Router', '0x0017880104e45552', 6540,4151, [new Endpoint(1, [0], [])], true, "Mains (single phase)", 'J1 (5502)'), + 'E11_G13': new Device('EndDevice', '0x0017880104e45553', 6540,4151, [new Endpoint(1, [0, 6], [])], true, "Mains (single phase)", 'E11-G13'), + 'nomodel': new Device('Router', '0x0017880104e45535', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Mains (single phase)", undefined, true), + 'unsupported_router': new Device('Router', '0x0017880104e45525', 6536, 0, [new Endpoint(1, [0], [0,3,4,6,8,5])], true, "Mains (single phase)", "notSupportedModelID", false, "Boef"), + 'CC2530_ROUTER': new Device('Router', '0x0017880104e45559', 6540,4151, [new Endpoint(1, [0, 6], [])], true, "Mains (single phase)", 'lumi.router'), +} + +const groups = { + 'group_1': new Group(1), +} + +const mock = { + start: jest.fn(), + permitJoin: jest.fn(), + getCoordinatorVersion: jest.fn().mockReturnValue({type: 'z-Stack', meta: {version: 1}}), + getNetworkParameters: jest.fn().mockReturnValue({panID: 0x162a, extendedPanID: [0, 11, 22], channel: 15}), + on: (type, handler) => { + events[type] = handler; + }, + stop: jest.fn(), + disableLED: jest.fn(), + getDevices: jest.fn().mockImplementation((query) => { + return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)) + }), + getDevice: jest.fn().mockImplementation((query) => { + return Object.values(devices).filter((d) => returnDevices.length === 0 || returnDevices.includes(d.ieeeAddr)) + .find((d) => { + return (!query.hasOwnProperty('ieeeAddr') || query.ieeeAddr === d.ieeeAddr) && + (!query.hasOwnProperty('type') || query.type === d.type); + }) + }), + getGroups: jest.fn().mockImplementation((query) => { + return Object.values(groups); + }), + getGroup: jest.fn().mockImplementation((query) => { + return Object.values(groups).find((g) => { + return (!query.hasOwnProperty('groupID') || g.groupID === query.groupID); + }) + }), + getPermitJoin: jest.fn().mockReturnValue(false), + softReset: jest.fn(), + createGroup: jest.fn().mockImplementation((groupID) => { + const group = new Group(groupID); + groups[`group_${groupID}`] = group + return group; + }) +}; + +const mockConstructor = jest.fn().mockImplementation(() => mock); + +jest.mock('zigbee-herdsman', () => ({ + Controller: mockConstructor, +})); + +module.exports = { + events, ...mock, constructor: mockConstructor, devices, groups, returnDevices +}; diff --git a/test/utils.js b/test/utils.js deleted file mode 100644 index c32bfe451..000000000 --- a/test/utils.js +++ /dev/null @@ -1,14 +0,0 @@ -const logger = require('../lib/util/logger'); - -module.exports = { - stubLogger: (jest) => { - jest.spyOn(logger, 'info').mockReturnValue(undefined); - jest.spyOn(logger, 'warn').mockReturnValue(undefined); - jest.spyOn(logger, 'debug').mockReturnValue(undefined); - jest.spyOn(logger, 'error').mockReturnValue(undefined); - jest.spyOn(logger, 'trace').mockReturnValue(undefined); - }, - zigbeeMessage: (device, cid, type, data, epId, groupid=0) => { - return {data: {cid, data}, type, groupid, endpoints: [{device, epId}]}; - }, -}; diff --git a/test/utils.test.js b/test/utils.test.js index 0c2c0240f..488889b37 100644 --- a/test/utils.test.js +++ b/test/utils.test.js @@ -1,78 +1,53 @@ const utils = require('../lib/util/utils.js'); -const testUtils = require('./utils'); +const version = require('../package.json').version; describe('Utils', () => { - beforeAll(() => { - testUtils.stubLogger(jest); - }); - describe('Is xiaomi device', () => { it('Identify xiaomi device', () => { - const device = {type: 'Router', manufId: 4151, manufName: 'Xiaomi'}; + const device = {type: 'Router', manufacturerID: 4151, manufacturerName: 'Xiaomi'}; expect(true).toBe(utils.isXiaomiDevice(device)); }); - it('Identify xiaomi device without manufName', () => { - const device = {type: 'Router', manufId: 4447}; + it('Identify xiaomi device without manufacturerName', () => { + const device = {type: 'Router', manufacturerID: 4447}; expect(true).toBe(utils.isXiaomiDevice(device)); }); - it('Identify xiaomi device with different manufName', () => { - const device = {type: 'Router', manufId: 4151, manufName: 'Trust International B.V.\u0000'}; + it('Identify xiaomi device with different manufacturerName', () => { + const device = {type: 'Router', manufacturerID: 4151, manufacturerName: 'Trust International B.V.\u0000'}; expect(false).toBe(utils.isXiaomiDevice(device)); }); - - it('Identify QBKG03LM as enddevice', () => { - const device = {type: 'Router', manufId: 4447, modelId: 'lumi.ctrl_neutral1'}; - expect(false).toBe(utils.isRouter(device)); - expect('EndDevice').toBe(utils.correctDeviceType(device)); - }); - - it('Identify QBKG04LM as enddevice', () => { - const device = {type: 'Router', manufId: 4447, modelId: 'lumi.ctrl_neutral2'}; - expect(false).toBe(utils.isRouter(device)); - expect('EndDevice').toBe(utils.correctDeviceType(device)); - }); }); - describe('Get endpoint by id', () => { - it('Pick default ep', () => { - const zigbee = { - getDevice: (entityID) => { - return {modelId: 'TRADFRI on/off switch'}; - }, - getEndpoint: (entityID, epId) => { - return {epId: epId == null ? 1 : 0}; - }, - }; - const endpoint = utils.getEndpointByEntityID(zigbee, '0x12345678', null); - expect(endpoint.epId).toBe(1); - }); + it('Convert milliseconds to seconds', () => { + expect(utils.millisecondsToSeconds(2000)).toBe(2); + }) - it('Pick default ep from mapping when default defined', () => { - const zigbee = { - getDevice: (entityID) => { - return {modelId: 'SML002'}; - }, - getEndpoint: (entityID, epId) => { - return {epId}; - }, - }; - const endpoint = utils.getEndpointByEntityID(zigbee, '0x12345678', null); - expect(endpoint.epId).toBe(2); - }); + it('Object has properties', () => { + expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b'])).toBeTruthy(); + expect(utils.objectHasProperties({a: 1, b: 2, c: 3}, ['a', 'b', 'd'])).toBeFalsy(); + }) - it('Pick default ep from mapping when not defined', () => { - const zigbee = { - getDevice: (entityID) => { - return {modelId: 'lumi.sensor_86sw2.es1'}; - }, - getEndpoint: (entityID, epId) => { - return {epId: epId == null ? 1 : 0}; - }, - }; - const endpoint = utils.getEndpointByEntityID(zigbee, '0x12345678', null); - expect(endpoint.epId).toBe(1); - }); - }); + it('git last commit', async () => { + let mockReturnValue = []; + jest.mock('git-last-commit', () => ({ + getLastCommit: (cb) => cb(mockReturnValue[0], mockReturnValue[1]) + })); + + mockReturnValue = [false, {shortHash: '123'}] + expect(await utils.getZigbee2mqttVersion()).toStrictEqual({"commitHash": "123", "version": version}); + + mockReturnValue = [true, null] + expect(await utils.getZigbee2mqttVersion()).toStrictEqual({"commitHash": "unknown", "version": version}); + }) + + it('To local iso string', async () => { + var date = new Date('August 19, 1975 23:15:30 UTC+00:00'); + var getTimezoneOffset = Date.prototype.getTimezoneOffset; + Date.prototype.getTimezoneOffset = () => 60; + expect(utils.formatDate(date, 'ISO_8601_local').endsWith('-01:00')).toBeTruthy(); + Date.prototype.getTimezoneOffset = () => -60; + expect(utils.formatDate(date, 'ISO_8601_local').endsWith('+01:00')).toBeTruthy(); + Date.prototype.getTimezoneOffset = getTimezoneOffset; + }) });