diff --git a/.gitignore b/.gitignore index 0143bd9..1f3b0fb 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,5 @@ typings/ # vs code garbage .vscode + +lib/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c6a1376 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "trailingComma": "es5", + "singleQuote": true +} diff --git a/index.js b/index.js index 3b3e271..bf57381 100644 --- a/index.js +++ b/index.js @@ -1,185 +1,2 @@ -const SwitchAccessory = require('./lib/switch_accessory'); -const OutletAccessory = require('./lib/outlet_accessory'); -const DimmerAccessory = require('./lib/dimmer_accessory'); -const LightAccessory = require('./lib/light_accessory'); -const TuyaWebApi = require('./lib/tuyawebapi'); - -var Accessory, Service, Characteristic, UUIDGen; - -module.exports = function (homebridge) { - // Accessory must be created from PlatformAccessory Constructor - Accessory = homebridge.platformAccessory; - - // Service and Characteristic are from hap-nodejs - Service = homebridge.hap.Service; - Characteristic = homebridge.hap.Characteristic; - UUIDGen = homebridge.hap.uuid; - - // For platform plugin to be considered as dynamic platform plugin, - // registerPlatform(pluginName, platformName, constructor, dynamic), dynamic must be true - homebridge.registerPlatform("homebridge-tuya-web", "TuyaWebPlatform", TuyaWebPlatform, true); -} - -class TuyaWebPlatform { - constructor(log, config, api) { - this.log = log; - this.config = config; - this.pollingInterval = 10; // default 10 seconds - this.refreshInterval; - - if (!config || !config.options) { - this.log('No config found, disabling plugin.') - return; - } - - // Set cloud polling interval - this.pollingInterval = this.config.options.pollingInterval || 10; - - // Create Tuya Web API instance - this.tuyaWebApi = new TuyaWebApi( - this.config.options.username, - this.config.options.password, - this.config.options.countryCode, - this.config.options.platform, - this.log - ); - - this.accessories = new Map(); - - if (api) { - // Save the API object as plugin needs to register new accessory via this object - this.api = api; - - // Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories. - // Platform Plugin should only register new accessory that doesn't exist in homebridge after this event. - // Or start discover new accessories. - this.api.on('didFinishLaunching', function () { - this.log("Initializing TuyaWebPlatform..."); - - // Get access token - this.tuyaWebApi.getOrRefreshToken().then((token) => { - this.tuyaWebApi.token = token; - - // Start discovery for devices - this.tuyaWebApi.discoverDevices().then((devices) => { - // Add devices to Homebridge - for (const device of devices) { - this.addAccessory(device); - } - // Get device strate of all devices - once - this.refreshDeviceStates(); - }).catch((error) => { - this.log.error(error); - }); - - // Set interval for refreshing device states - this.refreshInterval = setInterval(() => { - this.refreshDeviceStates(); - }, this.pollingInterval * 1000); - }).catch((error) => { - this.log.error(error); - }); - - }.bind(this)); - } - } - - refreshDeviceStates() { - this.log.debug('Refreshing state for all devices...'); - this.tuyaWebApi.getAllDeviceStates().then((devices) => { - // Refresh device states - for (const device of devices) { - const uuid = this.api.hap.uuid.generate(device.id); - const homebridgeAccessory = this.accessories.get(uuid); - if (homebridgeAccessory) { - homebridgeAccessory.controller.updateAccessory(device); - } - else { - this.log.error('Could not find accessory in dictionary'); - } - } - }).catch((error) => { - this.log.error('Error retrieving devices states', error); - }); - } - - addAccessory(device) { - var deviceType = device.dev_type || 'switch'; - this.log.info('Adding: %s (%s / %s)', device.name || 'unnamed', deviceType, device.id); - - // Get UUID - const uuid = this.api.hap.uuid.generate(device.id); - const homebridgeAccessory = this.accessories.get(uuid); - - // Is device type overruled in config defaults? - if (this.config.defaults) { - for (const def of this.config.defaults) { - if (def.id === device.id) { - deviceType = def.device_type || deviceType; - this.log('Device type is overruled in config to: ', deviceType); - } - } - } - - // Construct new accessory - let deviceAccessory; - switch (deviceType) { - case 'light': - deviceAccessory = new LightAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - break; - case 'dimmer': - deviceAccessory = new DimmerAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - break; - case 'switch': - case 'outlet': - deviceAccessory = new OutletAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - break; - default: - this.log.warn('Could not init class for device type [%s]', deviceType); - break; - } - } - - // Called from device classes - registerPlatformAccessory(platformAccessory) { - this.log.debug('Register Platform Accessory (%s)', platformAccessory.displayName); - this.api.registerPlatformAccessories('homebridge-tuya-web', 'TuyaWebPlatform', [platformAccessory]); - } - - // Function invoked when homebridge tries to restore cached accessory. - // Developer can configure accessory at here (like setup event handler). - // Update current value. - configureAccessory(accessory) { - this.log("Configuring cached accessory [%s]", accessory.displayName, accessory.context.deviceId, accessory.UUID); - - // Set the accessory to reachable if plugin can currently process the accessory, - // otherwise set to false and update the reachability later by invoking - // accessory.updateReachability() - accessory.reachable = true; - - accessory.on('identify', function (paired, callback) { - this.log.debug('[IDENTIFY][%s]', accessory.displayName); - callback(); - }); - - this.accessories.set(accessory.UUID, accessory); - } - - updateAccessoryReachability(accessory, state) { - this.log("Update Reachability [%s]", accessory.displayName, state); - accessory.updateReachability(state); - } - - // Sample function to show how developer can remove accessory dynamically from outside event - removeAccessory(accessory) { - this.log("Remove Accessory [%s]", accessory.displayName); - this.api.unregisterPlatformAccessories("homebridge-tuya-web", "TuyaWebPlatform", [accessory]); - - this.accessories.delete(accessory.uuid); - } -} - - +const TuyaWebAPI = require('./lib/'); +module.exports = TuyaWebAPI; diff --git a/lib/dimmer_accessory.js b/lib/dimmer_accessory.js deleted file mode 100644 index 7bdb054..0000000 --- a/lib/dimmer_accessory.js +++ /dev/null @@ -1,123 +0,0 @@ -const TuyaWebApi = require('./tuyawebapi'); -const BaseAccessory = require('./base_accessory') - -let PlatformAccessory; -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class DimmerAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.LIGHTBULB - ) - - // Characteristic.On - this.service.getCharacteristic(Characteristic.On) - .on('get', (callback) => { - - // Retrieve state from cache - if (this.hasValidCache()) { - callback(null, this.getCachedState(Characteristic.On)); - } - else { - - // Retrieve device state from Tuya Web API - this.platform.tuyaWebApi.getDeviceState(this.deviceId).then((data) => { - this.log.debug('[GET][%s] Characteristic.On: %s', this.homebridgeAccessory.displayName, data.state); - this.getCachedState(Characteristic.On, data.state); - callback(null, data.state); - }).catch((error) => { - this.log.error('[GET][%s] Characteristic.On Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - - } - }) - .on('set', (state, callback) => { - - // Set device state in Tuya Web API - const value = state ? 1 : 0; - this.platform.tuyaWebApi.setDeviceState(this.deviceId, 'turnOnOff', { value: value }).then(() => { - this.log.debug('[SET][%s] Characteristic.On: %s %s', this.homebridgeAccessory.displayName, state, value); - this.setCachedState(Characteristic.On, state); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.On Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - - }); - - // Characteristic.Brightness - this.service.getCharacteristic(Characteristic.Brightness) - .on('get', callback => { - - // Retrieve state from cache - if (this.hasValidCache()) { - callback(null, this.getCachedState(Characteristic.Brightness)); - } - else { - - // Retrieve device state from Tuya Web API - this.platform.tuyaWebApi.getDeviceState(this.deviceId).then((data) => { - const percentage = Math.floor((parseInt(data.brightness) / 255) * 100); - this.log.debug('[GET][%s] Characteristic.Brightness: %s (%s percent)', this.homebridgeAccessory.displayName, data.brightness, percentage); - this.getCachedState(Characteristic.Brightness, percentage); - callback(null, percentage); - }).catch((error) => { - this.log.error('[GET][%s] Characteristic.Brightness Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - - } - }) - .on('set', (percentage, callback) => { - - // NOTE: For some strange reason, the set value for brightness is in percentage. - - // Set device state in Tuya Web API - this.platform.tuyaWebApi.setDeviceState(this.deviceId, 'brightnessSet', { value: percentage }).then(() => { - this.log.debug('[SET][%s] Characteristic.Brightness: %s percent', this.homebridgeAccessory.displayName, percentage); - this.setCachedState(Characteristic.Brightness, percentage); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.Brightness Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - - }); - } - - updateState(data) { - // Update device type specific state - this.log.debug('[UPDATING][%s]:', this.homebridgeAccessory.displayName, data); - if (data.state) { - const state = (data.state === 'true'); - this.service - .getCharacteristic(Characteristic.On) - .updateValue(state); - this.setCachedState(Characteristic.On, state); - } - if (data.percentage) { - const percentage = Math.floor((parseInt(data.brightness) / 255) * 100); - this.service - .getCharacteristic(Characteristic.Brightness) - .updateValue(percentage); - this.setCachedState(Characteristic.Brightness, percentage); - } - } -} - -module.exports = DimmerAccessory; \ No newline at end of file diff --git a/lib/light_accessory.js b/lib/light_accessory.js deleted file mode 100644 index 8481f3f..0000000 --- a/lib/light_accessory.js +++ /dev/null @@ -1,269 +0,0 @@ -const TuyaWebApi = require('./tuyawebapi'); -const BaseAccessory = require('./base_accessory') - -let PlatformAccessory; -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class LightAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.LIGHTBULB - ); - - // homekit compatible defaults - const defaultBrightness = 100; // 100% - const defaultSaturation = 100; // 100% - const defaultHue = 359; // red (max hue) - - // Characteristic.On - this.service.getCharacteristic(Characteristic.On) - .on('get', (callback) => { - // Retrieve state from cache - if (this.hasValidCache()) { - callback(null, this.getCachedState(Characteristic.On)); - } else { - // Retrieve device state from Tuya Web API - this.platform.tuyaWebApi.getDeviceState(this.deviceId).then((data) => { - this.log.debug('[GET][%s] Characteristic.On: %s', this.homebridgeAccessory.displayName, data.state); - this.setCachedState(Characteristic.On, data.state); - callback(null, data.state); - }).catch((error) => { - this.log.error('[GET][%s] Characteristic.On Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - } - }) - .on('set', (isOn, callback) => { - // Set device state in Tuya Web API - const value = isOn ? 1 : 0; - - this.platform.tuyaWebApi.setDeviceState(this.deviceId, 'turnOnOff', { value: value }).then(() => { - this.log.debug('[SET][%s] Characteristic.On: %s %s', this.homebridgeAccessory.displayName, isOn, value); - this.setCachedState(Characteristic.On, isOn); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.On Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - - // Characteristic.Brightness - this.service.getCharacteristic(Characteristic.Brightness) - .on('get', callback => { - // Retrieve state from cache - if (this.hasValidCache()) { - callback(null, this.getCachedState(Characteristic.Brightness)); - } else { - // Retrieve device state from Tuya Web API - this.platform.tuyaWebApi.getDeviceState(this.deviceId).then((data) => { - // data.brightness only valid for color_mode!=color > https://github.com/PaulAnnekov/tuyaha/blob/master/tuyaha/devices/light.py - // however, according to local tuya app, calculation for color_mode=color is stil incorrect (even more so in lower range) - - let rawValue; - let percentage; - - if (data.color_mode == 'color') { - rawValue = data.color.brightness; // 0-100 - percentage = rawValue; - } else { - rawValue = data.brightness; // 0-255 - percentage = Math.floor(rawValue / 255) * 100; // 0-100 - } - - this.log.debug('[GET][%s] Characteristic.Brightness: %s (%s percent)', this.homebridgeAccessory.displayName, rawValue, percentage); - this.setCachedState(Characteristic.Brightness, percentage); - callback(null, percentage); - }).catch((error) => { - this.log.error('[GET][%s] Characteristic.Brightness Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - } - }) - .on('set', (percentage, callback) => { - // NOTE: For some strange reason, the set value for brightness is in percentage - const value = percentage; // 0-100 - - // Set device state in Tuya Web API - this.platform.tuyaWebApi.setDeviceState(this.deviceId, 'brightnessSet', { value: value }).then(() => { - this.log.debug('[SET][%s] Characteristic.Brightness: %s percent', this.homebridgeAccessory.displayName, percentage); - this.setCachedState(Characteristic.Brightness, percentage); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.Brightness Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - - // Characteristic.Saturation - this.service.getCharacteristic(Characteristic.Saturation) - .on('get', callback => { - // Retrieve state from cache - if (this.hasValidCache()) { - callback(null, this.getCachedState(Characteristic.Saturation)); - } else { - // Retrieve device state from Tuya Web API - this.platform.tuyaWebApi.getDeviceState(this.deviceId).then((data) => { - if (data.color) { - const saturation = data.color.saturation; // 0-100 - const hue = data.color.hue; // 0-359 - this.log.debug('[GET][%s] Characteristic.Saturation: %s', this.homebridgeAccessory.displayName, saturation); - this.setCachedState(Characteristic.Saturation, saturation); - this.setCachedState(Characteristic.Hue, hue); - callback(null, saturation); - } - else { - callback(null, null); - } - }).catch((error) => { - this.log.error('[GET][%s] Characteristic.Saturation Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - } - }) - .on('set', (percentage, callback) => { - let color = {}; - - const cachedBrightness = this.getCachedState(Characteristic.Brightness); - const cachedHue = this.getCachedState(Characteristic.Hue); - - color.brightness = cachedBrightness ? cachedBrightness : defaultBrightness; // 0-100 - color.saturation = Math.floor(percentage / 100) * 255; // 0-255 - color.hue = cachedHue ? cachedHue : defaultHue; // 0-359 - - // Set device state in Tuya Web API - this.platform.tuyaWebApi.setDeviceState(this.deviceId, 'colorSet', { color: color }).then(() => { - this.log.debug('[SET][%s] Characteristic.Saturation: (%s) %s percent', this.homebridgeAccessory.displayName, color.saturation, percentage); - this.setCachedState(Characteristic.Brightness, color.brightness); - this.setCachedState(Characteristic.Saturation, percentage); - this.setCachedState(Characteristic.Hue, color.hue); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.Saturation Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - - // Characteristic.Hue - this.service.getCharacteristic(Characteristic.Hue) - .on('get', callback => { - // Retrieve state from cache - if (this.hasValidCache()) { - callback(null, this.getCachedState(Characteristic.Hue)); - } else { - // Retrieve device state from Tuya Web API - this.platform.tuyaWebApi.getDeviceState(this.deviceId).then((data) => { - if (data.color) { - const saturation = data.color.saturation; // 0-100 - const hue = data.color.hue; // 0-359 - this.log.debug('[GET][%s] Characteristic.Hue: %s', this.homebridgeAccessory.displayName, hue); - this.setCachedState(Characteristic.Saturation, saturation); - this.setCachedState(Characteristic.Hue, hue); - callback(null, hue); - } - else { - callback(null, null); - } - }).catch((error) => { - this.log.error('[GET][%s] Characteristic.Hue Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - } - }) - .on('set', (hue, callback) => { - let color = {}; - - const cachedBrightness = this.getCachedState(Characteristic.Brightness); - const cachedSaturation = this.getCachedState(Characteristic.Saturation); - - color.brightness = cachedBrightness ? cachedBrightness : defaultBrightness; // 0-100 - color.saturation = cachedSaturation ? Math.floor(cachedSaturation / 100) * 255 : defaultSaturation; // 0-255 - const newSaturationPercentage = Math.floor(color.saturation / 255) * 100; - color.hue = hue; - - // Set device state in Tuya Web API - this.platform.tuyaWebApi.setDeviceState(this.deviceId, 'colorSet', { color: color }).then(() => { - this.log.debug('[SET][%s] Characteristic.Hue: %s', this.homebridgeAccessory.displayName, hue); - this.setCachedState(Characteristic.Brightness, color.brightness); - this.setCachedState(Characteristic.Saturation, newSaturationPercentage); - this.setCachedState(Characteristic.Hue, color.hue); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.Hue Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - } - - updateState(data) { - // Update device type specific state - this.log.debug('[UPDATING][%s]:', this.homebridgeAccessory.displayName, data); - - if (data.state) { - const isOn = (data.state === 'true'); - this.service - .getCharacteristic(Characteristic.On) - .updateValue(isOn); - this.setCachedState(Characteristic.On, isOn); - } - if (data.brightness || data.color.brightness) { - let rawValue; - let percentage; - - if (data.color_mode == 'color') { - rawValue = data.color.brightness; // 0-100 - percentage = rawValue; - } else { - rawValue = data.brightness; // 0-255 - percentage = Math.floor(rawValue / 255) * 100; // 0-100 - } - - this.service - .getCharacteristic(Characteristic.Brightness) - .updateValue(percentage); - this.setCachedState(Characteristic.Brightness, percentage); - } - if (data.color && data.color.saturation) { - let rawValue; - let percentage; - - if (data.color_mode == 'color') { - rawValue = data.color.brightness; // 0-100 - percentage = rawValue; - } else { - rawValue = data.brightness; // 0-255 - percentage = Math.floor(rawValue / 255) * 100; // 0-100 - } - - this.service - .getCharacteristic(Characteristic.Saturation) - .updateValue(percentage); - this.setCachedState(Characteristic.Saturation, percentage); - } - if (data.color && data.color.hue) { - const hue = data.color.hue; - this.service - .getCharacteristic(Characteristic.Hue) - .updateValue(hue); - this.setCachedState(Characteristic.Hue, hue); - } - } -} - -module.exports = LightAccessory; \ No newline at end of file diff --git a/lib/outlet_accessory.js b/lib/outlet_accessory.js deleted file mode 100644 index 04dabf1..0000000 --- a/lib/outlet_accessory.js +++ /dev/null @@ -1,71 +0,0 @@ -const TuyaWebApi = require('./tuyawebapi'); -const BaseAccessory = require('./base_accessory') - -let PlatformAccessory; -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class OutletAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.OUTLET - ) - - this.service.getCharacteristic(Characteristic.On) - .on('get', (callback) => { - - // Retrieve state from cache - if (this.hasValidCache()) { - callback(null, this.getCachedState(Characteristic.On)); - } - else { - - // Retrieve device state from Tuya Web API - this.platform.tuyaWebApi.getDeviceState(this.homebridgeAccessory.context.deviceId).then((data) => { - this.log.debug('[GET][%s] Characteristic.On: %s', this.homebridgeAccessory.displayName, data.state); - this.getCachedState(Characteristic.On, data.state); - callback(null, data.state); - }).catch((error) => { - this.log.error('[GET][%s] Characteristic.On Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - - } - }) - .on('set', (state, callback) => { - - // Set device state in Tuya Web API - const value = state ? 1 : 0; - this.platform.tuyaWebApi.setDeviceState(this.homebridgeAccessory.context.deviceId, 'turnOnOff', { value: value }).then(() => { - this.log.debug('[SET][%s] Characteristic.On: %s %s', this.homebridgeAccessory.displayName, state, value); - this.setCachedState(Characteristic.On, state); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.On Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - - }); - } - - updateState(data) { - this.log.debug('[UPDATING][%s]:', this.homebridgeAccessory.displayName, data); - const state = (data.state === true); - this.service - .getCharacteristic(Characteristic.On) - .updateValue(data.state); - this.setCachedState(Characteristic.On, state); - } -} - -module.exports = OutletAccessory; \ No newline at end of file diff --git a/lib/switch_accessory.js b/lib/switch_accessory.js deleted file mode 100644 index 14f4e6c..0000000 --- a/lib/switch_accessory.js +++ /dev/null @@ -1,71 +0,0 @@ -const TuyaWebApi = require('./tuyawebapi'); -const BaseAccessory = require('./base_accessory') - -let PlatformAccessory; -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class SwitchAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.SWITCH - ) - - this.service.getCharacteristic(Characteristic.On) - .on('get', (callback) => { - - // Retrieve state from cache - if (this.hasValidCache()) { - callback(null, this.getCachedState(Characteristic.On)); - } - else { - - // Retrieve device state from Tuya Web API - this.platform.tuyaWebApi.getDeviceState(this.deviceId).then((data) => { - this.log.debug('[GET][%s] Characteristic.On: %s', this.homebridgeAccessory.displayName, data.state); - this.getCachedState(Characteristic.On, data.state); - callback(null, data.state); - }).catch((error) => { - this.log.error('[GET][%s] Characteristic.On Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - } - - }) - .on('set', (state, callback) => { - - // Set device state in Tuya Web API - const value = state ? 1 : 0; - this.platform.tuyaWebApi.setDeviceState(this.deviceId, 'turnOnOff', { value: value }).then(() => { - this.log.debug('[SET][%s] Characteristic.On: %s %s', this.homebridgeAccessory.displayName, state, value); - this.setCachedState(Characteristic.On, state); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.On Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - - }); - } - - updateState(data) { - this.log.debug('[UPDATING][%s]:', this.homebridgeAccessory.displayName, data); - const state = (data.state === true); - this.service - .getCharacteristic(Characteristic.On) - .updateValue(data.state); - this.setCachedState(Characteristic.On, state); - } -} - -module.exports = SwitchAccessory; \ No newline at end of file diff --git a/lib/tuyawebapi.js b/lib/tuyawebapi.js deleted file mode 100644 index 77cc8e2..0000000 --- a/lib/tuyawebapi.js +++ /dev/null @@ -1,322 +0,0 @@ -const request = require('request'); -const querystring = require('querystring'); - -class Session { - constructor(accessToken, refreshToken, expiresIn, areaCode, areaBaseUrl) { - this.accessToken; - this.refreshToken; - this.expiresOn; - this.areaCode = areaCode; - this.areaBaseUrl = areaBaseUrl; - this.resetToken(accessToken, refreshToken, expiresIn); - } - - resetToken(accessToken, refreshToken, expiresIn) { - this.accessToken = accessToken; - this.refreshToken = refreshToken; - this.expiresOn = this._getCurrentEpoch() + expiresIn - 100; // subtract 100 ticks to expire token before it actually does - } - - hasToken() { - return this.accessToken && true; - } - - isTokenExpired() { - return this.token.expiresOn > this._getCurrentEpoch(); - } - - hasValidToken() { - return this.accessToken && this.expiresOn > this._getCurrentEpoch(); - } - - _getCurrentEpoch() { - return Math.round((new Date()).getTime() / 1000); - } -} - -class TuyaWebApi { - constructor(username, password, countryCode, tuyaPlatform = 'tuya', log = null) { - this.username = username; - this.password = password; - this.countryCode = countryCode; - this.tuyaPlatform = tuyaPlatform; - - this.session = new Session(); - - this.authBaseUrl = 'https://px1.tuyaeu.com'; - - this.log = log; - } - - discoverDevices() { - if (!this.session.hasValidToken()) { - throw new Error('No valid token'); - } - - var data = { - 'header': { - 'name': 'Discovery', - 'namespace': 'discovery', - 'payloadVersion': 1 - }, - 'payload': { - 'accessToken': this.session.accessToken - } - } - - return new Promise((resolve, reject) => { - this.sendRequestJson( - this.session.areaBaseUrl + '/homeassistant/skill', - JSON.stringify(data), - 'GET', - (response, obj) => { - if (obj.header && obj.header.code === 'SUCCESS') { - if (obj.payload && obj.payload.devices) { - resolve(obj.payload.devices); - } - } - reject(new Error('No valid response from API', obj)); - }, - (error) => { - reject(error); - } - ) - }); - } - - getAllDeviceStates() { - return this.discoverDevices(); - } - - getDeviceState(deviceId) { - if (!this.session.hasValidToken()) { - throw new Error('No valid token'); - } - - var data = { - 'header': { - 'name': 'QueryDevice', - 'namespace': 'query', - 'payloadVersion': 1 - }, - 'payload': { - 'accessToken': this.session.accessToken, - 'devId': deviceId, - 'value': 1 - } - } - - return new Promise((resolve, reject) => { - this.sendRequestJson( - this.session.areaBaseUrl + '/homeassistant/skill', - JSON.stringify(data), - 'GET', - (response, obj) => { - if (obj.payload && obj.header && obj.header.code === 'SUCCESS') { - resolve(obj.payload.data); - } - else { - reject(new Error('Invalid payload in response: ', obj)) - } - }, - (error) => { - reject(error); - } - ) - }); - } - - setDeviceState(deviceId, method, payload) { - if (!this.session.hasValidToken()) { - throw new Error('No valid token'); - } - - /* Methods - * turnOnOff -> 0 = off, 1 = on - * brightnessSet --> 0..100 - */ - - var data = { - 'header': { - 'name': method, - 'namespace': 'control', - 'payloadVersion': 1 - }, - 'payload': payload - } - - data.payload.accessToken = this.session.accessToken; - data.payload.devId = deviceId; - - return new Promise((resolve, reject) => { - this.sendRequestJson( - this.session.areaBaseUrl + '/homeassistant/skill', - JSON.stringify(data), - 'POST', - (response, obj) => { - if (obj.header && obj.header.code === 'SUCCESS') { - resolve(); - } - else { - reject(new Error('Invalid payload in response: ', obj)) - } - }, - (error) => { - reject(error); - } - ) - }); - } - - getOrRefreshToken() { - if (!this.session.hasToken()) { - // No token, lets get a token from the Tuya Web API - if (!this.username) { - throw new Error('No username configured'); - } - else { - if (!this.password) { - throw new Error('No password configured'); - } - else { - if (!this.countryCode) { - throw new Error('No country code configured'); - } - else { - - var form = { - userName: this.username, - password: this.password, - countryCode: this.countryCode, - bizType: this.tuyaPlatform, - from: "tuya" - } - - var formData = querystring.stringify(form); - var contentLength = formData.length; - - return new Promise((resolve, reject) => { - request({ - headers: { - 'Content-Length': contentLength, - 'Content-Type': 'application/x-www-form-urlencoded' - }, - uri: this.authBaseUrl + '/homeassistant/auth.do', - body: formData, - method: 'POST' - }, - (err, res, body) => { - if (err) { - reject(new Error('Authentication fault, could not retreive token.', err)); - } - else { - let obj; - try { - obj = JSON.parse(body); - } - catch (error) { - reject(new Error(`Could not parse json. Body: ${body}`, error)); - } - if (obj.responseStatus === 'error') { - reject(new Error('Authentication fault: ' + obj.errorMsg)); - } - else { - // Received token - this.session.resetToken( - obj.access_token, - obj.refresh_token, - obj.expires_in - ); - // Change url based on areacode in accesstoken first two chars - this.session.areaCode = obj.access_token.substr(0, 2); - switch (this.session.areaCode) { - case 'AY': - this.session.areaBaseUrl = 'https://px1.tuyacn.com'; - break; - case 'EU': - this.session.areaBaseUrl = 'https://px1.tuyaeu.com'; - break; - case 'US': - default: - this.session.areaBaseUrl = 'https://px1.tuyaus.com'; - } - resolve(this.session); - } - } - }); - }); - } - } - } - } - else { - if (this.session.hasToken() && this.session.isTokenExpired()) { - // Refresh token - return new Promise((resolve, reject) => { - this.sendRequestJson( - this.session.areaBaseUrl + '/homeassistant/access.do?grant_type=refresh_token&refresh_token=' + this.session.refreshToken, - '', - 'GET', - (response, obj) => { - // Received token - this.session.resetToken( - obj.access_token, - obj.refresh_token, - obj.expires_in - ); - resolve(this.session); - }, - (error) => { - reject(error); - } - ) - }); - } - } - } - - /* - * -------------------------------------- - * HTTP methods - */ - - sendRequest(url, body, method, callbackSuccess, callbackError) { - request({ - url: url, - body: body, - method: method, - rejectUnauthorized: false, - }, - (error, response, body) => { - if (error) { - callbackError(error); - } - else { - callbackSuccess(response, body) - } - }); - } - - sendRequestJson(url, body, method, callbackSuccess, callbackError) { - // this.log.debug(JSON.stringify(body)); - - this.sendRequest(url, body, method, - (response, body) => { - // this.log.debug(JSON.stringify(body)); - - try { - const obj = JSON.parse(body); - callbackSuccess(response, obj); - } - catch (error) { - callbackError(new Error(`Could not parse json. Body: ${body}`, error)); - } - }, - (error) => { - callbackError(error); - } - ); - } -} - -module.exports = TuyaWebApi; \ No newline at end of file diff --git a/package.json b/package.json index 8aeb80d..f3bda13 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,16 @@ { - "name": "homebridge-tuya-web", + "name": "@vitramir/homebridge-tuya-web", "version": "0.1.7", "description": "Homebrigde plugin for use with Tuya devices (uses Home Assistant Tuya Web API)", "main": "index.js", "scripts": { + "prepare": "rimraf ./lib && tsc", "test": "mocha" }, + "files": [ + "lib/*", + "index.js" + ], "keywords": [ "homebridge", "homebridge-plugin", @@ -23,10 +28,19 @@ "url": "https://github.com/basdelfos/homebridge-tuya-web.git" }, "dependencies": { - "request": "^2.88.0" + "hap-nodejs": "^0.5.7", + "querystring": "^0.2.0", + "request": "^2.88.0", + "request-promise": "^4.2.5" + }, + "publishConfig": { + "access": "public" }, "devDependencies": { + "@types/request-promise": "^4.1.46", "chai": "^4.2.0", - "mocha": "^6.2.0" + "mocha": "^6.2.0", + "rimraf": "^3.0.2", + "typescript": "^3.8.3" } } diff --git a/lib/base_accessory.js b/src/base_accessory.ts similarity index 68% rename from lib/base_accessory.js rename to src/base_accessory.ts index 31f7a67..00550e3 100644 --- a/lib/base_accessory.js +++ b/src/base_accessory.ts @@ -1,32 +1,42 @@ -const TuyaWebApi = require('./tuyawebapi'); - -let PlatformAccessory; -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig, categoryType) { - this.platform = platform; +import { + Accessory, + Service, + Characteristic, + CharacteristicValue, +} from 'hap-nodejs'; +import { TuyaDevice } from './types'; + +export abstract class BaseAccessory { + public deviceId: string; + public log: any; + public cachedState: Map = new Map(); + public validCache: boolean = false; + public serviceType: typeof Service; + public service: Service; + + private PlatformAccessory; + private UUIDGen: any; + + constructor( + public platform, + public homebridgeAccessory, + public deviceConfig: TuyaDevice, + public categoryType + ) { this.deviceId = deviceConfig.id; this.categoryType = categoryType; - PlatformAccessory = platform.api.platformAccessory; - ({ Accessory, Service, Characteristic, uuid: UUIDGen } = platform.api.hap); + this.PlatformAccessory = platform.api.platformAccessory; + + this.UUIDGen = platform.api.hap.uuid; this.log = platform.log; this.homebridgeAccessory = homebridgeAccessory; this.deviceConfig = deviceConfig; - // Setup caching - this.cachedState = new Map(); - this.validCache = false; - - this.serviceType; switch (categoryType) { case Accessory.Categories.LIGHTBULB: - this.serviceType = Service.Lightbulb + this.serviceType = Service.Lightbulb; break; case Accessory.Categories.SWITCH: this.serviceType = Service.Switch; @@ -51,15 +61,16 @@ class BaseAccessory { 'Existing Accessory found [%s] [%s] [%s]', homebridgeAccessory.displayName, homebridgeAccessory.context.deviceId, - homebridgeAccessory.UUID); + homebridgeAccessory.UUID + ); this.homebridgeAccessory.displayName = this.deviceConfig.name; - } - else { + } else { this.log.info('Creating New Accessory %s', this.deviceConfig.id); - this.homebridgeAccessory = new PlatformAccessory( + this.homebridgeAccessory = new this.PlatformAccessory( this.deviceConfig.name, - UUIDGen.generate(this.deviceConfig.id), - categoryType); + this.UUIDGen.generate(this.deviceConfig.id), + categoryType + ); this.homebridgeAccessory.context.deviceId = this.deviceConfig.id; this.homebridgeAccessory.controller = this; this.platform.registerPlatformAccessory(this.homebridgeAccessory); @@ -68,11 +79,16 @@ class BaseAccessory { // Create service this.service = this.homebridgeAccessory.getService(this.serviceType); if (this.service) { - this.service.setCharacteristic(Characteristic.Name, this.deviceConfig.name); - } - else { + this.service.setCharacteristic( + Characteristic.Name, + this.deviceConfig.name + ); + } else { this.log.debug('Creating New Service %s', this.deviceConfig.id); - this.service = this.homebridgeAccessory.addService(this.serviceType, this.deviceConfig.name); + this.service = this.homebridgeAccessory.addService( + this.serviceType, + this.deviceConfig.name + ); } this.homebridgeAccessory.on('identify', (paired, callback) => { @@ -81,17 +97,20 @@ class BaseAccessory { }); } - updateAccessory(device) { + abstract async updateState(data: TuyaDevice['data']): Promise; + + updateAccessory(device: TuyaDevice) { // Update general accessory information if (device.name) { this.homebridgeAccessory.displayName = device.name; - this.homebridgeAccessory._associatedHAPAccessory.displayName = device.name; - var accessoryInformationService = ( + this.homebridgeAccessory._associatedHAPAccessory.displayName = + device.name; + var accessoryInformationService = this.homebridgeAccessory.getService(Service.AccessoryInformation) || - this.homebridgeAccessory.addService(Service.AccessoryInformation)); - var characteristicName = ( + this.homebridgeAccessory.addService(Service.AccessoryInformation); + var characteristicName = accessoryInformationService.getCharacteristic(Characteristic.Name) || - accessoryInformationService.addCharacteristic(Characteristic.Name)); + accessoryInformationService.addCharacteristic(Characteristic.Name); if (characteristicName) { characteristicName.setValue(device.name); } @@ -128,5 +147,3 @@ class BaseAccessory { this.validCache = false; } } - -module.exports = BaseAccessory; \ No newline at end of file diff --git a/src/dimmer_accessory.ts b/src/dimmer_accessory.ts new file mode 100644 index 0000000..29dc38d --- /dev/null +++ b/src/dimmer_accessory.ts @@ -0,0 +1,183 @@ +import { + Accessory, + Service, + Characteristic, + CharacteristicEventTypes, +} from 'hap-nodejs'; + +import TuyaWebApi from './tuyawebapi'; +import { BaseAccessory } from './base_accessory'; +import { pifyGetEvt, pifySetEvt } from './promisifyEvent'; +import { TuyaDevice } from './types'; + +export class DimmerAccessory extends BaseAccessory { + constructor(platform, homebridgeAccessory, deviceConfig: TuyaDevice) { + super( + platform, + homebridgeAccessory, + deviceConfig, + Accessory.Categories.LIGHTBULB + ); + + // Characteristic.On + this.service + .getCharacteristic(Characteristic.On) + .on( + CharacteristicEventTypes.GET, + pifyGetEvt(async () => { + // Retrieve state from cache + if (this.hasValidCache()) { + return this.getCachedState(Characteristic.On); + } else { + // Retrieve device state from Tuya Web API + try { + const data = await this.platform.tuyaWebApi.getDeviceState( + this.deviceId + ); + this.log.debug( + '[GET][%s] Characteristic.On: %s', + this.homebridgeAccessory.displayName, + data.state + ); + this.setCachedState(Characteristic.On, data.state); + return data.state; + } catch (error) { + this.log.error( + '[GET][%s] Characteristic.On Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + throw error; + } + } + }) + ) + .on( + CharacteristicEventTypes.SET, + pifySetEvt(async state => { + // Set device state in Tuya Web API + const value = state ? 1 : 0; + + try { + const result = await this.platform.tuyaWebApi.setDeviceState( + this.deviceId, + 'turnOnOff', + { value: value } + ); + + this.log.debug( + '[SET][%s] Characteristic.On: %s %s', + this.homebridgeAccessory.displayName, + state, + value + ); + this.setCachedState(Characteristic.On, state); + } catch (error) { + this.log.error( + '[SET][%s] Characteristic.On Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + throw error; + } + }) + ); + + // Characteristic.Brightness + this.service + .getCharacteristic(Characteristic.Brightness) + .on( + CharacteristicEventTypes.GET, + pifyGetEvt(async () => { + // Retrieve state from cache + if (this.hasValidCache()) { + return this.getCachedState(Characteristic.Brightness); + } else { + // Retrieve device state from Tuya Web API + try { + const data = await this.platform.tuyaWebApi.getDeviceState( + this.deviceId + ); + + const percentage = Math.floor( + (parseInt(data.brightness) / 255) * 100 + ); + this.log.debug( + '[GET][%s] Characteristic.Brightness: %s (%s percent)', + this.homebridgeAccessory.displayName, + data.brightness, + percentage + ); + this.setCachedState(Characteristic.Brightness, percentage); + return percentage; + } catch (error) { + this.log.error( + '[GET][%s] Characteristic.Brightness Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + throw error; + } + } + }) + ) + .on( + CharacteristicEventTypes.SET, + pifySetEvt(async percentage => { + // NOTE: For some strange reason, the set value for brightness is in percentage. + + // Set device state in Tuya Web API + try { + const data = await this.platform.tuyaWebApi.setDeviceState( + this.deviceId, + 'brightnessSet', + { + value: percentage, + } + ); + + this.log.debug( + '[SET][%s] Characteristic.Brightness: %s percent', + this.homebridgeAccessory.displayName, + percentage + ); + this.setCachedState(Characteristic.Brightness, percentage); + } catch (error) { + this.log.error( + '[SET][%s] Characteristic.Brightness Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + throw error; + } + }) + ); + } + + async updateState(data: TuyaDevice['data']) { + // Update device type specific state + this.log.debug( + '[UPDATING][%s]:', + this.homebridgeAccessory.displayName, + data + ); + if (data.state) { + const state = data.state === 'true'; + this.service.getCharacteristic(Characteristic.On).updateValue(state); + this.setCachedState(Characteristic.On, state); + } + if (data.percentage | data.brightness) { + const percentage = Math.floor( + (parseInt((data.percentage || data.brightness).toString()) / 255) * 100 + ); + this.service + .getCharacteristic(Characteristic.Brightness) + .updateValue(percentage); + this.setCachedState(Characteristic.Brightness, percentage); + } + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d9a9916 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,207 @@ +import { DimmerAccessory } from './dimmer_accessory'; +import { LightAccessory } from './light_accessory'; +import { OutletAccessory } from './outlet_accessory'; +import TuyaWebApi from './tuyawebapi'; +import { TuyaDevice, PlatformAccessory } from './types'; + +export default function(homebridge) { + homebridge.registerPlatform( + 'homebridge-tuya-web', + 'TuyaWebPlatform', + TuyaWebPlatform, + true + ); +} + +class TuyaWebPlatform { + private pollingInterval: number; + private refreshInterval: NodeJS.Timeout; + private tuyaWebApi: TuyaWebApi; + private accessories: Map; + + constructor(private log, private config, private api) { + this.pollingInterval = 10; // default 10 seconds + this.refreshInterval; + + if (!config || !config.options) { + this.log('No config found, disabling plugin.'); + return; + } + + // Set cloud polling interval + this.pollingInterval = this.config.options.pollingInterval || 10; + + // Create Tuya Web API instance + this.tuyaWebApi = new TuyaWebApi( + this.config.options.username, + this.config.options.password, + this.config.options.countryCode, + this.config.options.platform, + this.log + ); + + this.accessories = new Map(); + + if (api) { + // Save the API object as plugin needs to register new accessory via this object + this.api = api; + + // Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories. + // Platform Plugin should only register new accessory that doesn't exist in homebridge after this event. + // Or start discover new accessories. + this.api.on('didFinishLaunching', async () => { + this.log('Initializing TuyaWebPlatform...'); + try { + // Get access token + const token = await this.tuyaWebApi.getOrRefreshToken(); + + // Start discovery for devices + const devices = await this.tuyaWebApi.discoverDevices(); + + // Add devices to Homebridge + for (const device of devices) { + this.addAccessory(device); + } + // Get device strate of all devices - once + this.refreshDeviceStates(); + } catch (error) { + this.log.error(error); + } + }); + + // Set interval for refreshing device states + this.refreshInterval = setInterval(() => { + this.refreshDeviceStates(); + }, this.pollingInterval * 1000); + } + } + + async refreshDeviceStates() { + this.log.debug('Refreshing state for all devices...'); + try { + const devices = await this.tuyaWebApi.getAllDeviceStates(); + + // Refresh device states + for (const device of devices) { + const uuid = this.api.hap.uuid.generate(device.id); + const homebridgeAccessory = this.accessories.get(uuid); + + if (homebridgeAccessory) { + homebridgeAccessory.controller.updateAccessory(device); + } else { + this.log.error('Could not find accessory in dictionary'); + } + } + } catch (error) { + this.log.error('Error retrieving devices states', error); + } + } + + addAccessory(device: TuyaDevice) { + var deviceType = device.dev_type || 'switch'; + this.log.info( + 'Adding: %s (%s / %s)', + device.name || 'unnamed', + deviceType, + device.id + ); + + // Get UUID + const uuid = this.api.hap.uuid.generate(device.id); + const homebridgeAccessory = this.accessories.get(uuid); + + // Is device type overruled in config defaults? + if (this.config.defaults) { + for (const def of this.config.defaults) { + if (def.id === device.id) { + deviceType = def.device_type || deviceType; + this.log('Device type is overruled in config to: ', deviceType); + } + } + } + + // Construct new accessory + let deviceAccessory; + switch (deviceType) { + case 'light': + deviceAccessory = new LightAccessory(this, homebridgeAccessory, device); + this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); + break; + case 'dimmer': + deviceAccessory = new DimmerAccessory( + this, + homebridgeAccessory, + device + ); + this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); + break; + case 'switch': + case 'outlet': + case 'cover': + deviceAccessory = new OutletAccessory( + this, + homebridgeAccessory, + device + ); + this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); + break; + default: + this.log.warn('Could not init class for device type [%s]', deviceType); + break; + } + } + + // Called from device classes + registerPlatformAccessory(platformAccessory) { + this.log.debug( + 'Register Platform Accessory (%s)', + platformAccessory.displayName + ); + this.api.registerPlatformAccessories( + 'homebridge-tuya-web', + 'TuyaWebPlatform', + [platformAccessory] + ); + } + + // Function invoked when homebridge tries to restore cached accessory. + // Developer can configure accessory at here (like setup event handler). + // Update current value. + configureAccessory(accessory) { + this.log( + 'Configuring cached accessory [%s]', + accessory.displayName, + accessory.context.deviceId, + accessory.UUID + ); + + // Set the accessory to reachable if plugin can currently process the accessory, + // otherwise set to false and update the reachability later by invoking + // accessory.updateReachability() + accessory.reachable = true; + + accessory.on('identify', function(paired, callback) { + this.log.debug('[IDENTIFY][%s]', accessory.displayName); + callback(); + }); + + this.accessories.set(accessory.UUID, accessory); + } + + updateAccessoryReachability(accessory, state) { + this.log('Update Reachability [%s]', accessory.displayName, state); + accessory.updateReachability(state); + } + + // Sample function to show how developer can remove accessory dynamically from outside event + removeAccessory(accessory) { + this.log('Remove Accessory [%s]', accessory.displayName); + this.api.unregisterPlatformAccessories( + 'homebridge-tuya-web', + 'TuyaWebPlatform', + [accessory] + ); + + this.accessories.delete(accessory.uuid); + } +} diff --git a/src/light_accessory.ts b/src/light_accessory.ts new file mode 100644 index 0000000..abca1cf --- /dev/null +++ b/src/light_accessory.ts @@ -0,0 +1,380 @@ +import TuyaWebApi from './tuyawebapi'; +import { BaseAccessory } from './base_accessory'; +import { + Accessory, + Service, + Characteristic, + CharacteristicEventTypes, +} from 'hap-nodejs'; +import { TuyaDevice } from './types'; + +type Color = { + brightness?: number; + saturation?: number; + hue?: number; +}; + +export class LightAccessory extends BaseAccessory { + constructor(platform, homebridgeAccessory, deviceConfig: TuyaDevice) { + super( + platform, + homebridgeAccessory, + deviceConfig, + Accessory.Categories.LIGHTBULB + ); + + // homekit compatible defaults + const defaultBrightness = 100; // 100% + const defaultSaturation = 100; // 100% + const defaultHue = 359; // red (max hue) + + // Characteristic.On + this.service + .getCharacteristic(Characteristic.On) + .on(CharacteristicEventTypes.GET, callback => { + // Retrieve state from cache + if (this.hasValidCache()) { + callback(null, this.getCachedState(Characteristic.On)); + } else { + // Retrieve device state from Tuya Web API + this.platform.tuyaWebApi + .getDeviceState(this.deviceId) + .then(data => { + this.log.debug( + '[GET][%s] Characteristic.On: %s', + this.homebridgeAccessory.displayName, + data.state + ); + this.setCachedState(Characteristic.On, data.state); + callback(null, data.state); + }) + .catch(error => { + this.log.error( + '[GET][%s] Characteristic.On Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + } + }) + .on(CharacteristicEventTypes.SET, (isOn, callback) => { + // Set device state in Tuya Web API + const value = isOn ? 1 : 0; + + this.platform.tuyaWebApi + .setDeviceState(this.deviceId, 'turnOnOff', { value: value }) + .then(() => { + this.log.debug( + '[SET][%s] Characteristic.On: %s %s', + this.homebridgeAccessory.displayName, + isOn, + value + ); + this.setCachedState(Characteristic.On, isOn); + callback(); + }) + .catch(error => { + this.log.error( + '[SET][%s] Characteristic.On Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + }); + + // Characteristic.Brightness + this.service + .getCharacteristic(Characteristic.Brightness) + .on(CharacteristicEventTypes.GET, callback => { + // Retrieve state from cache + if (this.hasValidCache()) { + callback(null, this.getCachedState(Characteristic.Brightness)); + } else { + // Retrieve device state from Tuya Web API + this.platform.tuyaWebApi + .getDeviceState(this.deviceId) + .then(data => { + // data.brightness only valid for color_mode!=color > https://github.com/PaulAnnekov/tuyaha/blob/master/tuyaha/devices/light.py + // however, according to local tuya app, calculation for color_mode=color is stil incorrect (even more so in lower range) + + let rawValue; + let percentage; + + if (data.color_mode == 'color') { + rawValue = data.color.brightness; // 0-100 + percentage = rawValue; + } else { + rawValue = data.brightness; // 0-255 + percentage = Math.floor(rawValue / 255) * 100; // 0-100 + } + + this.log.debug( + '[GET][%s] Characteristic.Brightness: %s (%s percent)', + this.homebridgeAccessory.displayName, + rawValue, + percentage + ); + this.setCachedState(Characteristic.Brightness, percentage); + callback(null, percentage); + }) + .catch(error => { + this.log.error( + '[GET][%s] Characteristic.Brightness Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + } + }) + .on(CharacteristicEventTypes.SET, (percentage, callback) => { + // NOTE: For some strange reason, the set value for brightness is in percentage + const value = percentage; // 0-100 + + // Set device state in Tuya Web API + this.platform.tuyaWebApi + .setDeviceState(this.deviceId, 'brightnessSet', { value: value }) + .then(() => { + this.log.debug( + '[SET][%s] Characteristic.Brightness: %s percent', + this.homebridgeAccessory.displayName, + percentage + ); + this.setCachedState(Characteristic.Brightness, percentage); + callback(); + }) + .catch(error => { + this.log.error( + '[SET][%s] Characteristic.Brightness Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + }); + + // Characteristic.Saturation + this.service + .getCharacteristic(Characteristic.Saturation) + .on(CharacteristicEventTypes.GET, callback => { + // Retrieve state from cache + if (this.hasValidCache()) { + callback(null, this.getCachedState(Characteristic.Saturation)); + } else { + // Retrieve device state from Tuya Web API + this.platform.tuyaWebApi + .getDeviceState(this.deviceId) + .then(data => { + if (data.color) { + const saturation = data.color.saturation; // 0-100 + const hue = data.color.hue; // 0-359 + this.log.debug( + '[GET][%s] Characteristic.Saturation: %s', + this.homebridgeAccessory.displayName, + saturation + ); + this.setCachedState(Characteristic.Saturation, saturation); + this.setCachedState(Characteristic.Hue, hue); + callback(null, saturation); + } else { + callback(null, null); + } + }) + .catch(error => { + this.log.error( + '[GET][%s] Characteristic.Saturation Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + } + }) + .on(CharacteristicEventTypes.SET, (percentage, callback) => { + let color: Color = {}; + + const cachedBrightness = this.getCachedState( + Characteristic.Brightness + ) as number; + const cachedHue = this.getCachedState(Characteristic.Hue) as number; + + color.brightness = cachedBrightness + ? cachedBrightness + : defaultBrightness; // 0-100 + color.saturation = Math.floor(percentage / 100) * 255; // 0-255 + color.hue = cachedHue ? cachedHue : defaultHue; // 0-359 + + // Set device state in Tuya Web API + this.platform.tuyaWebApi + .setDeviceState(this.deviceId, 'colorSet', { color: color }) + .then(() => { + this.log.debug( + '[SET][%s] Characteristic.Saturation: (%s) %s percent', + this.homebridgeAccessory.displayName, + color.saturation, + percentage + ); + this.setCachedState(Characteristic.Brightness, color.brightness); + this.setCachedState(Characteristic.Saturation, percentage); + this.setCachedState(Characteristic.Hue, color.hue); + callback(); + }) + .catch(error => { + this.log.error( + '[SET][%s] Characteristic.Saturation Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + }); + + // Characteristic.Hue + this.service + .getCharacteristic(Characteristic.Hue) + .on(CharacteristicEventTypes.GET, callback => { + // Retrieve state from cache + if (this.hasValidCache()) { + callback(null, this.getCachedState(Characteristic.Hue)); + } else { + // Retrieve device state from Tuya Web API + this.platform.tuyaWebApi + .getDeviceState(this.deviceId) + .then(data => { + if (data.color) { + const saturation = data.color.saturation; // 0-100 + const hue = data.color.hue; // 0-359 + this.log.debug( + '[GET][%s] Characteristic.Hue: %s', + this.homebridgeAccessory.displayName, + hue + ); + this.setCachedState(Characteristic.Saturation, saturation); + this.setCachedState(Characteristic.Hue, hue); + callback(null, hue); + } else { + callback(null, null); + } + }) + .catch(error => { + this.log.error( + '[GET][%s] Characteristic.Hue Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + } + }) + .on(CharacteristicEventTypes.SET, (hue, callback) => { + let color: Color = {}; + + const cachedBrightness = this.getCachedState( + Characteristic.Brightness + ) as number; + const cachedSaturation = this.getCachedState( + Characteristic.Saturation + ) as number; + + color.brightness = cachedBrightness + ? cachedBrightness + : defaultBrightness; // 0-100 + color.saturation = cachedSaturation + ? Math.floor(cachedSaturation / 100) * 255 + : defaultSaturation; // 0-255 + const newSaturationPercentage = + Math.floor(color.saturation / 255) * 100; + color.hue = hue; + + // Set device state in Tuya Web API + this.platform.tuyaWebApi + .setDeviceState(this.deviceId, 'colorSet', { color: color }) + .then(() => { + this.log.debug( + '[SET][%s] Characteristic.Hue: %s', + this.homebridgeAccessory.displayName, + hue + ); + this.setCachedState(Characteristic.Brightness, color.brightness); + this.setCachedState( + Characteristic.Saturation, + newSaturationPercentage + ); + this.setCachedState(Characteristic.Hue, color.hue); + callback(); + }) + .catch(error => { + this.log.error( + '[SET][%s] Characteristic.Hue Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + }); + } + + async updateState(data: TuyaDevice['data']) { + // Update device type specific state + this.log.debug( + '[UPDATING][%s]:', + this.homebridgeAccessory.displayName, + data + ); + + if (data.state) { + const isOn = data.state === 'true'; + this.service.getCharacteristic(Characteristic.On).updateValue(isOn); + this.setCachedState(Characteristic.On, isOn); + } + if (data.brightness || data.color.brightness) { + let rawValue; + let percentage; + + if (data.color_mode == 'color') { + rawValue = data.color.brightness; // 0-100 + percentage = rawValue; + } else { + rawValue = data.brightness; // 0-255 + percentage = Math.floor(rawValue / 255) * 100; // 0-100 + } + + this.service + .getCharacteristic(Characteristic.Brightness) + .updateValue(percentage); + this.setCachedState(Characteristic.Brightness, percentage); + } + if (data.color && data.color.saturation) { + let rawValue; + let percentage; + + if (data.color_mode == 'color') { + rawValue = data.color.brightness; // 0-100 + percentage = rawValue; + } else { + rawValue = data.brightness; // 0-255 + percentage = Math.floor(rawValue / 255) * 100; // 0-100 + } + + this.service + .getCharacteristic(Characteristic.Saturation) + .updateValue(percentage); + this.setCachedState(Characteristic.Saturation, percentage); + } + if (data.color && data.color.hue) { + const hue = data.color.hue; + this.service.getCharacteristic(Characteristic.Hue).updateValue(hue); + this.setCachedState(Characteristic.Hue, hue); + } + } +} diff --git a/src/outlet_accessory.ts b/src/outlet_accessory.ts new file mode 100644 index 0000000..0865912 --- /dev/null +++ b/src/outlet_accessory.ts @@ -0,0 +1,92 @@ +import { + Accessory, + Service, + Characteristic, + CharacteristicEventTypes, +} from 'hap-nodejs'; + +import TuyaWebApi from './tuyawebapi'; +import { BaseAccessory } from './base_accessory'; +import { TuyaDevice } from './types'; + +export class OutletAccessory extends BaseAccessory { + constructor(platform, homebridgeAccessory, deviceConfig: TuyaDevice) { + super( + platform, + homebridgeAccessory, + deviceConfig, + Accessory.Categories.OUTLET + ); + + this.service + .getCharacteristic(Characteristic.On) + .on(CharacteristicEventTypes.GET, callback => { + // Retrieve state from cache + if (this.hasValidCache()) { + callback(null, this.getCachedState(Characteristic.On)); + } else { + // Retrieve device state from Tuya Web API + this.platform.tuyaWebApi + .getDeviceState(this.homebridgeAccessory.context.deviceId) + .then(data => { + this.log.debug( + '[GET][%s] Characteristic.On: %s', + this.homebridgeAccessory.displayName, + data.state + ); + this.setCachedState(Characteristic.On, data.state); + callback(null, data.state); + }) + .catch(error => { + this.log.error( + '[GET][%s] Characteristic.On Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + } + }) + .on(CharacteristicEventTypes.SET, (state, callback) => { + // Set device state in Tuya Web API + const value = state ? 1 : 0; + this.platform.tuyaWebApi + .setDeviceState( + this.homebridgeAccessory.context.deviceId, + 'turnOnOff', + { value: value } + ) + .then(() => { + this.log.debug( + '[SET][%s] Characteristic.On: %s %s', + this.homebridgeAccessory.displayName, + state, + value + ); + this.setCachedState(Characteristic.On, state); + callback(); + }) + .catch(error => { + this.log.error( + '[SET][%s] Characteristic.On Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + }); + } + + async updateState(data: TuyaDevice['data']) { + this.log.debug( + '[UPDATING][%s]:', + this.homebridgeAccessory.displayName, + data + ); + const state = data.state === true; + this.service.getCharacteristic(Characteristic.On).updateValue(data.state); + this.setCachedState(Characteristic.On, state); + } +} diff --git a/src/promisifyEvent.ts b/src/promisifyEvent.ts new file mode 100644 index 0000000..22fe3a4 --- /dev/null +++ b/src/promisifyEvent.ts @@ -0,0 +1,31 @@ +import { + CharacteristicValue, + CharacteristicGetCallback, + CharacteristicSetCallback, +} from 'hap-nodejs'; + +type Callback = (data: any) => void; + +export const pifyGetEvt = (evt: () => Promise) => ( + callback: CharacteristicGetCallback +) => { + evt() + .then(data => { + callback(null, data); + }) + .catch(err => { + callback(err); + }); +}; + +export const pifySetEvt = ( + evt: (data: CharacteristicValue) => Promise +) => (data: CharacteristicValue, callback: CharacteristicSetCallback) => { + evt(data) + .then(data => { + callback(null); + }) + .catch(err => { + callback(err); + }); +}; diff --git a/src/switch_accessory.ts b/src/switch_accessory.ts new file mode 100644 index 0000000..7261253 --- /dev/null +++ b/src/switch_accessory.ts @@ -0,0 +1,88 @@ +import { + Accessory, + Service, + Characteristic, + CharacteristicEventTypes, +} from 'hap-nodejs'; + +import TuyaWebApi from './tuyawebapi'; +import { BaseAccessory } from './base_accessory'; +import { TuyaDevice } from './types'; + +export class SwitchAccessory extends BaseAccessory { + constructor(platform, homebridgeAccessory, deviceConfig) { + super( + platform, + homebridgeAccessory, + deviceConfig, + Accessory.Categories.SWITCH + ); + + this.service + .getCharacteristic(Characteristic.On) + .on(CharacteristicEventTypes.GET, callback => { + // Retrieve state from cache + if (this.hasValidCache()) { + callback(null, this.getCachedState(Characteristic.On)); + } else { + // Retrieve device state from Tuya Web API + this.platform.tuyaWebApi + .getDeviceState(this.deviceId) + .then(data => { + this.log.debug( + '[GET][%s] Characteristic.On: %s', + this.homebridgeAccessory.displayName, + data.state + ); + this.setCachedState(Characteristic.On, data.state); + callback(null, data.state); + }) + .catch(error => { + this.log.error( + '[GET][%s] Characteristic.On Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + } + }) + .on(CharacteristicEventTypes.SET, (state, callback) => { + // Set device state in Tuya Web API + const value = state ? 1 : 0; + this.platform.tuyaWebApi + .setDeviceState(this.deviceId, 'turnOnOff', { value: value }) + .then(() => { + this.log.debug( + '[SET][%s] Characteristic.On: %s %s', + this.homebridgeAccessory.displayName, + state, + value + ); + this.setCachedState(Characteristic.On, state); + callback(); + }) + .catch(error => { + this.log.error( + '[SET][%s] Characteristic.On Error: %s', + this.homebridgeAccessory.displayName, + error + ); + this.invalidateCache(); + callback(error); + }); + }); + } + + async updateState(data: TuyaDevice['data']) { + this.log.debug( + '[UPDATING][%s]:', + this.homebridgeAccessory.displayName, + data + ); + const state = data.state == true; + this.service.getCharacteristic(Characteristic.On).updateValue(data.state); + this.setCachedState(Characteristic.On, state); + } +} diff --git a/src/tuyawebapi.ts b/src/tuyawebapi.ts new file mode 100644 index 0000000..710fcd3 --- /dev/null +++ b/src/tuyawebapi.ts @@ -0,0 +1,306 @@ +import request from 'request-promise'; +import querystring from 'querystring'; +import { TuyaDevice } from './types'; + +class Session { + private expiresOn: number; + private token: { expiresOn: number }; + + constructor( + public accessToken?: string, + public refreshToken?: string, + private expiresIn?: number, + public areaCode?: string, + public areaBaseUrl?: string + ) { + this.resetToken(accessToken, refreshToken, expiresIn); + } + + resetToken(accessToken: string, refreshToken: string, expiresIn: number) { + this.accessToken = accessToken; + this.refreshToken = refreshToken; + this.expiresOn = this._getCurrentEpoch() + expiresIn - 100; // subtract 100 ticks to expire token before it actually does + } + + hasToken() { + return this.accessToken && true; + } + + isTokenExpired() { + return this.token.expiresOn > this._getCurrentEpoch(); + } + + hasValidToken() { + return this.accessToken && this.expiresOn > this._getCurrentEpoch(); + } + + _getCurrentEpoch() { + return Math.round(new Date().getTime() / 1000); + } +} + +class TuyaWebApi { + private session: Session; + private authBaseUrl: string; + + constructor( + private username: string, + private password: string, + private countryCode: string, + private tuyaPlatform: string = 'tuya', + private log = null + ) { + this.session = new Session(); + + this.authBaseUrl = 'https://px1.tuyaeu.com'; + } + + async discoverDevices() { + if (!this.session.hasValidToken()) { + throw new Error('No valid token'); + } + + var data = { + header: { + name: 'Discovery', + namespace: 'discovery', + payloadVersion: 1, + }, + payload: { + accessToken: this.session.accessToken, + }, + }; + + const obj = await this.sendRequestJson<{ + devices: TuyaDevice[]; + scenes: {}[]; + }>( + this.session.areaBaseUrl + '/homeassistant/skill', + JSON.stringify(data), + 'GET' + ); + + if (obj.header && obj.header.code === 'SUCCESS') { + if (obj.payload && obj.payload.devices) { + return obj.payload.devices; + } + } + throw new Error(`No valid response from API ${JSON.stringify(obj)}`); + } + + getAllDeviceStates() { + return this.discoverDevices(); + } + + async getDeviceState(deviceId: string) { + if (!this.session.hasValidToken()) { + throw new Error('No valid token'); + } + + var data = { + header: { + name: 'QueryDevice', + namespace: 'query', + payloadVersion: 1, + }, + payload: { + accessToken: this.session.accessToken, + devId: deviceId, + value: 1, + }, + }; + + const obj = await this.sendRequestJson<{ + data: { + support_stop: boolean; + online: boolean; + state: number | string; + }; + }>( + this.session.areaBaseUrl + '/homeassistant/skill', + JSON.stringify(data), + 'GET' + ); + + if (obj.payload && obj.header && obj.header.code === 'SUCCESS') { + return obj.payload.data; + } else { + throw new Error(`Invalid payload in response: ${JSON.stringify(obj)}`); + } + } + + async setDeviceState(deviceId: string, method: string, payload: any) { + if (!this.session.hasValidToken()) { + throw new Error('No valid token'); + } + + /* Methods + * turnOnOff -> 0 = off, 1 = on + * brightnessSet --> 0..100 + */ + + var data = { + header: { + name: method, + namespace: 'control', + payloadVersion: 1, + }, + payload: payload, + }; + + data.payload.accessToken = this.session.accessToken; + data.payload.devId = deviceId; + + const obj = await this.sendRequestJson( + this.session.areaBaseUrl + '/homeassistant/skill', + JSON.stringify(data), + 'POST' + ); + if (!(obj.header && obj.header.code === 'SUCCESS')) { + throw new Error(`Invalid payload in response: ${JSON.stringify(obj)}`); + } + } + + async getOrRefreshToken(): Promise { + if (!this.session.hasToken()) { + // No token, lets get a token from the Tuya Web API + if (!this.username) { + throw new Error('No username configured'); + } else { + if (!this.password) { + throw new Error('No password configured'); + } else { + if (!this.countryCode) { + throw new Error('No country code configured'); + } else { + var form = { + userName: this.username, + password: this.password, + countryCode: this.countryCode, + bizType: this.tuyaPlatform, + from: 'tuya', + }; + + var formData = querystring.stringify(form); + var contentLength = formData.length; + + return new Promise((resolve, reject) => { + request( + { + headers: { + 'Content-Length': contentLength, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + uri: this.authBaseUrl + '/homeassistant/auth.do', + body: formData, + method: 'POST', + }, + (err, res, body) => { + if (err) { + reject( + new Error( + `Authentication fault, could not retreive token. ${err.toString()}` + ) + ); + } else { + let obj; + try { + obj = JSON.parse(body); + } catch (error) { + reject( + new Error( + `Could not parse json. Body: ${body} ${error}` + ) + ); + } + if (obj.responseStatus === 'error') { + reject( + new Error('Authentication fault: ' + obj.errorMsg) + ); + } else { + // Received token + this.session.resetToken( + obj.access_token, + obj.refresh_token, + obj.expires_in + ); + // Change url based on areacode in accesstoken first two chars + this.session.areaCode = obj.access_token.substr(0, 2); + switch (this.session.areaCode) { + case 'AY': + this.session.areaBaseUrl = 'https://px1.tuyacn.com'; + break; + case 'EU': + this.session.areaBaseUrl = 'https://px1.tuyaeu.com'; + break; + case 'US': + default: + this.session.areaBaseUrl = 'https://px1.tuyaus.com'; + } + resolve(this.session); + } + } + } + ); + }); + } + } + } + } else { + if (this.session.hasToken() && this.session.isTokenExpired()) { + // Refresh token + + const obj = JSON.parse( + await this.sendRequest( + this.session.areaBaseUrl + + '/homeassistant/access.do?grant_type=refresh_token&refresh_token=' + + this.session.refreshToken, + '', + 'GET' + ) + ); + this.session.resetToken( + obj.access_token, + obj.refresh_token, + obj.expires_in + ); + + return this.session; + } + } + } + + /* + * -------------------------------------- + * HTTP methods + */ + + sendRequest(url: string, body: any, method: string) { + return request({ + url: url, + body: body, + method: method, + rejectUnauthorized: false, + }); + } + + async sendRequestJson( + url: string, + body: any, + method: string + ) { + // this.log.debug(JSON.stringify(body)); + const result = await this.sendRequest(url, body, method); + try { + const obj = JSON.parse(result); + return obj as { + payload: payload; + header: { code: string; payloadVersion: 1 }; + }; + } catch (err) { + throw new Error(`Could not parse json. Body: ${result} ${err}`); + } + } +} + +export default TuyaWebApi; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..4684fe2 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,22 @@ +export type TuyaDevice = { + data: { + online: boolean; + state: number | string | boolean; + color_mode?: string; + percentage?: number; + brightness?: number; + support_stop?: boolean; + color?: { + brightness?: number; + saturation?: number; + hue?: number; + }; + }; + name: string; + icon: string; + id: string; + dev_type: string; + ha_type: string; +}; + +export type PlatformAccessory = any; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..612f649 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2015", + "module": "CommonJS", + "declaration": true, + "outDir": "./lib", + "strict": false, + "esModuleInterop": true, + "allowJs": true + }, + "include": ["src/*"], + "exclude": ["node_modules", "**/__tests__/*"] +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..3cfcadd --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1208 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/bluebird@*": + version "3.5.30" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.30.tgz#ee034a0eeea8b84ed868b1aa60d690b08a6cfbc5" + integrity sha512-8LhzvcjIoqoi1TghEkRMkbbmM+jhHnBokPGkJWjclMK+Ks0MxEBow3/p2/iFTZ+OIbJHQDSfpgdZEb+af3gfVw== + +"@types/caseless@*": + version "0.12.2" + resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8" + integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w== + +"@types/node@*": + version "13.9.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-13.9.5.tgz#59738bf30b31aea1faa2df7f4a5f55613750cf00" + integrity sha512-hkzMMD3xu6BrJpGVLeQ3htQQNAcOrJjX7WFmtK8zWQpz2UJf13LCFF2ALA7c9OVdvc2vQJeDdjfR35M0sBCxvw== + +"@types/request-promise@^4.1.46": + version "4.1.46" + resolved "https://registry.yarnpkg.com/@types/request-promise/-/request-promise-4.1.46.tgz#37df6efae984316dfbfbbe8fcda37f3ba52822f2" + integrity sha512-3Thpj2Va5m0ji3spaCk8YKrjkZyZc6RqUVOphA0n/Xet66AW/AiOAs5vfXhQIL5NmkaO7Jnun7Nl9NEjJ2zBaw== + dependencies: + "@types/bluebird" "*" + "@types/request" "*" + +"@types/request@*": + version "2.48.4" + resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.4.tgz#df3d43d7b9ed3550feaa1286c6eabf0738e6cf7e" + integrity sha512-W1t1MTKYR8PxICH+A4HgEIPuAC3sbljoEVfyZbeFJJDbr30guDspJri2XOaM2E+Un7ZjrihaDi7cf6fPa2tbgw== + dependencies: + "@types/caseless" "*" + "@types/node" "*" + "@types/tough-cookie" "*" + form-data "^2.5.0" + +"@types/tough-cookie@*": + version "2.3.6" + resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5" + integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ== + +ajv@^6.5.5: + version "6.12.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.0.tgz#06d60b96d87b8454a5adaba86e7854da629db4b7" + integrity sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-colors@3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" + integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-flatten@^2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099" + integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ== + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assertion-error@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" + integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== + +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha1-x57Zf380y48robyXkLzDZkdLS3k= + +aws-sign2@~0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" + integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= + +aws4@^1.8.0: + version "1.9.1" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" + integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +bcrypt-pbkdf@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz#a4301d389b6a43f9b67ff3ca11a3f6637e360e9e" + integrity sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4= + dependencies: + tweetnacl "^0.14.3" + +bluebird@^3.5.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" + integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== + +bonjour-hap@^3.5.1: + version "3.5.4" + resolved "https://registry.yarnpkg.com/bonjour-hap/-/bonjour-hap-3.5.4.tgz#ba7d2e6220000cdb50bb80078a72ccdbcefa1c6f" + integrity sha512-MgU27SEZYQ09Skm71Xa7SZoAg259V4IlAJNWaloFVMlYDn1OjJy3nkwSixRHnDzrcLWocVI84f9exI1ObZChBw== + dependencies: + array-flatten "^2.1.0" + deep-equal "^1.0.1" + dns-equal "^1.0.0" + dns-txt "^2.0.2" + multicast-dns "^7.2.0" + multicast-dns-service-types "^1.1.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +browser-stdout@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/browser-stdout/-/browser-stdout-1.3.1.tgz#baa559ee14ced73452229bad7326467c61fabd60" + integrity sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw== + +buffer-indexof@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-indexof/-/buffer-indexof-1.1.1.tgz#52fabcc6a606d1a00302802648ef68f639da268c" + integrity sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g== + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +caseless@~0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc" + integrity sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw= + +chai@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/chai/-/chai-4.2.0.tgz#760aa72cf20e3795e84b12877ce0e83737aa29e5" + integrity sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw== + dependencies: + 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@^2.0.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +check-error@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" + integrity sha1-V00xLt2Iu13YkS6Sht1sCu1KrII= + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +combined-stream@^1.0.6, combined-stream@~1.0.6: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +core-util-is@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= + +dashdash@^1.12.0: + version "1.14.1" + resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" + integrity sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA= + dependencies: + assert-plus "^1.0.0" + +debug@3.2.6: + version "3.2.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" + integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== + dependencies: + ms "^2.1.1" + +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +decimal.js@^7.2.3: + version "7.5.1" + resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-7.5.1.tgz#cf4cf5eeb9faa24fc4ee6af361faebb7bfcca2ce" + integrity sha512-1K5Y6MykxQYfHBcFfAj2uBaLmwreq4MsjsvrlgcEOvg+X82IeeXlIVIVkBMiypksu+yo9vcYP6lfU3qTedofSQ== + +deep-eql@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-3.0.1.tgz#dfc9404400ad1c8fe023e7da1df1c147c4b444df" + integrity sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw== + dependencies: + type-detect "^4.0.0" + +deep-equal@^1.0.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" + integrity sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g== + dependencies: + is-arguments "^1.0.4" + is-date-object "^1.0.1" + is-regex "^1.0.4" + object-is "^1.0.1" + object-keys "^1.1.1" + regexp.prototype.flags "^1.2.0" + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= + +diff@3.5.0: + version "3.5.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" + integrity sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA== + +dns-equal@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/dns-equal/-/dns-equal-1.0.0.tgz#b39e7f1da6eb0a75ba9c17324b34753c47e0654d" + integrity sha1-s55/HabrCnW6nBcySzR1PEfgZU0= + +dns-packet@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-4.2.0.tgz#3fd6f5ff5a4ec3194ed0b15312693ffe8776b343" + integrity sha512-bn1AKpfkFbm0MIioOMHZ5qJzl2uypdBwI4nYNsqvhjsegBhcKJUlCrMPWLx6JEezRjxZmxhtIz/FkBEur2l8Cw== + dependencies: + ip "^1.1.5" + safe-buffer "^5.1.1" + +dns-txt@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/dns-txt/-/dns-txt-2.0.2.tgz#b91d806f5d27188e4ab3e7d107d881a1cc4642b6" + integrity sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY= + dependencies: + buffer-indexof "^1.0.0" + +ecc-jsbn@~0.1.1: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz#3a83a904e54353287874c564b7549386849a98c9" + integrity sha1-OoOpBOVDUyh4dMVkt1SThoSamMk= + dependencies: + jsbn "~0.1.0" + safer-buffer "^2.1.0" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +es-abstract@^1.17.0-next.1: + version "1.17.5" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.5.tgz#d8c9d1d66c8981fb9200e2251d799eee92774ae9" + integrity sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg== + dependencies: + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + is-callable "^1.1.5" + is-regex "^1.0.5" + object-inspect "^1.7.0" + object-keys "^1.1.1" + object.assign "^4.1.0" + string.prototype.trimleft "^2.1.1" + string.prototype.trimright "^2.1.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +extend@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" + integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== + +extsprintf@1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" + integrity sha1-lpGEQOMEGnpBT4xS48V06zw+HgU= + +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= + +fast-deep-equal@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz#545145077c501491e33b15ec408c294376e94ae4" + integrity sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-srp-hap@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/fast-srp-hap/-/fast-srp-hap-1.0.1.tgz#377124d196bc6a5157aae5b37bf5fa35bb4ad2d9" + integrity sha1-N3Ek0Za8alFXquWze/X6NbtK0tk= + +find-up@3.0.0, find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +flat@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/flat/-/flat-4.1.0.tgz#090bec8b05e39cba309747f1d588f04dbaf98db2" + integrity sha512-Px/TiLIznH7gEDlPXcUD4KnBusa6kR6ayRUVcnEAbreRIuhkqow/mun59BuRXwoYk7ZQOLW1ZM05ilIvK38hFw== + dependencies: + is-buffer "~2.0.3" + +forever-agent@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" + integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= + +form-data@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.5.1.tgz#f2cbec57b5e59e23716e128fe44d4e5dd23895f4" + integrity sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +form-data@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6" + integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.6" + mime-types "^2.1.12" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-func-name@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" + integrity sha1-6td0q+5y4gQJQzoGY2YCPdaIekE= + +getpass@^0.1.1: + version "0.1.7" + resolved "https://registry.yarnpkg.com/getpass/-/getpass-0.1.7.tgz#5eff8e3e684d569ae4cb2b1282604e8ba62149fa" + integrity sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo= + dependencies: + assert-plus "^1.0.0" + +glob@7.1.3: + version "7.1.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" + integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== + dependencies: + 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@^7.1.3: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + 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" + +growl@1.10.5: + version "1.10.5" + resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e" + integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA== + +hap-nodejs@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/hap-nodejs/-/hap-nodejs-0.5.7.tgz#24eb961d64a5c65aa24d1b293748e486da43bbd4" + integrity sha512-SQJ0Fq+yImvwVWYb8U3hZbwnB+8OeTBLewBU70DTUmujzkrUSQVv+fZG/hjuyHWD03TU8/Y/8fyHQ1y57NkXoA== + dependencies: + bonjour-hap "^3.5.1" + debug "^2.2.0" + decimal.js "^7.2.3" + fast-srp-hap "^1.0.1" + ip "^1.1.3" + node-persist "^0.0.11" + tweetnacl "^1.0.1" + +har-schema@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92" + integrity sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI= + +har-validator@~5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080" + integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g== + dependencies: + ajv "^6.5.5" + har-schema "^2.0.0" + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0, has-symbols@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.1.tgz#9f5214758a44196c406d9bd76cebf81ec2dd31e8" + integrity sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +he@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" + integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== + +http-signature@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" + integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= + dependencies: + assert-plus "^1.0.0" + jsprim "^1.2.2" + sshpk "^1.7.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +ip@^1.1.3, ip@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" + integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= + +is-arguments@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.0.4.tgz#3faf966c7cba0ff437fb31f6250082fcf0448cf3" + integrity sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA== + +is-buffer@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== + +is-callable@^1.1.4, is-callable@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.5.tgz#f7e46b596890456db74e7f6e976cb3273d06faab" + integrity sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q== + +is-date-object@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.2.tgz#bda736f2cd8fd06d32844e7743bfa7494c3bfd7e" + integrity sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g== + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-regex@^1.0.4, is-regex@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.5.tgz#39d589a358bf18967f726967120b8fc1aed74eae" + integrity sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ== + dependencies: + has "^1.0.3" + +is-symbol@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.3.tgz#38e1014b9e6329be0de9d24a414fd7441ec61937" + integrity sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ== + dependencies: + has-symbols "^1.0.1" + +is-typedarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha1-5HnICFjfDBsR3dppQPlgEfzaSpo= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +isstream@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" + integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= + +js-yaml@3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +jsbn@~0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" + integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM= + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema@0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" + integrity sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM= + +json-stringify-safe@~5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" + integrity sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus= + +jsprim@^1.2.2: + version "1.4.1" + resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2" + integrity sha1-MT5mvB5cwG5Di8G3SZwuXFastqI= + dependencies: + assert-plus "1.0.0" + extsprintf "1.3.0" + json-schema "0.2.3" + verror "1.10.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash@^4.17.15: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +log-symbols@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" + integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== + dependencies: + chalk "^2.0.1" + +mime-db@1.43.0: + version "1.43.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" + integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== + +mime-types@^2.1.12, mime-types@~2.1.19: + version "2.1.26" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" + integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + dependencies: + mime-db "1.43.0" + +minimatch@3.0.4, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" + integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== + +mkdirp@0.5.4, mkdirp@~0.5.1: + version "0.5.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" + integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== + dependencies: + minimist "^1.2.5" + +mocha@^6.2.0: + version "6.2.3" + resolved "https://registry.yarnpkg.com/mocha/-/mocha-6.2.3.tgz#e648432181d8b99393410212664450a4c1e31912" + integrity sha512-0R/3FvjIGH3eEuG17ccFPk117XL2rWxatr81a57D+r/x2uTYZRbdZ4oVidEUMh2W2TJDa7MdAb12Lm2/qrKajg== + dependencies: + ansi-colors "3.2.3" + browser-stdout "1.3.1" + debug "3.2.6" + diff "3.5.0" + escape-string-regexp "1.0.5" + find-up "3.0.0" + glob "7.1.3" + growl "1.10.5" + he "1.2.0" + js-yaml "3.13.1" + log-symbols "2.2.0" + minimatch "3.0.4" + mkdirp "0.5.4" + ms "2.1.1" + node-environment-flags "1.0.5" + object.assign "4.1.0" + strip-json-comments "2.0.1" + supports-color "6.0.0" + which "1.3.1" + wide-align "1.1.3" + yargs "13.3.2" + yargs-parser "13.1.2" + yargs-unparser "1.6.0" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" + integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +multicast-dns-service-types@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz#899f11d9686e5e05cb91b35d5f0e63b773cfc901" + integrity sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE= + +multicast-dns@^7.2.0: + version "7.2.2" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.2.tgz#5731da04f47d1e435ac457e5ac7b4b39d866a5a1" + integrity sha512-XqSMeO8EWV/nOXOpPV8ztIpNweVfE1dSpz6SQvDPp71HD74lMXjt4m/mWB1YBMG0kHtOodxRWc5WOb/UNN1A5g== + dependencies: + dns-packet "^4.0.0" + thunky "^1.0.2" + +node-environment-flags@1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/node-environment-flags/-/node-environment-flags-1.0.5.tgz#fa930275f5bf5dae188d6192b24b4c8bbac3d76a" + integrity sha512-VNYPRfGfmZLx0Ye20jWzHUjyTW/c+6Wq+iLhDzUI4XmhrDd9l/FozXV3F2xOaXjvp0co0+v1YSR3CMP6g+VvLQ== + dependencies: + object.getownpropertydescriptors "^2.0.3" + semver "^5.7.0" + +node-persist@^0.0.11: + version "0.0.11" + resolved "https://registry.yarnpkg.com/node-persist/-/node-persist-0.0.11.tgz#d66eba3ebef620f079530fa7b13076a906665874" + integrity sha1-1m66Pr72IPB5Uw+nsTB2qQZmWHQ= + dependencies: + mkdirp "~0.5.1" + q "~1.1.1" + +oauth-sign@~0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" + integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ== + +object-inspect@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.7.0.tgz#f4f6bd181ad77f006b5ece60bd0b6f398ff74a67" + integrity sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw== + +object-is@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.0.2.tgz#6b80eb84fe451498f65007982f035a5b445edec4" + integrity sha512-Epah+btZd5wrrfjkJZq1AOB9O6OxUQto45hzFd7lXGrpHPGE0W1k+426yrZV+k6NJOzLNNW/nVsmZdIWsAqoOQ== + +object-keys@^1.0.11, object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@4.1.0, object.assign@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.0.tgz#968bf1100d7956bb3ca086f006f846b3bc4008da" + integrity sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w== + dependencies: + define-properties "^1.1.2" + function-bind "^1.1.1" + has-symbols "^1.0.0" + object-keys "^1.0.11" + +object.getownpropertydescriptors@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz#369bf1f9592d8ab89d712dced5cb81c7c5352649" + integrity sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +p-limit@^2.0.0: + version "2.2.2" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.2.tgz#61279b67721f5287aa1c13a9a7fbbc48c9291b1e" + integrity sha512-WGR+xHecKTr7EbUEhyLSh5Dube9JtdiG78ufaeLxTgpudf/20KqyMioIUZJAezlTIi6evxuoUs9YXc11cU+yzQ== + dependencies: + p-try "^2.0.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +pathval@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.0.tgz#b942e6d4bde653005ef6b71361def8727d0645e0" + integrity sha1-uULm1L3mUwBe9rcTYd74cn0GReA= + +performance-now@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" + integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns= + +psl@^1.1.28: + version "1.8.0" + resolved "https://registry.yarnpkg.com/psl/-/psl-1.8.0.tgz#9326f8bcfb013adcc005fdff056acce020e51c24" + integrity sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ== + +punycode@^2.1.0, punycode@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +q@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/q/-/q-1.1.2.tgz#6357e291206701d99f197ab84e57e8ad196f2a89" + integrity sha1-Y1fikSBnAdmfGXq4TlforRlvKok= + +qs@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" + integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== + +querystring@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620" + integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA= + +regexp.prototype.flags@^1.2.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz#7aba89b3c13a64509dabcf3ca8d9fbb9bdf5cb75" + integrity sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.17.0-next.1" + +request-promise-core@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/request-promise-core/-/request-promise-core-1.1.3.tgz#e9a3c081b51380dfea677336061fea879a829ee9" + integrity sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ== + dependencies: + lodash "^4.17.15" + +request-promise@^4.2.5: + version "4.2.5" + resolved "https://registry.yarnpkg.com/request-promise/-/request-promise-4.2.5.tgz#186222c59ae512f3497dfe4d75a9c8461bd0053c" + integrity sha512-ZgnepCykFdmpq86fKGwqntyTiUrHycALuGggpyCZwMvGaZWgxW6yagT0FHkgo5LzYvOaCNvxYwWYIjevSH1EDg== + dependencies: + bluebird "^3.5.0" + request-promise-core "1.1.3" + stealthy-require "^1.1.1" + tough-cookie "^2.3.3" + +request@^2.88.0: + version "2.88.2" + resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" + integrity sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw== + dependencies: + 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.3" + 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.5.0" + tunnel-agent "^0.6.0" + uuid "^3.3.2" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@^5.1.2: + version "5.2.0" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" + integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== + +safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +semver@^5.7.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +sshpk@^1.7.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.16.1.tgz#fb661c0bef29b39db40769ee39fa70093d6f6877" + integrity sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg== + dependencies: + 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" + +stealthy-require@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" + integrity sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks= + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trimleft@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz#9bdb8ac6abd6d602b17a4ed321870d2f8dcefc74" + integrity sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz#440314b15996c866ce8a0341894d45186200c5d9" + integrity sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-json-comments@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= + +supports-color@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.0.0.tgz#76cfe742cf1f41bb9b1c29ad03068c05b4c0e40a" + integrity sha512-on9Kwidc1IUQo+bQdhi8+Tijpo0e1SS6RoGo2guUwn5vdaxw8RXOF9Vb2ws+ihWOmh4JnCJOvaziZWP1VABaLg== + dependencies: + has-flag "^3.0.0" + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +tough-cookie@^2.3.3, tough-cookie@~2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.5.0.tgz#cd9fb2a0aa1d5a12b473bd9fb96fa3dcff65ade2" + integrity sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g== + dependencies: + psl "^1.1.28" + punycode "^2.1.1" + +tunnel-agent@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd" + integrity sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0= + dependencies: + safe-buffer "^5.0.1" + +tweetnacl@^0.14.3, tweetnacl@~0.14.0: + version "0.14.5" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" + integrity sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q= + +tweetnacl@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + +type-detect@^4.0.0, type-detect@^4.0.5: + version "4.0.8" + resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" + integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== + +typescript@^3.8.3: + version "3.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" + integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +uuid@^3.3.2: + version "3.4.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" + integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== + +verror@1.10.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" + integrity sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA= + dependencies: + assert-plus "^1.0.0" + core-util-is "1.0.2" + extsprintf "^1.2.0" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wide-align@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + integrity sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA== + dependencies: + string-width "^1.0.2 || 2" + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yargs-parser@13.1.2, yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs-unparser@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" + integrity sha512-W9tKgmSn0DpSatfri0nx52Joq5hVXgeLiqR/5G0sZNDoLZFOr/xjBUDcShCOGNsBnEMNo1KAMBkTej1Hm62HTw== + dependencies: + flat "^4.1.0" + lodash "^4.17.15" + yargs "^13.3.0" + +yargs@13.3.2, yargs@^13.3.0: + version "13.3.2" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2"