diff --git a/.gitignore b/.gitignore index a2070c6..6dbf473 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -devices/ +old/ test/ config.json diff --git a/README.md b/README.md index 9769aaa..2a3883a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ # tuya-mqtt -MQTT interface for Tuya home automation devices sold under various names. -This is a wrapper script for the Project codetheweb/tuyapi. https://github.com/codetheweb/tuyapi +This project provides a method for locally controlling IOT devices manufactured by Tuya Inc., and sold under many different brands, via MQTT. -This project provides an MQTT gateway for locally controlling home automation devices made by Tuya Inc. To use this script you will need to obtain the device ID and local keys for each of your devices after they are configured via the Tuya/Smart Life or other Tuya compatible app (there are many). With this information it is possible to communicate locally with Tuya devices using protocol 3.1 and 3.3, without using the Tuya Cloud service, however, getting the keys requires signing up for a Tuya IOT developer account or using one of several other alternative methods (such as dumping the memory of a Tuya based app running on Andriod). Acquiring keys is not part of this project, please see the instructions at the TuyAPI project (on which this script is based) available at the TuyAPI project site: +Using this script requires obtaining the device ID and local keys for each of your devices after they are configured via the Tuya/Smart Life or other Tuya compatible app (there are many). With this information it is possible to communicate locally with Tuya devices using Tuya protocol version 3.1 and 3.3 without using the Tuya Cloud service, however, getting the keys requires signing up for a Tuya IOT developer account or using one of several other alternative methods (such as dumping the memory of a Tuya based app running on Andriod). -https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md. +To acquire keys for your device please see the instructions at the TuyAPI project (on which this script is based) available at the [TuyAPI GitHub site](https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md). -## Instructions: +**Acquiring device keys is outside the scope of this project!** Issues opened regarding acquiring keys will likely be closed without comment. Please verify that your device can be queried and controlled via tuya-cli before opening any issue. If your device can't be controlled by tuya-cli then it cannot be used with this project. + +**!!!!!!!!!! Important information regarding the 3.0 release !!!!!!!!!!**\ +The 3.0.0 release (Oct 17th, 2020) is a major refactor of the tuya-mqtt project and, as such, is a breaking release for all users of previous versions. Almost everything about the project is different, including configuration method, topic names, etc. Upgrading users should carefully read the instructions below and assume they are starting over from scratch. + +## Installation Download this project to your system into any directory (example below uses /opt/tuya-mqtt) and install tuyapi from the same folder that the tuya-mqtt.js is in ``` // switch to opt directory @@ -22,93 +26,101 @@ cd tuya-mqtt npm install ``` +## Configuration +Tuya-mqtt has two different configuration files. The first is config.json, a simple file which contains settings for connection to the MQTT broker. The second is devices.conf, a JSON5 formatted file which defines the Tuya devices that the script should connect to and expose via MQTT. This file uses the same basic format as the "tuya-cli wizard" outputs when used to acquire the device keys, so it can be used as the basis for your tuya-mqtt device configuration. -## Basic Usage -### Create your configuration file: +### Seting up config.json: ``` cp config.json.sample config.json - -// edit the configuration file +``` +Edit config.json with your MQTT broker settings and save: +``` nano config.json ``` -### Start command +### Setting up devices.conf: +If you use the "tuya-cli wizard" method to acquire your device keys you can leverage the output of this tool as the start of your devices.conf file. Otherwise, you want to create a file using a formate like this: ``` -node tuya-mqtt.js - -// For debugging purpose, to use DEBUG : https://www.npmjs.com/package/debug - -//on Linux machines at the bash command prompt, to turn ON DEBUG: -DEBUG=* tuya-mqtt.js - -//on Linux machines at the bash command prompt, to turn OFF DEBUG: -DEBUG=-* tuya-mqtt.js +[ + { + name: 'Tuya Device 1', + id: '86435357d8b123456789', + key: '8b2a69c9876543210' + }, + { + name: 'Tuya Device 2', + id: 'eb532eea7d12345678abc', + key: '899810012345678' + } +] +``` +Note that, because the format is JSON5, which is a superset of JSON, you can use standard, strict JSON syntax, or the more forgiving JSON5 format, or even mix and match in the same file. -// on Windows machines at the cmd.exe command prompt, to turn ON DEBUG: -Set DEBUG=* & node c:/openhab2/userdata/etc/scripts/tuya-mqtt.js +While the above syntax is enough to create a working tuya-mqtt install with generic devices, the full power and simplicity of tuya-mqtt 3.0 is only unlocked by configuring device types to get . Please see the full [DEVICES](docs/DEVICES.md) documenation for details. -// on Windows machines at the cmd.exe command prompt, to turn OFF DEBUG: -Set DEBUG=-* & node c:/openhab2/userdata/etc/scripts/tuya-mqtt.js +### Starting tuya-mqtt ``` - -### MQTT Topic's (send data) -**It's possible to replace the device IP address \ with the word "discover" to have the API attempt to automatically discover the device IP address. This allows support for 3.3 protocol devices transparently, without additional configuraiton, but does require the system running this script to be on the same IP subnet as the Tuya device since the discovery protocol relies on UDP broadcast packets from the devices.** +node tuya-mqtt.js ``` - tuya///discover/state - tuya///discover/command +To enable debugging output (required when opening an issue): ``` -**If discovery will not work for your case you can still use the IP address, but, to use protocol 3.3 you must specify it in the topic explicitly** +DEBUG=tuya-mqtt:* tuya-mqtt.js ``` - tuya/ver3.3//////command + +### Usage Overview +Tuya devices work by mapping device functions to various values stored in data points (refered to as DPS values) which are referenced via an index number, referred to as the DPS key. For example, a simple on/off switch may have a single DPS value, stored in DPS kep 1 (DPS1). This value is likely to have a setting of true/false representing the on/off state of the device. The device state can be read via DPS1, and, for values that can be changed (some DPS values are read-only), sending true/false to DPS1 will turn the device on/off. A simple dimmer might have the same DPS1 value, but an additional DPS2 value from 1-255 representing the state of the dimmer. More complex devices use more DPS keys with various values representing the states and control functions of the device. + +The tuya-mqtt script provides access to these DPS keys and their values via MQTT, allowing any tool that can use MQTT to monitor and control these devices via a local network connection. In addition to providing access to the raw DPS data, there is also a template engine that allows those DPS values to be mapped to device specific topics, called "friendly topics", allowing for consistent mapping even between devices that use different DPS keys for the same functions. These friendly topics also support various transforms and other functions that make it easier for other devices to communicate with Tuya devices without a detailed understanding of the data formats Tuya devices use. + +### MQTT Topic Overview +The top level topics are created using the device name or ID as the primary identifier. If the device name is available, it will be converted to lowercase and any spaces replace with underscores('_') characters so, for example, if the device as the name "Kitche Table", the top level topic would be: ``` -### Example command topic to set the device state: +tuya/kitchen_table/ ``` - tuya////command +If the device name was not available in the devices.conf file, tuya-mqtt falls back to using the device ID for the top level topic: ``` -### Example MQTT message payload for basic commands (default controls DPS[1] value, assumes true/false state control): +tuya/86435357d8b123456789/ ``` - "ON" - "OFF" - "on" - "off" - "1" - "0" - "toggle" - "TOGGLE" +All additional state/command topics are then built below this level. You can view the connectivity status of the device using the status topic, which reports online/offline based on whether tuya-mqtt has an active connection to the device or not. The script monitors both the device socket connection for errors and also device heartbeats, to report proper status. ``` -### Example MQTT message payload for advanced commands (set any DPS value): +tuya/kitche_table/state --> online/offline ``` - "{ \"dps\": 1, \"set\": true }" - "{ \"dps\": 7, \"set\": true }" - "{ \"multiple\": true, \"data\": { \"1\": true, \"7\": true } }" - "{ \"schema\": true }" - "{ \"multiple\": true, \"data\": { \"1\": true, \"2\": \"scene_4\" } }" - "{ \"multiple\": true, \"data\": { \"1\": true, \"2\": \"scene\", \"6\": \"c479000025ffc3\" } }" +You can also trigger the device to send an immediate update of all known device DPS topics by sending the message "get-states" to the command topic (this topic exist for all devices): ``` -### Example command topic for color change of lightbulb +tuya/kitche_table/command <-- get-states ``` - tuya////color +As noted above, tuya-mqtt supports two distinct topic types for interfacing with and controlling devices. For all devices, the DPS topics are always published and commands are accepted, however, friendly topics are the generally recommended approach but require you to use a pre-defined device template or create a customer template for your device when using the generic device. - Example MQTT message payload: - 64,0,100 - 0,0,89 -``` +If you do create a template for your device, please feel free to share it with the community as adding additional pre-defined devices is desired for future versions of tuya-mqtt. There is a templates section of the project that you can submit a PR for your templates. -### Example state topics (get device data) -### Get current device state (always DPS[1] value): - tuya////state +If you would like to use the raw DPS topics, please jump to the [DPS topics](#dps-topics) section of this document. -### Get all available device DPS values -Returns JSON.stringify(dps) values, use with care, does not always contain all dps values +## Friendly Topics +Friendly topics are only available when using a pre-defined device template or, for the generic device, when you have defined a custom template for your device. Friendly topics use the tuyq-mqtt templating engine to map raw Tuya DPS key values to easy to consume topics and transform the data where needed. + +Another advantage of friendly topics is that not all devices respond to schema requets (i.e. a request to report all DPS topics the device uses). Because of this, it's not always possible for tuya-mqtt to know which DPS topics to acquire state information from during initial startup. With a defined template the required DPS keys for each friendly topic are configured and tuya-mqtt will always query these DPS key values during initial connection to the device and report their state appropriately. + +For more details on using friendly topics, please read the [DEVICES](docs/DEVICES.md) documentation which discusses how to configure supported devices or define a custom template. + +## DPS Topics +Controlling devices directly via DPS topics requires enough knowledge of the device to know which topics accept what values. Described below are two differnt methods for interfacing with DPS values, the JSON DPS topic, and the individual DPS key topics. + +### DPS JSON topic +The JSON DPS topic allows controlling Tuya devices by sending Tuya native style JSON messages to the command topic, and by monitoring for Tuya style JSON replies on the state topic. You can get more details on this format by reading the [TuyAPI documentaiton](https://codetheweb.github.io/tuyapi/index.html), but, for example, to turn off a dimmer switch you could issue a MQTT message containing the JSON value ```{dps: 1, set: false}``` to the DPS/command topic for the device. If you wanted to turn the dimmer on, and set brightness to 50%, you could issue separate messages ```{dps: 1, set: true}``` and then ```{dps: 2, set: 128}```, or, the Tuya JSON protocol also allows setting multiple values in a single set command using the format ```{'multiple': true, 'data': {'1': true, '2': 128}}```. JSON state and commands should use the DPS/state and DPS/command topics respectively. Below is an example of the topics: ``` - tuya////dps +tuya/dimmer_device/DPS/state +tuya/dimmer_device/DPS/command ``` - -### Get any single DPS data value +### DPS Key topics +In addition to the JSON DPS topic, it's also possible to use the DPS key topics. DPS key topics allow you to monitor and send simple bool/number/string values directly to DPS keys without having to use the Tuya JSON format, the conversion to Tuya JSON is handled by tuya-mqtt. Using the example from above, turning on the dimmer and setting brightness to 50% you would simply issue the message "true" to DPS/1/command and the message "128" to DPS/2/command. ``` - tuya////dps/ +tuya/dimmer_device/DPS/1/state --> true/false for on/off state +tuya/dimmer_device/DPS/2/command <-- 1-255 for brightness state +tuya/dimmer_device/DPS/1/state --> accept true/false for turning device on/off +tuya/dimmer_device/DPS/2/command <-- accepts 1-255 for controlling brightness level ``` +**!!! Important Note !!!**\ +When sending commands directly to DPS values there are no limitation on what values are sent as tuya-mqtt has no way to know what are valid vs invalid for any given DPS key. Sending values that are out-of-range or of different types than the DPS key expects can cause unpredicatable behavior of your device, from causing timeouts, to reboots, to hanging the device. While I've never seen a device fail to recover after a restart, please keep this in mind when sending commands to your device. ## Issues Not all Tuya protocols are supported. For example, some devices use protocol 3.2 which currently remains unsupported by the TuyAPI project due to lack of enough information to reverse engineer the protcol. If you are unable to control your devices with tuya-mqtt please verify that you can query and control them with tuya-cli first. If tuya-cli works, then this script should also work, if it doesn't then this script will not work either. diff --git a/devices/generic-device.js b/devices/generic-device.js new file mode 100644 index 0000000..a4f974e --- /dev/null +++ b/devices/generic-device.js @@ -0,0 +1,29 @@ +const TuyaDevice = require('./tuya-device') +const debug = require('debug')('tuya-mqtt:device') +const utils = require('../lib/utils') + +class GenericDevice extends TuyaDevice { + async init() { + this.deviceData.mdl = 'Generic Device' + + // Check if custom template in device config + if (this.config.hasOwnProperty('template')) { + // Map generic DPS topics to device specific topic names + this.deviceTopics = this.config.template + } else { + // Try to get schema to at least know what DPS keys to get initial update + const result = await this.device.get({"schema": true}) + if (!utils.isJsonString(result)) { + if (result === 'Schema for device not available') { + debug('Device id '+this.config.id+' failed schema discovery and no custom template defined') + debug('Cannot get initial DPS state data for device '+this.options.name+' but data updates will be publish') + } + } + } + + // Get initial states and start publishing topics + this.getStates() + } +} + +module.exports = GenericDevice \ No newline at end of file diff --git a/devices/rgbtw-light.js b/devices/rgbtw-light.js new file mode 100644 index 0000000..53db969 --- /dev/null +++ b/devices/rgbtw-light.js @@ -0,0 +1,175 @@ +const TuyaDevice = require('./tuya-device') +const debug = require('debug')('tuya-mqtt:device-detect') +const debugDiscovery = require('debug')('tuya-mqtt:discovery') +const utils = require('../lib/utils') + +class RGBTWLight extends TuyaDevice { + async init() { + // If no manual config try to detect device settings + if (!this.config.dpsPower) { + await this.guessLightInfo() + } + + // If detection failed and no manual config return without initializing + if (!this.guess.dpsPower && !this.config.dpsPower) { + debug('Automatic discovery of Tuya bulb settings failed and no manual configuration') + return + } + + // Set device specific variables + this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : this.guess.dpsPower + this.config.dpsMode = this.config.dpsMode ? this.config.dpsMode : this.guess.dpsMode + this.config.dpsWhiteValue = this.config.dpsWhiteValue ? this.config.dpsWhiteValue : this.guess.dpsWhiteValue + this.config.whiteValueScale = this.config.whiteValueScale ? this.config.whiteValueScale : this.guess.whiteValueScale + this.config.dpsColorTemp = this.config.dpsColorTemp ? this.config.dpsColorTemp : this.guess.dpsColorTemp + this.config.minColorTemp = this.config.minColorTemp ? this.config.minColorTemp : 154 // ~6500K + this.config.maxColorTemp = this.config.maxColorTemp ? this.config.maxColorTemp : 400 // ~2500K + this.config.colorTempScale = this.config.colorTempScale ? this.config.colorTempScale : this.guess.colorTempScale + this.config.dpsColor = this.config.dpsColor ? this.config.dpsColor : this.guess.dpsColor + this.config.colorType = this.config.colorType ? this.config.colorType : this.guess.colorType + + this.deviceData.mdl = 'RGBTW Light' + this.isRgbtwLight = true + + // Set white value transform math + let whiteValueStateMath + let whiteValueCommandMath + if (this.config.whiteValueScale === 255) { + // Devices with brightness scale of 255 seem to not allow values + // less then 25 (10%) without producing timeout errors. + whiteValueStateMath = '/2.3-10.86' + whiteValueCommandMath = '*2.3+25' + } else { + // For other scale (usually 1000), 10-1000 seems OK. + whiteValueStateMath = '/('+this.config.whiteValueScale+'/100)' + whiteValueCommandMath = '*('+this.config.whiteValueScale+'/100)' + } + + // Map generic DPS topics to device specific topic names + this.deviceTopics = { + state: { + key: this.config.dpsPower, + type: 'bool' + }, + white_brightness_state: { + key: this.config.dpsWhiteValue, + type: 'int', + topicMin: 0, + topicMax: 100, + stateMath: whiteValueStateMath, + commandMath: whiteValueCommandMath + }, + hs_state: { + key: this.config.dpsColor, + type: this.config.colorType, + components: 'h,s' + }, + color_brightness_state: { + key: this.config.dpsColor, + type: this.config.colorType, + components: 'b' + }, + hsb_state: { + key: this.config.dpsColor, + type: this.config.colorType, + components: 'h,s,b' + }, + mode_state: { + key: this.config.dpsMode, + type: 'str' + } + } + + // If device supports Color Temperature add color temp device topic + if (this.config.dpsColorTemp) { + // Values used for tranforming from 1-255 scale to mireds range + const rangeFactor = (this.config.maxColorTemp-this.config.minColorTemp)/100 + const scaleFactor = this.config.colorTempScale/100 + const tuyaMaxColorTemp = this.config.maxColorTemp/rangeFactor*scaleFactor + + this.deviceTopics.color_temp_state = { + key: this.config.dpsColorTemp, + type: 'int', + topicMin: this.config.minColorTemp, + topicMax: this.config.maxColorTemp, + stateMath: '/'+scaleFactor+'*-'+rangeFactor+'+'+this.config.maxColorTemp, + commandMath: '/'+rangeFactor+'*-'+scaleFactor+'+'+tuyaMaxColorTemp + } + } + + // Send home assistant discovery data and give it a second before sending state updates + this.initDiscovery() + await utils.sleep(1) + + // Get initial states and start publishing topics + this.getStates() + } + + initDiscovery() { + const configTopic = 'homeassistant/light/'+this.config.id+'/config' + + const discoveryData = { + name: (this.config.name) ? this.config.name : this.config.id, + state_topic: this.baseTopic+'state', + command_topic: this.baseTopic+'command', + brightness_state_topic: this.baseTopic+'color_brightness_state', + brightness_command_topic: this.baseTopic+'color_brightness_command', + brightness_scale: 100, + hs_state_topic: this.baseTopic+'hs_state', + hs_command_topic: this.baseTopic+'hs_command', + white_value_state_topic: this.baseTopic+'white_brightness_state', + white_value_command_topic: this.baseTopic+'white_brightness_command', + white_value_scale: 100, + availability_topic: this.baseTopic+'status', + payload_available: 'online', + payload_not_available: 'offline', + unique_id: this.config.id, + device: this.deviceData + } + + if (this.config.dpsColorTemp) { + discoveryData.color_temp_state_topic = this.baseTopic+'color_temp_state' + discoveryData.color_temp_command_topic = this.baseTopic+'color_temp_command' + discoveryData.min_mireds = this.config.minColorTemp + discoveryData.max_mireds = this.config.maxColorTemp + } + + debugDiscovery('Home Assistant config topic: '+configTopic) + debugDiscovery(discoveryData) + this.publishMqtt(configTopic, JSON.stringify(discoveryData)) + } + + async guessLightInfo() { + this.guess = new Object() + debug('Attempting to detect light capabilites and DPS values...') + debug('Querying DPS 2 for white/color mode setting...') + + // Check if DPS 2 contains typical values for RGBTW light + const mode2 = await this.device.get({"dps": 2}) + const mode21 = await this.device.get({"dps": 21}) + if (mode2 && (mode2 === 'white' || mode2 === 'colour' || mode2.toString().includes('scene'))) { + debug('Detected probably Tuya color bulb at DPS 1-5, checking more details...') + this.guess = {'dpsPower': 1, 'dpsMode': 2, 'dpsWhiteValue': 3, 'whiteValueScale': 255, 'dpsColorTemp': 4, 'colorTempScale': 255, 'dpsColor': 5} + } else if (mode21 && (mode21 === 'white' || mode21 === 'colour' || mode21.toString().includes('scene'))) { + debug('Detected likely Tuya color bulb at DPS 20-24, checking more details...') + this.guess = {'dpsPower': 20, 'dpsMode': 21, 'dpsWhiteValue': 22, 'whiteValueScale': 1000, 'dpsColorTemp': 23, 'colorTempScale': 1000, 'dpsColor': 24} + } + + if (this.guess.dpsPower) { + debug('Attempting to detect if bulb supports color temperature...') + const colorTemp = await this.device.get({"dps": this.guess.dpsColorTemp}) + if (colorTemp !== '' && colorTemp >= 0 && colorTemp <= this.guess.colorTempScale) { + debug('Detected likely color temerature support') + } else { + debug('No color temperature support detected') + this.guess.dpsColorTemp = 0 + } + debug('Attempting to detect Tuya color format used by device...') + const color = await this.device.get({"dps": this.guess.dpsColor}) + this.guess.colorType = (color && color.length === 12) ? 'hsb' : 'hsbhex' + debug ('Detected Tuya color format '+this.guess.colorType.toUpperCase()) + } + } +} + +module.exports = RGBTWLight \ No newline at end of file diff --git a/devices/simple-dimmer.js b/devices/simple-dimmer.js new file mode 100644 index 0000000..5cbcdd9 --- /dev/null +++ b/devices/simple-dimmer.js @@ -0,0 +1,76 @@ +const TuyaDevice = require('./tuya-device') +const debug = require('debug')('tuya-mqtt:device') +const debugDiscovery = require('debug')('tuya-mqtt:discovery') +const utils = require('../lib/utils') + +class SimpleDimmer extends TuyaDevice { + async init() { + // Set device specific variables + this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : 1 + this.config.dpsBrightness = this.config.dpsBrightness ? this.config.dpsBrightness : 2 + this.config.brightnessScale = this.config.brightnessScale ? this.config.brightnessScale : 255 + + this.deviceData.mdl = 'Dimmer Switch' + + // Set white value transform math + let brightnessStateMath + let brightnessCommandMath + if (this.config.brightnessScale === 255) { + // Devices with brightness scale of 255 seem to not allow values + // less then 25 (10%) without producing timeout errors. + brightnessStateMath = '/2.3-10.86' + brightnessCommandMath = '*2.3+25' + } else { + // For other scale (usually 1000), 10-1000 seems OK. + brightnessStateMath = '/('+this.config.brightnessScale+'/100)' + brightnessCommandMath = '*('+this.config.brightnessScale+'/100)' + } + + // Map generic DPS topics to device specific topic names + this.deviceTopics = { + state: { + key: this.config.dpsPower, + type: 'bool' + }, + brightness_state: { + key: this.config.dpsBrightness, + type: 'int', + topicMin: 0, + topicMax: 100, + stateMath: brightnessStateMath, + commandMath: brightnessCommandMath + } + } + + // Send home assistant discovery data and give it a second before sending state updates + this.initDiscovery() + await utils.sleep(1) + + // Get initial states and start publishing topics + this.getStates() + } + + initDiscovery() { + const configTopic = 'homeassistant/light/'+this.config.id+'/config' + + const discoveryData = { + name: (this.config.name) ? this.config.name : this.config.id, + state_topic: this.baseTopic+'state', + command_topic: this.baseTopic+'command', + brightness_state_topic: this.baseTopic+'brightness_state', + brightness_command_topic: this.baseTopic+'brightness_command', + brightness_scale: 100, + availability_topic: this.baseTopic+'status', + payload_available: 'online', + payload_not_available: 'offline', + unique_id: this.config.id, + device: this.deviceData + } + + debugDiscovery('Home Assistant config topic: '+configTopic) + debugDiscovery(discoveryData) + this.publishMqtt(configTopic, JSON.stringify(discoveryData)) + } +} + +module.exports = SimpleDimmer \ No newline at end of file diff --git a/devices/simple-switch.js b/devices/simple-switch.js new file mode 100644 index 0000000..ef8337e --- /dev/null +++ b/devices/simple-switch.js @@ -0,0 +1,49 @@ +const TuyaDevice = require('./tuya-device') +const debug = require('debug')('tuya-mqtt:device') +const debugDiscovery = require('debug')('tuya-mqtt:discovery') +const utils = require('../lib/utils') + +class SimpleSwitch extends TuyaDevice { + async init() { + // Set device specific variables + this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : 1 + + this.deviceData.mdl = 'Switch/Socket' + + // Map generic DPS topics to device specific topic names + this.deviceTopics = { + state: { + key: this.config.dpsPower, + type: 'bool' + } + } + + // Send home assistant discovery data and give it a second before sending state updates + this.initDiscovery() + await utils.sleep(1) + + // Get initial states and start publishing topics + this.getStates() + } + + initDiscovery() { + const configTopic = 'homeassistant/switch/'+this.config.id+'/config' + + const discoveryData = { + name: (this.config.name) ? this.config.name : this.config.id, + state_topic: this.baseTopic+'state', + command_topic: this.baseTopic+'command', + availability_topic: this.baseTopic+'status', + payload_available: 'online', + payload_not_available: 'offline', + unique_id: this.config.id, + device: this.deviceData + } + + debugDiscovery('Home Assistant config topic: '+configTopic) + debugDiscovery(discoveryData) + this.publishMqtt(configTopic, JSON.stringify(discoveryData)) + } +} + +module.exports = SimpleSwitch \ No newline at end of file diff --git a/devices/tuya-device.js b/devices/tuya-device.js new file mode 100644 index 0000000..c3a9d93 --- /dev/null +++ b/devices/tuya-device.js @@ -0,0 +1,630 @@ +const TuyAPI = require('tuyapi') +const { evaluate } = require('mathjs') +const utils = require('../lib/utils') +const debug = require('debug')('tuya-mqtt:tuyapi') +const debugState = require('debug')('tuya-mqtt:state') +const debugCommand = require('debug')('tuya-mqtt:command') +const debugError = require('debug')('tuya-mqtt:error') + +class TuyaDevice { + constructor(deviceInfo) { + this.config = deviceInfo.configDevice + this.mqttClient = deviceInfo.mqttClient + this.topic = deviceInfo.topic + + // Build TuyAPI device options from device config info + this.options = { + id: this.config.id, + key: this.config.key + } + if (this.config.name) { this.options.name = this.config.name.toLowerCase().replace(/ /g,'_') } + if (this.config.ip) { + this.options.ip = this.config.ip + if (this.config.version) { + this.options.version = this.config.version + } else { + this.options.version = '3.1' + } + } + + // Set default device data for Home Assistant device registry + // Values may be overridden by individual devices + this.deviceData = { + ids: [ this.config.id ], + name: (this.config.name) ? this.config.name : this.config.id, + mf: 'Tuya' + } + + // Initialize properties to hold cached device state data + this.dps = {} + this.color = {'h': 0, 's': 0, 'b': 0} + + // Device friendly topics + this.deviceTopics = {} + + // Missed heartbeat monitor + this.heartbeatsMissed = 0 + + // Build the MQTT topic for this device (friendly name or device id) + if (this.options.name) { + this.baseTopic = this.topic + this.options.name + '/' + } else { + this.baseTopic = this.topic + this.options.id + '/' + } + + // Create the new Tuya Device + this.device = new TuyAPI(JSON.parse(JSON.stringify(this.options))) + + // Listen for device data and call update DPS function if valid + this.device.on('data', (data) => { + if (typeof data === 'object') { + debug('Received JSON data from device '+this.options.id+' ->', JSON.stringify(data.dps)) + this.updateState(data) + } else { + if (data !== 'json obj data unvalid') { + debug('Received string data from device '+this.options.id+' ->', data.replace(/[^a-zA-Z0-9 ]/g, '')) + } + } + }) + + // Attempt to find/connect to device and start heartbeat monitor + this.connectDevice() + this.monitorHeartbeat() + + // On connect perform device specific init + this.device.on('connected', async () => { + // Sometimes TuyAPI reports connection even on socket error + // Wait one second to check if device is really connected before initializing + await utils.sleep(1) + if (this.device.isConnected()) { + debug('Connected to device ' + this.toString()) + this.heartbeatsMissed = 0 + this.publishMqtt(this.baseTopic+'status', 'online') + this.init() + } + }) + + // On disconnect perform device specific disconnect + this.device.on('disconnected', () => { + this.connected = false + this.publishMqtt(this.baseTopic+'status', 'offline') + debug('Disconnected from device ' + this.toString()) + }) + + // On connect error call reconnect + this.device.on('error', async (err) => { + debugError(err) + await utils.sleep(1) + if (!this.device.isConnected()) { + this.reconnect() + } + }) + + // On heartbeat reset heartbeat timer + this.device.on('heartbeat', () => { + this.heartbeatsMissed = 0 + }) + } + + // Get and update cached values of all configured/known dps value for device + async getStates() { + // Suppress topic updates while syncing device state with cached state + this.connected = false + for (let topic in this.deviceTopics) { + const key = this.deviceTopics[topic].key + try { + const result = await this.device.get({"dps": key}) + this.dps[key].val = result + this.dps[key].updated = true + } catch { + debugError('Could not get value for device DPS key '+key) + } + } + this.connected = true + // Force topic update now that all states are fully syncronized + this.publishTopics() + } + + // Update cached DPS values on data updates + updateState(data) { + if (typeof data.dps != 'undefined') { + // Update cached device state data + for (let key in data.dps) { + // Only update if the received value is different from previous value + if (this.dps[key] !== data.dps[key]) { + this.dps[key] = { + 'val': data.dps[key], + 'updated': true + } + } + if (this.isRgbtwLight) { + if (this.config.hasOwnProperty('dpsColor') && this.config.dpsColor == key) { + this.updateColorState(data.dps[key]) + } else if (this.config.hasOwnProperty('dpsMode') && this.config.dpsMode == key) { + // If color/white mode is changing, force sending color state + // Allows overriding saturation value to 0% for white mode for the HSB device topics + this.dps[this.config.dpsColor].updated = true + } + } + } + if (this.connected) { + this.publishTopics() + } + } + } + + // Publish device specific state topics + publishTopics() { + // Don't publish if device is not connected + if (!this.connected) return + + // Loop through and publish all device specific topics + for (let topic in this.deviceTopics) { + const deviceTopic = this.deviceTopics[topic] + const key = deviceTopic.key + // Only publish values if different from previous value + if (this.dps[key] && this.dps[key].updated) { + const state = this.getTopicState(deviceTopic, this.dps[key].val) + if (state) { + this.publishMqtt(this.baseTopic + topic, state, true) + } + } + } + + // Publish Generic Dps Topics + this.publishDpsTopics() + } + + // Publish all dps-values to topic + publishDpsTopics() { + try { + if (!Object.keys(this.dps).length) { return } + + const dpsTopic = this.baseTopic + 'dps' + // Publish DPS JSON data if not empty + let data = {} + for (let key in this.dps) { + // Only publish values if different from previous value + if (this.dps[key].updated) { + data[key] = this.dps[key].val + } + } + data = JSON.stringify(data) + const dpsStateTopic = dpsTopic + '/state' + debugState('MQTT DPS JSON: ' + dpsStateTopic + ' -> ', data) + this.publishMqtt(dpsStateTopic, data, false) + + // Publish dps/<#>/state value for each device DPS + for (let key in this.dps) { + // Only publish values if different from previous value + if (this.dps[key].updated) { + const dpsKeyTopic = dpsTopic + '/' + key + '/state' + const data = this.dps.hasOwnProperty(key) ? this.dps[key].val.toString() : 'None' + debugState('MQTT DPS'+key+': '+dpsKeyTopic+' -> ', data) + this.publishMqtt(dpsKeyTopic, data, false) + this.dps[key].updated = false + } + } + } catch (e) { + debugError(e); + } + } + + // Get the friendly topic state based on configured DPS value type + getTopicState(deviceTopic, value) { + let state + switch (deviceTopic.type) { + case 'bool': + state = value ? 'ON' : 'OFF' + break; + case 'int': + case 'float': + state = this.parseNumberState(value, deviceTopic) + break; + case 'hsb': + case 'hsbhex': + // Return comma separate array of component values for specific topic + state = new Array() + const components = deviceTopic.components.split(',') + for (let i in components) { + // If light is in white mode always report saturation 0%, otherwise report actual value + state.push((components[i] === 's' && this.dps[this.config.dpsMode].val === 'white') ? 0 : this.color[components[i]]) + } + state = (state.join(',')) + break; + case 'str': + state = value ? value : '' + break; + } + return state + } + + // Parse the received state numeric value based on deviceTopic rules + parseNumberState(value, deviceTopic) { + // Check if it's a number and it's not outside of defined range + if (isNaN(value)) { + return '' + } + + // Perform any required math transforms before returing command value + switch (deviceTopic.type) { + case 'int': + value = (deviceTopic.stateMath) ? parseInt(Math.round(evaluate(value+deviceTopic.stateMath))) : value = parseInt(value) + break; + case 'float': + value = (deviceTopic.stateMath) ? parseFloat(evaluate(value+deviceTopic.stateMath)) : value = parseFloat(value) + break; + } + + return value.toString() + } + + // Initial processing of MQTT commands for all command topics + processCommand(message, commandTopic) { + let command + if (utils.isJsonString(message)) { + debugCommand('Received MQTT command message is a JSON string') + command = JSON.parse(message); + } else { + debugCommand('Received MQTT command message is a text string') + command = message.toLowerCase() + } + + // If get-states command, then updates all states and re-publish topics + if (commandTopic === 'command' && command === 'get-states') { + // Handle "get-states" command to update device state + debugCommand('Received command: ', command) + this.getStates() + } else { + // Call device specific command topic handler + this.processDeviceCommand(command, commandTopic) + } + } + + // Process MQTT commands for all device command topics + processDeviceCommand(command, commandTopic) { + // Determine state topic from command topic to find proper template + const stateTopic = commandTopic.replace('command', 'state') + const deviceTopic = this.deviceTopics.hasOwnProperty(stateTopic) ? this.deviceTopics[stateTopic] : '' + + if (deviceTopic) { + debugCommand('Device '+this.options.id+' received command topic: '+commandTopic+', message: '+command) + let commandResult = this.sendTuyaCommand(command, deviceTopic) + if (!commandResult) { + debugCommand('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command) + } + } else { + debugCommand('Invalid command topic '+this.baseTopic+commandTopic+' for device id: '+this.config.id) + return + } + } + + // Process Tuya JSON commands via DPS command topic + processDpsCommand(message) { + if (utils.isJsonString(message)) { + const command = JSON.parse(message) + debugCommand('Parsed Tuya JSON command: '+JSON.stringify(command)) + this.set(command) + } else { + debugCommand('DPS command topic requires Tuya style JSON value') + } + } + + // Process text based Tuya commands via DPS key command topics + processDpsKeyCommand(message, dpsKey) { + if (utils.isJsonString(message)) { + debugCommand('Individual DPS command topics do not accept JSON values') + } else { + const dpsMessage = this.parseDpsMessage(message) + debugCommand('Received command for DPS'+dpsKey+': ', message) + const command = { + dps: dpsKey, + set: dpsMessage + } + this.set(command) + } + } + + // Parse string message into boolean and number types + parseDpsMessage(message) { + if (typeof message === 'boolean' ) { + return message; + } else if (message === 'true' || message === 'false') { + return (message === 'true') ? true : false + } else if (!isNaN(message)) { + return Number(message) + } else { + return message + } + } + + // Set state based on command topic + sendTuyaCommand(message, deviceTopic) { + let command = message.toLowerCase() + const tuyaCommand = new Object() + tuyaCommand.dps = deviceTopic.key + switch (deviceTopic.type) { + case 'bool': + if (command === 'toggle') { + tuyaCommand.set = !this.dps[tuyaCommand.dps].val + } else { + command = this.parseBoolCommand(command) + if (typeof command.set === 'boolean') { + tuyaCommand.set = command.set + } else { + tuyaCommand.set = '!!!INVALID!!!' + } + } + break; + case 'int': + case 'float': + tuyaCommand.set = this.parseNumberCommand(command, deviceTopic) + break; + case 'hsb': + this.updateCommandColor(command, deviceTopic.components) + tuyaCommand.set = this.parseTuyaHsbColor() + break; + case 'hsbhex': + this.updateCommandColor(command, deviceTopic.components) + tuyaCommand.set = this.parseTuyaHsbHexColor() + break; + default: + // If type is not one of the above just use the raw string as is + tuyaCommand.set = message + } + if (tuyaCommand.set === '!!!INVALID!!!') { + return false + } else { + if (this.isRgbtwLight) { + this.setLight(deviceTopic, tuyaCommand) + } else { + this.set(tuyaCommand) + } + return true + } + } + + // Convert simple bool commands to true/false + parseBoolCommand(command) { + switch(command) { + case 'on': + case 'off': + case '0': + case '1': + case 'true': + case 'false': + return { + set: (command === 'on' || command === '1' || command === 'true' || command === 1) ? true : false + } + default: + return command + } + } + + // Validate/transform set interger values + parseNumberCommand(command, deviceTopic) { + let value = undefined + const invalid = '!!!INVALID!!!' + + // Check if it's a number and it's not outside of defined range + if (isNaN(command)) { + return invalid + } else if (deviceTopic.hasOwnProperty('topicMin') && command < deviceTopic.topicMin) { + debugError('Received command value "'+command+'" that is less than the configured minimum value') + debugError('Overriding command with minimum value '+deviceTopic.topicMin) + command = deviceTopic.topicMin + } else if (deviceTopic.hasOwnProperty('topicMax') && command > deviceTopic.topicMax) { + debugError('Received command value "'+command+'" that is greater than the configured maximum value') + debugError('Overriding command with maximum value: '+deviceTopic.topicMax) + command = deviceTopic.topicMax + } + + // Perform any required math transforms before returing command value + switch (deviceTopic.type) { + case 'int': + if (deviceTopic.commandMath) { + value = parseInt(Math.round(evaluate(command+deviceTopic.commandMath))) + } else { + value = parseInt(command) + } + break; + case 'float': + if (deviceTopic.commandMath) { + value = parseFloat(evaluate(command+deviceTopic.commandMath)) + } else { + value = parseFloat(command) + } + break; + } + + return value + } + + // Takes Tuya color value in HSB or HSBHEX format and updates cached HSB color state for device + // Credit homebridge-tuya project for HSB/HSBHEX conversion code + updateColorState(value) { + let h, s, b + if (this.config.colorType === 'hsbhex') { + [, h, s, b] = (value || '0000000000ffff').match(/^.{6}([0-9a-f]{4})([0-9a-f]{2})([0-9a-f]{2})$/i) || [0, '0', 'ff', 'ff']; + this.color.h = parseInt(h, 16) + this.color.s = Math.round(parseInt(s, 16) / 2.55) // Convert saturation to 100 scale + this.color.b = Math.round(parseInt(b, 16) / 2.55) // Convert brightness to 100 scale + } else { + [, h, s, b] = (value || '000003e803e8').match(/^([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})$/i) || [0, '0', '3e8', '3e8'] + // Convert from Hex to Decimal and cache values + this.color.h = parseInt(h, 16) + this.color.s = Math.round(parseInt(s, 16) / 10) // Convert saturation to 100 Scale + this.color.b = Math.round(parseInt(b, 16) / 10) // Convert brightness to 100 scale + } + + // Initialize the command color values with existing color state + if (!this.hasOwnProperty('cmdColor')) { + this.cmdColor = { + 'h': this.color.h, + 's': this.color.s, + 'b': this.color.b + } + } + } + + // Caches color updates when HSB components have separate device topics + // cmdColor property always contains the desired HSB color state based on received + // command topic messages vs actual device color state, which may be pending + updateCommandColor(value, components) { + // Update any HSB component with a changed value + components = components.split(',') + const values = value.split(',') + for (let i in components) { + this.cmdColor[components[i]] = Math.round(values[i]) + } + } + + // Returns Tuya HSB format value from current cmdColor HSB values + // Credit homebridge-tuya project for HSB conversion code + parseTuyaHsbColor() { + let {h, s, b} = this.cmdColor + const hexColor = h.toString(16).padStart(4, '0') + (10 * s).toString(16).padStart(4, '0') + (10 * b).toString(16).padStart(4, '0') + return hexColor + } + + // Returns Tuya HSBHEX format value from current cmdColor HSB values + // Credit homebridge-tuya project for HSBHEX conversion code + parseTuyaHsbHexColor() { + let {h, s, b} = this.cmdColor + const hsb = h.toString(16).padStart(4, '0') + Math.round(2.55 * s).toString(16).padStart(2, '0') + Math.round(2.55 * b).toString(16).padStart(2, '0'); + h /= 60; + s /= 100; + b *= 2.55; + const + i = Math.floor(h), + f = h - i, + p = b * (1 - s), + q = b * (1 - s * f), + t = b * (1 - s * (1 - f)), + rgb = (() => { + switch (i % 6) { + case 0: + return [b, t, p]; + case 1: + return [q, b, p]; + case 2: + return [p, b, t]; + case 3: + return [p, q, b]; + case 4: + return [t, p, b]; + case 5: + return [b, p, q]; + } + })().map(c => Math.round(c).toString(16).padStart(2, '0')), + hex = rgb.join(''); + + return hex + hsb; + } + + // Set white/colour mode based on received commands + async setLight(topic, command) { + let targetMode = undefined + + if (topic.key === this.config.dpsWhiteValue || topic.key === this.config.dpsColorTemp) { + // If setting white level or color temperature, light should be in white mode + targetMode = 'white' + } else if (topic.key === this.config.dpsColor) { + // Split device topic HSB components into array + const components = topic.components.split(',') + + // If device topic inlucdes saturation check for changes + if (components.includes('s')) { + if (this.cmdColor.s < 10) { + // Saturation changed to < 10% = white mode + targetMode = 'white' + } else { + // Saturation changed to >= 10% = color mode + targetMode = 'colour' + } + } else { + // For other cases stay in existing mode + targetMode = this.dps[this.config.dpsMode].val + } + } + + // Send the issued command + this.set(command) + + // Make sure the bulb stays in the correct mode + if (targetMode) { + command = { + dps: this.config.dpsMode, + set: targetMode + } + this.set(command) + } + } + + // Simple function to help debug output + toString() { + return this.config.name+' (' +(this.options.ip ? this.options.ip+', ' : '')+this.options.id+', '+this.options.key+')' + } + + set(command) { + debug('Set device '+this.options.id+' -> '+JSON.stringify(command)) + return new Promise((resolve, reject) => { + this.device.set(command).then((result) => { + resolve(result) + }) + }) + } + + // Search for and connect to device + connectDevice() { + // Find device on network + debug('Search for device id '+this.options.id) + this.device.find().then(() => { + debug('Found device id '+this.options.id) + // Attempt connection to device + this.device.connect().catch((error) => { + debugError(error.message) + this.reconnect() + }) + }).catch(async (error) => { + debugError(error.message) + debugError('Will attempt to find device again in 60 seconds') + await utils.sleep(60) + this.connectDevice() + }) + } + + // Retry connection every 10 seconds if unable to connect + async reconnect() { + debugError('Error connecting to device id '+this.options.id+'...retry in 10 seconds.') + await utils.sleep(10) + if (this.connected) { return } + this.connectDevice() + } + + // Simple function to monitor heartbeats to determine if + monitorHeartbeat() { + setInterval(async () => { + if (this.connected) { + if (this.heartbeatsMissed > 3) { + debugError('Device id '+this.options.id+' not responding to heartbeats...disconnecting') + this.device.disconnect() + await utils.sleep(1) + this.connectDevice() + } else if (this.heartbeatsMissed > 0) { + const errMessage = this.heartbeatsMissed > 1 ? " consecutive heartbeats" : " heartbeat" + debugError('Device id '+this.options.id+' has missed '+this.heartbeatsMissed+errMessage) + } + this.heartbeatsMissed++ + } + }, 10000) + } + + // Publish MQTT + publishMqtt(topic, message, isDebug) { + if (isDebug) { debugState(topic, message) } + this.mqttClient.publish(topic, message, { qos: 1 }); + } +} + +module.exports = TuyaDevice \ No newline at end of file diff --git a/docs/DEVICES.md b/docs/DEVICES.md new file mode 100644 index 0000000..0acdb53 --- /dev/null +++ b/docs/DEVICES.md @@ -0,0 +1,213 @@ +# tuya-mqtt - Devices +The most powerful feature in tuya-mqtt is the ability to configure devices to use friendly topics. For some devices there exist pre-defined device templates which makes using those devices quite easy, simply add the type information to the devices.conf file and tuya-mqtt automatically creates friendly topics for that device. + +Friendly topics make it easy to communicate with the device in a standard way and thus integrating into various Home Automation platforms. The topic style generally follows that used by the Home Assistant MQTT integration components and the pre-defined devices automatically send Home Assistant style MQTT discovery messages during startup to make integration with Home Assistant, or other platforms which understand Home Assistant MQTT discovery, even easier. + +If the device does not have a pre-defined device template, it's possible to create a template using the [generic device template](#generic-device-templates) feature. + +## Pre-defined Device Templates +Pre-defined device templates (except for the Generic Device) will always expose friendly topics for the given device in a consistent manner. Currently the following pre-defined device templates are available: + +| Device Type | Descrition | +| --- | --- | +| SimpleSwitch | Supports simple on/off devices | +| SimpleDimmer | Supports simple devices with on/on and brightness | +| RGBTWLight | Supports color/white lights with optional color temerature support | +| GenericDevice | Allows defining a custom template for any device | + +To use a device template, simply add the "type" option to the devices.conf similar to the following example: +``` +[ + { + name: 'Tuya Device 1', + id: '86435357d8b123456789', + key: '8b2a69c9876543210', + type: 'RGBTWLight' + } +] +``` +Once the device type is defined tuya-mqtt will attempt to create friendly topics for that device type on connection to the device. Each device type defines specific defaults for DPS values which are typical for common Tuya devices and some, like RGBTWLight, have logic to attempt to detect different variation by querying the device. The goal is that, in most cases, simply adding the type is all that is needed, however, in many cases it is also possible to override the manual settings for the device. The device friendly topics and options for each device are documented below. + +### SimpleSwitch +Simple devices that support only on/off. +| Topic | Description | Values | +| --- | --- | --- | +| state | Power state | on/off | +| command | Set power state | on/off, 0/1, true/false | + +Manual configuration options: +| Option | Description | Default | +| --- | --- | --- | +| dpsPower | DPS key for power state | 1 | + +### SimpleDimmer +Simple device with on/off and brightness functions (dimmer switches or lights) +| Topic | Description | Values | +| --- | --- | --- | +| state | Power state | on/off | +| command | Set power state | on/off, 0/1, true/false | +| brightness_state | Brightness in % | 0-100 | +| brightness_command | set brightness in % | 0-100 | + +Manual configuration options: +| Option | Description | Default | +| --- | --- | --- | +| dpsPower | DPS key for power state | 1 | +| dpsBrightness | DPS key for brightness state | 2 | +| brightnessScale | Scale for brightness DPS value | 255 | + +### RGBTWLight +The RGBTWLight device support Tuya color lights (bulbs and LEDs). Tuya lights operate in either white or color mode. The RGBTWLight device automatically switches between modes on certain conditions as documented below: +| Condition | Mode | +| --- | --- | +| Changes white brightness | white | +| Changes to color temperature (for device with color temp support) | white | +| Saturation < 10 % | white | +| Saturation >= 10 % | color | +| All other changes | current mode | + +This means changing the hue of the light will only switch to color mode if saturation is also >= 10%. Some lights automatically attempt to switch to color mode when any HSB value is updated however, if the saturation setting remains < 10%, tuya-mqtt will force the light back to white mode in this case. This can cause a very quick flicker when chaning hue or color brightness while the saturation remains below the 10% threshold. I expect this not to be a common issue and implemented this in an attempt to make all tuya lights behave in a consistent way. + +When the bulb is in white mode, saturation values in the friendly topics are always reported as 0%. This is true even if the mode is toggled manually from color to white mode using the mode_command topic or the Tuya/SmartLife app. When the light is toggled back to color mode, saturation will be reported at the correct level. This is done primarly as a means to indicate color state to automation platforms that don't have a concept of white/color mode, otherwise a light in white mode may still be represented with a color icon in the platforms UI. + +Not all devices support color temperature and the script attempts to detect this capability and enables the color temperature topics only when found. Color temperature topics report in Mireds (commonly used by automation tools) and the default range supports roughly 2500K-6500K. This works reasonably well for most available Tuya devices, even if they are not exactly in this range, but, if you know a devices specific color range, the limits can be manually specified to more accurately reflect the exact color temperature. + +Tuya bulbs store their HSB color value in a single DPS key using a custom format. Some bulbs use a 14 character format, referred to as HSBHEX, which represents the saturation and brightness values from 0-255 as 2 character hex, while the others use a 12 character format, referred to as HSB, which still uses hex values, but stores saturation and brightness values from 0-1000 as 4 character hex. The code attempts to autodetect the format used by the bulb and perform the proper conversion in all cases, but this can be overridden for cases where the dection method fails. + +| Topic | Description | Values | +| --- | --- | --- | +| state | Power state | on/off | +| command | Set power state | on/off, 0/1, true/false | +| white_brightness_state | White mode brightness in % | 0-100 | +| white_brightness_command | Set white mode brightness in % | 0-100 | +| color_brightness_state | Color mode brightness in % | 0-100 | +| color_brightness_command | Set white mode brightness in % | 0-100 | +| hs_state | Hue, saturation % | H,S (Hue 0-360, Saturation 0-100) | +| hs_command | Set hue, saturation % | H,S (Hue 0-360, Saturation 0-100) | +| hsb_state | Hue, saturation %, brightness % | H,S,B (Hue 0-360, Saturation 0-100, Brightness 0-100) | +| hsb_command | Set hue, saturation %, brightness % | H,S,B (Hue 0-360, Saturation 0-100, Brightness 0-100) | +| mode_state | White/Color mode | 'white', 'colour' (some devices also support scenes here) | +| mode_command | Set white/color mode | 'white', 'colour' (some devices also support scenes here) | +| color_temp_state | Color temperature in mireds (only available if device support color temp) | 154-400 (defult range, can be overridden) | +| color_temp_command | Set color temperature in mireds (only available if device support color temp) | 154-400 (defult range, can be overridden) | + +Manual configuration options: +| Option | Description | Default (common detected values) | +| --- | --- | --- | +| dpsPower | DPS key for power state | Auto Detect (1,20) | +| dpsMode | DPS key for white/color mode state | Auto Detect (2,21) | +| dpsWhiteValue | DPS key for white mode brightness | Auto Detect (3,22) | +| whiteValueScale | White mode brightness DPS scale | Auto Detect (255, 1000) | +| dpsColorTemp | DPS key for color temperature | Auto Detect (4,23) | +| minColorTemp | Min color temperature in Mireds | 154 (~6500K) | +| maxColorTemp | Max color temperature in Mireds | 400 (~2500K) | +| colorTempScale | Color temperature DPS key scale | Auto Detect (255, 1000) | +| dpsColor | DPS key for HSB color values | Auto Detect (5,24) | +| colorType | Tuya color format for color DPS key | Auto Detect (hsb, hsbhex) | + +To use the manual configuration options simply add them to device.conf file after defining the device type like the following example: +``` +[ + { + name: 'Tuya Device 1', + id: '86435357d8b123456789', + key: '8b2a69c9876543210', + type: 'RGBTWLight', + dpsPower: 31, + dpsMode: 32, + dpsWhiteValue: 33, + whiteValueScale: 255, + dpsColorTemp: 34, + minColorTemp: 165, + maxColorTemp: 385, + colorTempScale: 255, + dpsColor: 34, + colorType: 'hsbhex' + } +] +``` + +## Generic Device Templates +If a pre-defined device tempate does not exist for the device, or does not expose all capabilities of the device, there are still mulitple options available to control the devices. One method is to use the DPS topics directly to control the device using either native Tuya JSON commands or via the DPS key values by using the DPS key topics (see [DPS Topics](TOPICS.md#dps-topics)). The second method is to create a template for your device to map DPS key values to friendly topics. The GenericDevice type allows you to manually create a template for any device using the same templating engine as the pre-defined device templates. Once you've created a tempalte for your device, it can be re-used with other, similar devices and you can submit your template to the tuya-mqtt project for other to use, or even for inclusion at a pre-defined device template in the future. + +Creating a device template is relatively straightforward, but first you must know what DPS keys your devices uses. The GenericDevice attempts to query all device DPS states on startup, but some devices to not respond to this command, however, the generic device will ALWAYS report any DPS topics from which it receives upated. The easiest way to determine how your device uses it's DPS topics is to connect to the MQTT broker via a tool like MQTT Explorer or mosquitto_sub, and watch the topics as you manipulate the device with the Tuya/Smartlife app. + +Once you have a reasonable idea of how the device uses it's DPS key values, you can create a template. A simple template for a dimmer looks something like this: +``` +[ + { + name: 'Tuya Device 1', + id: '86435357d8b123456789', + key: '8b2a69c9876543210', + template: { + state: { + key: 1, + type: 'bool' + }, + brightness_state: { + key: 2, + type: 'int', + topicMin: 1, + topicMax: 100, + stateMath: '/2.55', + commandMath: '*2.55' + } + } + } +] +``` +The template above defines two topics "state" and "brightness_state", and the template engine automatically creates the corresponding command topics, in this case specifically "command" and "brightness_command". + +The "state" topic maps to DPS key 1, and uses a bool (true/false) value in the DPS key. Now you will be able to see "on/off" state in the state topic instead of having to read the true/false value from the DPS/1 topic + +The the "brightness_state" topic maps to DPS key 2, and this value defines the brightness using an integer in the 1-255 scale. We define the value as an integer (type: 'int') and the stateMath and commandMath values allow transforming the raw DPS value into a more friendly value that will be presented in the topic. In this case the raw DPS value will be divided by 2.55 before being published to the state, and and received commands will be mulitpled by that same value, converting the 1-255 to a simple 1-100 scale. Note that the topicMin and topicMax values set the minimum and maximum values that the state topic will report and that the command topic will accept. These values are "post-math" for state topics, and "pre-math" for command topics. + +The following tables define the available template value types and their options: + +### Boolean values +| option | value | +| --- | --- | +| type | 'bool' | +| key | DPS key of the value | + +### Integer values +| option | value | +| --- | --- | +| type | 'int' | +| key | DPS key of the value | +| topicMin | Minumum value allowed for the command topic | +| topicMax | Maximum value allowed for the command topic | +| stateMath | Simple math applied to the DPS key value before being published to state topic | +| commandMath | Simple math applied to command value before being set to DPS key | + +### Floating point values +| option | value | +| --- | --- | +| type | 'float' | +| key | DPS key of the value | +| topicMin | Minumum value allowed for the command topic | +| topicMax | Maximum value allowed for the command topic | +| stateMath | Simple math applied to the DPS key value before being published to state topic | +| commandMath | Simple math applied to command value before being set to DPS key | + +### String values +| option | value | +| --- | --- | +| type | 'string' | +| key | DPS key of the value | + +### Tuya HSB values (newer style Tuya, 12 character color value) +| option | value | +| --- | --- | +| type | 'hsb' | +| key | DPS key of the value | +| components | Comma separated list of HSB components that should be included in this topic | + +### Tuya HSBHEX values (older style Tuya 14 character color value) +| option | value | +| --- | --- | +| type | 'hsbhex' | +| key | DPS key of the value | +| components | Comma separated list of HSB components that should be included in this topic | + +Using these value types you can define templates for a wide range of devices. Additional types and options are likely to be included in future versions of tuya-mqtt. \ No newline at end of file diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..e80e21b --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,28 @@ +class Utils +{ + + // Check if data is JSON or not + isJsonString(data) { + try { + const parsedData = JSON.parse(data) + if (parsedData && typeof parsedData === "object") { + return parsedData + } + } + catch (e) { } + + return false + } + + // Simple sleep function for various required delays + sleep(sec) { + return new Promise(res => setTimeout(res, sec*1000)) + } + + msSleep(ms) { + return new Promise(res => setTimeout(res, ms)) + } + +} + +module.exports = new Utils() \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9a4d42f..075cabb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,265 @@ { "name": "tuya-mqtt", - "version": "2.1.0", + "version": "3.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { + "@sindresorhus/is": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-2.1.1.tgz", + "integrity": "sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg==" + }, + "@szmarczak/http-timer": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.5.tgz", + "integrity": "sha512-PyRA9sm1Yayuj5OIoJ1hGt2YISX45w9WcFbh6ddT0Z/0yaFxOtGLInr4jUfU1EAFVs0Yfyfev4RNwBlUaHdlDQ==", + "requires": { + "defer-to-connect": "^2.0.0" + } + }, + "@tuyapi/cli": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/@tuyapi/cli/-/cli-1.13.4.tgz", + "integrity": "sha512-EiqeqqiAY2kHokd5uELbu2IYOarxPd3rNP3xgCPUypcH3UoN5gnHdrRB9iwgiDfbEL89v2rn48aPEjQX9NyQ0Q==", + "requires": { + "@tuyapi/link": "^0.3.3", + "@tuyapi/openapi": "^1.2.0", + "@tuyapi/stub": "^0.1.4", + "cli-table3": "^0.6.0", + "colors": "^1.4.0", + "commander": "^5.1.0", + "configstore": "^5.0.1", + "debug": "^4.1.1", + "finalhandler": "^1.1.2", + "http-mitm-proxy": "^0.8.2", + "inquirer": "^7.2.0", + "keypress": "^0.2.1", + "ora": "^4.0.4", + "promise.any": "^2.0.1", + "qrcode-terminal": "^0.12.0", + "serve-static": "^1.14.1", + "tuyapi": "^5.3.1", + "update-notifier": "^4.1.0" + }, + "dependencies": { + "tuyapi": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/tuyapi/-/tuyapi-5.3.2.tgz", + "integrity": "sha512-RKdTTnWVK+DDq3iRUTMh5DVd8coIwoulHntB+HvcDLuakDgSoNUc0Pzd69mw0CTTP7HTC6x6S9Ztg5pJIlYE8g==", + "requires": { + "debug": "4.1.1", + "p-retry": "4.2.0", + "p-timeout": "3.2.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + } + } + } + } + }, + "@tuyapi/link": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@tuyapi/link/-/link-0.3.3.tgz", + "integrity": "sha512-YWsMLJxqGe+kma188Lm9F/u8mZYQ9X0T//stzfPagbx5CcPAGVIvs0nmaYFr189Tu5dDc8xs/cFc2zAlj63kow==", + "requires": { + "@tuyapi/openapi": "^0.2.0", + "debug": "^4.1.1", + "delay": "^4.3.0" + }, + "dependencies": { + "@tuyapi/openapi": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@tuyapi/openapi/-/openapi-0.2.0.tgz", + "integrity": "sha512-SqwZ0hAquAVA8V6D7tjNgn8ji8oyMaguh766K3tiXxSSULLa5Q3TkrWhd1t2mFMINL+QKAvNpU2hzGVFmyyjig==", + "requires": { + "got": "^10.2.2" + } + } + } + }, + "@tuyapi/openapi": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@tuyapi/openapi/-/openapi-1.2.0.tgz", + "integrity": "sha512-ABS6F4H1UAneyso+3ttS4TiwKChRnT4r9aiYwMvvIKqym13cgJm54xylFn10gwFoFu17kfNm1D5fh0wxzgrumA==", + "requires": { + "got": "^10.2.2" + } + }, + "@tuyapi/stub": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@tuyapi/stub/-/stub-0.1.4.tgz", + "integrity": "sha512-uF2u1Z78Qif3hK92RYxMG4AbjedNRZYmM1jIvegf4KUfDVV/BhHFooz2y3Q28jPl5Q+H39QY/WBH2bMiXMAg/g==", + "requires": { + "debug": "^4.1.1", + "tuyapi": "^5.0.0" + }, + "dependencies": { + "tuyapi": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/tuyapi/-/tuyapi-5.3.2.tgz", + "integrity": "sha512-RKdTTnWVK+DDq3iRUTMh5DVd8coIwoulHntB+HvcDLuakDgSoNUc0Pzd69mw0CTTP7HTC6x6S9Ztg5pJIlYE8g==", + "requires": { + "debug": "4.1.1", + "p-retry": "4.2.0", + "p-timeout": "3.2.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + } + } + } + } + }, + "@types/cacheable-request": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", + "integrity": "sha512-ykFq2zmBGOCbpIXtoVbz4SKY5QriWPh3AjyU4G74RYbtt5yOc5OfaY75ftjg7mikMOla1CTGpX3lLbuJh8DTrQ==", + "requires": { + "@types/http-cache-semantics": "*", + "@types/keyv": "*", + "@types/node": "*", + "@types/responselike": "*" + } + }, + "@types/http-cache-semantics": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.0.tgz", + "integrity": "sha512-c3Xy026kOF7QOTn00hbIllV1dLR9hG9NkSrLQgCVs8NF6sBU+VGWjD3wLPhmh1TYAc7ugCFsvHYMN4VcBN1U1A==" + }, + "@types/keyv": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.1.tgz", + "integrity": "sha512-MPtoySlAZQ37VoLaPcTHCu1RWJ4llDkULYZIzOYxlhxBqYPB0RsRlmMU0R6tahtFe27mIdkHV+551ZWV4PLmVw==", + "requires": { + "@types/node": "*" + } + }, + "@types/node": { + "version": "14.11.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.11.8.tgz", + "integrity": "sha512-KPcKqKm5UKDkaYPTuXSx8wEP7vE9GnuaXIZKijwRYcePpZFDVuy2a57LarFKiORbHOuTOOwYzxVxcUzsh2P2Pw==" + }, + "@types/responselike": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", + "integrity": "sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA==", + "requires": { + "@types/node": "*" + } + }, "@types/retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==" }, + "ansi-align": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", + "integrity": "sha512-ZpClVKqXN3RGBmKibdfWzqCY4lnjEuoNzU5T0oEFpfd/z5qJHVarukridD4juLO2FXMiwUQxr9WqQtaYa8XRYw==", + "requires": { + "string-width": "^3.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", + "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + }, + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "requires": { + "ansi-regex": "^4.1.0" + } + } + } + }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==" + } + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "array.prototype.map": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.2.tgz", + "integrity": "sha512-Az3OYxgsa1g7xDYp86l0nnN4bcmuEITGe1rbdEBVkrqkzMgDcbdQ2R7r41pNzti+4NMces3H8gMmuioZUilLgw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "es-array-method-boxes-properly": "^1.0.0", + "is-string": "^1.0.4" + } + }, + "async": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", + "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "requires": { + "lodash": "^4.17.14" + } + }, + "async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -41,6 +292,37 @@ } } }, + "boxen": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", + "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", + "requires": { + "ansi-align": "^3.0.0", + "camelcase": "^5.3.1", + "chalk": "^3.0.0", + "cli-boxes": "^2.2.0", + "string-width": "^4.1.0", + "term-size": "^2.1.0", + "type-fest": "^0.8.1", + "widest-line": "^3.1.0" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==" + } + } + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -64,6 +346,29 @@ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, + "cacheable-lookup": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-2.0.1.tgz", + "integrity": "sha512-EMMbsiOTcdngM/K6gV/OxF2x0t07+vMOWxZNSCRQMjO2MY2nhZQ6OYhOOpyQrbhqsgtvKGI7hcq6xjnA92USjg==", + "requires": { + "@types/keyv": "^3.1.1", + "keyv": "^4.0.0" + } + }, + "cacheable-request": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.1.tgz", + "integrity": "sha512-lt0mJ6YAnsrBErpTMWeu5kl/tg9xMAWjavYTN6VQXM1A/teBITuNcccXsCxF0tDQQJf9DfAaX5O4e0zp0KlfZw==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^2.0.0" + } + }, "callback-stream": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/callback-stream/-/callback-stream-1.1.0.tgz", @@ -73,6 +378,83 @@ "readable-stream": "> 1.0.0 < 3.0.0" } }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "cli-boxes": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", + "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==" + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.4.0.tgz", + "integrity": "sha512-sJAofoarcm76ZGpuooaO0eDy8saEy+YoZBLjC4h8srt4jeBnkYeOgqxgsJQTpyt2LjI5PTfLJHSL+41Yu4fEJA==" + }, + "cli-table3": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.0.tgz", + "integrity": "sha512-gnB85c3MGC7Nm9I/FkiasNBOKjOiO1RNuXXarQms37q4QMpWdlbBgD/VnOStA2faG1dpXMv31RFApjX1/QdgWQ==", + "requires": { + "colors": "^1.1.2", + "object-assign": "^4.1.0", + "string-width": "^4.2.0" + } + }, + "cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==" + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, + "clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "requires": { + "mimic-response": "^1.0.0" + }, + "dependencies": { + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -86,6 +468,16 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==" + }, "commist": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", @@ -95,6 +487,11 @@ "minimist": "^1.1.0" } }, + "complex.js": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.11.tgz", + "integrity": "sha512-6IArJLApNtdg1P1dFtn3dnyzoZBEF0MwMnrfF1exSBRpZYoy4yieMkpZhQDC0uwctw48vii0CFVyHfpgZ/DfGw==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -111,11 +508,29 @@ "typedarray": "^0.0.6" } }, + "configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", + "requires": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" + } + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, + "crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==" + }, "d": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", @@ -126,13 +541,80 @@ } }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "requires": { + "ms": "2.1.2" + } + }, + "decimal.js": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", + "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==" + }, + "decompress-response": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-5.0.0.tgz", + "integrity": "sha512-TLZWWybuxWgoW7Lykv+gq9xvzOsUjQ9tF09Tj6NSTYGMTCHNXzrPnD6Hi+TgZq19PyTAGH4Ll/NIM/eTGglnMw==", + "requires": { + "mimic-response": "^2.0.0" + } + }, + "deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" + }, + "defaults": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", + "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "requires": { + "clone": "^1.0.2" + } + }, + "defer-to-connect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.0.tgz", + "integrity": "sha512-bYL2d05vOSf1JEZNx5vSAtPuBMkX8K9EUutg7zlKvTqKXHt7RhWJFbmd7qakVuf13i+IkGmp6FwSsONOf6VYIg==" + }, + "define-properties": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", + "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", + "requires": { + "object-keys": "^1.0.12" + } + }, + "delay": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/delay/-/delay-4.4.0.tgz", + "integrity": "sha512-txgOrJu3OdtOfTiEOT2e76dJVfG/1dz2NZ4F0Pyt4UGZJryssMRp5vdM5wQoLwSOBNdrJv3F9PAhp/heqd7vrA==" + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, + "dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "requires": { - "ms": "^2.1.1" + "is-obj": "^2.0.0" } }, + "duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=" + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -144,6 +626,21 @@ "stream-shift": "^1.0.0" } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -152,6 +649,65 @@ "once": "^1.4.0" } }, + "es-abstract": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", + "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "es-aggregate-error": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/es-aggregate-error/-/es-aggregate-error-1.0.4.tgz", + "integrity": "sha512-syuWJHsRD5TJ3nwqjf8eFEeGLJM6OxUjVFz0dMg2b/GB/Ub5VAFiQPEVB6ewdU2VHgkOJBo00uYwPmo7fyfzEg==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.6", + "function-bind": "^1.1.1", + "functions-have-names": "^1.2.1", + "globalthis": "^1.0.1" + } + }, + "es-array-method-boxes-properly": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", + "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==" + }, + "es-get-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.0.tgz", + "integrity": "sha512-UfrmHuWQlNMTs35e1ypnvikg6jCz3SK8v8ImvmDsh36fCVUR1MqoFDiyn0/k52C8NqO3YsO8Oe0azeesNuqSsQ==", + "requires": { + "es-abstract": "^1.17.4", + "has-symbols": "^1.0.1", + "is-arguments": "^1.0.4", + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, "es5-ext": { "version": "0.10.53", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", @@ -217,6 +773,31 @@ "ext": "^1.1.2" } }, + "escape-goat": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", + "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, + "escape-latex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz", + "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==" + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" + }, "event-emitter": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", @@ -226,6 +807,11 @@ "es5-ext": "~0.10.14" } }, + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, "ext": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", @@ -246,11 +832,86 @@ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "fraction.js": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.12.tgz", + "integrity": "sha512-8Z1K0VTG4hzYY7kA/1sj4/r1/RWLBD3xwReT/RCrUCbzPszjNQCCsy3ktkU/eaEqX3MYa4pY37a52eiBlPMlhA==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functions-have-names": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.1.tgz", + "integrity": "sha512-j48B/ZI7VKs3sgeI2cZp7WXWmZXu7Iq5pl5/vptV5N2mq+DGFuS/ulaDjtaoLpYzuD6u8UgrUKHfgo7fDTSiBA==" + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -290,6 +951,72 @@ "unique-stream": "^2.0.2" } }, + "global-dirs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.0.1.tgz", + "integrity": "sha512-5HqUqdhkEovj2Of/ms3IeS/EekcO54ytHRLV4PEY2rhRwrHXLQjeVEES0Lhka0xwNDtGYn58wyC4s5+MHsOO6A==", + "requires": { + "ini": "^1.3.5" + } + }, + "globalthis": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.1.tgz", + "integrity": "sha512-mJPRTc/P39NH/iNG4mXa9aIhNymaQikTrnspeCa2ZuJ+mH2QN/rXwtX3XwKrHqWgUQFbNZKtHM105aHzJalElw==", + "requires": { + "define-properties": "^1.1.3" + } + }, + "got": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/got/-/got-10.7.0.tgz", + "integrity": "sha512-aWTDeNw9g+XqEZNcTjMMZSy7B7yE9toWOFYip7ofFTLleJhvZwUxxTxkTpKvF+p1SAA4VHmuEy7PiHTHyq8tJg==", + "requires": { + "@sindresorhus/is": "^2.0.0", + "@szmarczak/http-timer": "^4.0.0", + "@types/cacheable-request": "^6.0.1", + "cacheable-lookup": "^2.0.0", + "cacheable-request": "^7.0.1", + "decompress-response": "^5.0.0", + "duplexer3": "^0.1.4", + "get-stream": "^5.0.0", + "lowercase-keys": "^2.0.0", + "mimic-response": "^2.1.0", + "p-cancelable": "^2.0.0", + "p-event": "^4.0.0", + "responselike": "^2.0.0", + "to-readable-stream": "^2.0.0", + "type-fest": "^0.10.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-symbols": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" + }, + "has-yarn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", + "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==" + }, "help-me": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz", @@ -301,11 +1028,60 @@ "xtend": "^4.0.0" } }, + "http-cache-semantics": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", + "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==" + }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, + "http-mitm-proxy": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/http-mitm-proxy/-/http-mitm-proxy-0.8.2.tgz", + "integrity": "sha512-QqaqHWssz4acqu2aIPJqJWt/gDa4SzQ9kj/rs16ONA2nBWNh/mfOW0Ez1Wxa5IivHHZSTciQ7wG0Dxzogurngw==", + "requires": { + "async": "^2.6.2", + "debug": "^4.1.0", + "mkdirp": "^0.5.1", + "node-forge": "^0.8.4", + "optimist": "^0.6.1", + "semaphore": "^1.1.0", + "ws": "^3.2.0" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, "ieee754": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, + "import-lazy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", + "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM=" + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -320,6 +1096,31 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" + }, + "inquirer": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", + "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.19", + "mute-stream": "0.0.8", + "run-async": "^2.4.0", + "rxjs": "^6.6.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6" + } + }, "is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", @@ -329,11 +1130,39 @@ "is-windows": "^1.0.1" } }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "requires": { + "ci-info": "^2.0.0" + } + }, + "is-date-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" + }, "is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, "is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", @@ -342,18 +1171,88 @@ "is-extglob": "^2.1.0" } }, + "is-installed-globally": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", + "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", + "requires": { + "global-dirs": "^2.0.1", + "is-path-inside": "^3.0.1" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" + }, + "is-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz", + "integrity": "sha512-T/S49scO8plUiAOA2DBTBG3JHpn1yiw0kRp6dgiZ0v2/6twi5eiB0rHtHFH9ZIrvlWc6+4O+m4zg5+Z833aXgw==" + }, "is-negated-glob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-negated-glob/-/is-negated-glob-1.0.0.tgz", "integrity": "sha1-aRC8pdqMleeEtXUbl2z1oQ/uNtI=" }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", - "requires": { - "is-unc-path": "^1.0.0" - } + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=" + }, + "is-npm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", + "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==" + }, + "is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==" + }, + "is-path-inside": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.2.tgz", + "integrity": "sha512-/2UGPSgmtqwo1ktx8NDHjuPwZWmHhO+gj0f93EkhLB5RgW9RZevWYYlIkS6zePc6U2WpOdQYIwHe9YC4DWEBVg==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "requires": { + "is-unc-path": "^1.0.0" + } + }, + "is-set": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.1.tgz", + "integrity": "sha512-eJEzOtVyenDs1TMzSQ3kU3K+E0GUS9sno+F0OBT97xsgcJsF9nXMBtkT9/kut5JEpM7oL7X/0qxR17K3mcwIAA==" + }, + "is-string": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", + "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==" + }, + "is-symbol": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", + "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" }, "is-unc-path": { "version": "1.0.0", @@ -368,21 +1267,181 @@ "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" }, + "is-yarn-global": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", + "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==" + }, "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" + }, + "iterate-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.1.tgz", + "integrity": "sha512-3Q6tudGN05kbkDQDI4CqjaBf4qf85w6W6GnuZDtUVYwKgtC1q8yxYX7CZed7N+tLzQqS6roujWvszf13T+n9aw==" + }, + "iterate-value": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", + "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", + "requires": { + "es-get-iterator": "^1.0.2", + "iterate-iterator": "^1.0.1" + } + }, + "javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=" + }, + "json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=" }, + "json5": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", + "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", + "requires": { + "minimist": "^1.2.5" + } + }, + "keypress": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/keypress/-/keypress-0.2.1.tgz", + "integrity": "sha1-HoBFQlABjbrUw/6USX1uZ7YmnHc=" + }, + "keyv": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.0.3.tgz", + "integrity": "sha512-zdGa2TOpSZPq5mU6iowDARnMBZgtCqJ11dJROFi6tg6kTn4nuUdU09lFyLFSaHrWqpIJ+EBq4E8/Dc0Vx5vLdA==", + "requires": { + "json-buffer": "3.0.1" + } + }, + "latest-version": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", + "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", + "requires": { + "package-json": "^6.3.0" + } + }, "leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" }, + "lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + }, + "log-symbols": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-3.0.0.tgz", + "integrity": "sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ==", + "requires": { + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "requires": { + "semver": "^6.0.0" + } + }, + "mathjs": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-7.5.1.tgz", + "integrity": "sha512-H2q/Dq0qxBLMw+G84SSXmGqo/znihuxviGgAQwAcyeFLwK2HksvSGNx4f3dllZF51bWOnu2op60VZxH2Sb51Pw==", + "requires": { + "complex.js": "^2.0.11", + "decimal.js": "^10.2.1", + "escape-latex": "^1.2.0", + "fraction.js": "^4.0.12", + "javascript-natural-sort": "^0.7.1", + "seed-random": "^2.2.0", + "tiny-emitter": "^2.1.0", + "typed-function": "^2.0.0" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -396,6 +1455,14 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, "mqtt": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.2.1.tgz", @@ -417,6 +1484,13 @@ "split2": "^3.1.0", "ws": "^7.3.1", "xtend": "^4.0.1" + }, + "dependencies": { + "ws": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", + "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" + } } }, "mqtt-packet": { @@ -434,11 +1508,81 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, + "node-forge": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.8.5.tgz", + "integrity": "sha512-vFMQIWt+J/7FLNyKouZ9TazT74PRV3wgv9UT4cRjC8BffxFbKXkgIWR42URCPSnHm/QDz6BOlb2Q0U4+VQT67Q==" + }, + "normalize-url": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz", + "integrity": "sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ==" + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + } + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -447,6 +1591,56 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optimist": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", + "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=", + "requires": { + "minimist": "~0.0.1", + "wordwrap": "~0.0.2" + }, + "dependencies": { + "minimist": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz", + "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8=" + } + } + }, + "ora": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-4.1.1.tgz", + "integrity": "sha512-sjYP8QyVWBpBZWD6Vr1M/KwknSw6kJOz41tvGMlwWeClHBtYKTbHMki1PsLZnxKpXMPbTKv9b3pjQu3REib96A==", + "requires": { + "chalk": "^3.0.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.2.0", + "is-interactive": "^1.0.0", + "log-symbols": "^3.0.0", + "mute-stream": "0.0.8", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, "ordered-read-streams": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", @@ -455,11 +1649,38 @@ "readable-stream": "^2.0.1" } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" + }, + "p-cancelable": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.0.0.tgz", + "integrity": "sha512-wvPXDmbMmu2ksjkB4Z3nZWTSkJEb9lqVdMaCKpZUGJG9TMiNp9XcbG3fn9fPKjem04fJMJnXoyFPk2FmgiaiNg==" + }, + "p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "requires": { + "p-timeout": "^3.1.0" + } + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" }, + "p-queue": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.1.tgz", + "integrity": "sha512-miQiSxLYPYBxGkrldecZC18OTLjdUqnlRebGzPRiVxB8mco7usCmm7hFuxiTvp93K18JnLtE4KMMycjAu/cQQg==", + "requires": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.1.0" + } + }, "p-retry": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.2.0.tgz", @@ -477,6 +1698,146 @@ "p-finally": "^1.0.0" } }, + "package-json": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", + "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", + "requires": { + "got": "^9.6.0", + "registry-auth-token": "^4.0.0", + "registry-url": "^5.0.0", + "semver": "^6.2.0" + }, + "dependencies": { + "@sindresorhus/is": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", + "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" + }, + "@szmarczak/http-timer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", + "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", + "requires": { + "defer-to-connect": "^1.0.1" + } + }, + "cacheable-request": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", + "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", + "requires": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^3.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^4.1.0", + "responselike": "^1.0.2" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" + } + } + }, + "decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "requires": { + "mimic-response": "^1.0.0" + } + }, + "defer-to-connect": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", + "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==" + }, + "get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "requires": { + "pump": "^3.0.0" + } + }, + "got": { + "version": "9.6.0", + "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", + "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", + "requires": { + "@sindresorhus/is": "^0.14.0", + "@szmarczak/http-timer": "^1.1.2", + "cacheable-request": "^6.0.0", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^4.1.0", + "lowercase-keys": "^1.0.1", + "mimic-response": "^1.0.1", + "p-cancelable": "^1.0.0", + "to-readable-stream": "^1.0.0", + "url-parse-lax": "^3.0.0" + } + }, + "json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=" + }, + "keyv": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", + "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", + "requires": { + "json-buffer": "3.0.0" + } + }, + "lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==" + }, + "mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==" + }, + "p-cancelable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", + "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==" + }, + "responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "requires": { + "lowercase-keys": "^1.0.0" + } + }, + "to-readable-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", + "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==" + } + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "path-dirname": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", @@ -487,11 +1848,29 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "promise.any": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise.any/-/promise.any-2.0.1.tgz", + "integrity": "sha512-3amHUuhVhkhFVw8mAM33pyt1zBoYK9O9SorjWbE+E3zSTb4AUpJmK5+rt5g6OCtZpgBlT1cTxF/bp/SNeMQSUQ==", + "requires": { + "array.prototype.map": "^1.0.1", + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1", + "es-aggregate-error": "^1.0.2", + "function-bind": "^1.1.1", + "iterate-value": "^1.0.1" + } + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -522,6 +1901,35 @@ } } }, + "pupa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.0.1.tgz", + "integrity": "sha512-hEJH0s8PXLY/cdXh66tNEQGndDrIKNqNC5xmrysZy3i5C3oEoLna7YAOad+7u125+zH1HNXUmGEkrhb3c2VriA==", + "requires": { + "escape-goat": "^2.0.0" + } + }, + "qrcode-terminal": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==" + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "requires": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + } + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -534,6 +1942,29 @@ "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" + }, + "dependencies": { + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + } + } + }, + "registry-auth-token": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.0.tgz", + "integrity": "sha512-P+lWzPrsgfN+UEpDS3U8AQKg/UjZX6mQSJueZj3EK+vNESoqBSpBUD3gmu4sF9lOsjXWjF11dQKUqemf3veq1w==", + "requires": { + "rc": "^1.2.8" + } + }, + "registry-url": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", + "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", + "requires": { + "rc": "^1.2.8" } }, "reinterval": { @@ -546,16 +1977,137 @@ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, + "responselike": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.0.tgz", + "integrity": "sha512-xH48u3FTB9VsZw7R+vvgaKeLKzT6jOogbQhEe/jewwnZgzPcnyWui2Av6JpoYZF/91uueC+lqhWqeURw5/qhCw==", + "requires": { + "lowercase-keys": "^2.0.0" + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, "retry": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" }, + "run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==" + }, + "rxjs": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.3.tgz", + "integrity": "sha512-trsQc+xYYXZ3urjOiJOuCOa5N3jAZ3eiSpQB5hIT8zGlL2QfnHLJ2r7GMkBGuIausdJN1OneaI6gQlsqNHHmZQ==", + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "seed-random": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz", + "integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=" + }, + "semaphore": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semaphore/-/semaphore-1.1.0.tgz", + "integrity": "sha512-O4OZEaNtkMd/K0i6js9SL+gqy0ZCBMgUvlSqHKi4IBdjhe7wB8pwztUk1BbZ1fmrvpwFrPbHzqd2w5pTcJH6LA==" + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "semver-diff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", + "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", + "requires": { + "semver": "^6.3.0" + } + }, + "send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", + "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "requires": { + "debug": "2.6.9", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "~1.7.2", + "mime": "1.6.0", + "ms": "2.1.1", + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "ms": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", + "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + } + } + }, + "serve-static": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", + "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.17.1" + } + }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, + "signal-exit": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", + "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" + }, "split2": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", @@ -576,11 +2128,44 @@ } } }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, "stream-shift": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "string.prototype.trimend": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", + "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", + "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -589,6 +2174,37 @@ "safe-buffer": "~5.1.0" } }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "term-size": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.0.tgz", + "integrity": "sha512-a6sumDlzyHVJWb8+YofY4TW112G6p2FCPEAFk+59gIYHv3XHRhm9ltVQ9kli4hNWeQBwSpe8cRN25x0ROunMOw==" + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -607,6 +2223,19 @@ "xtend": "~4.0.0" } }, + "tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "to-absolute-glob": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", @@ -616,14 +2245,40 @@ "is-negated-glob": "^1.0.0" } }, + "to-readable-stream": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-2.1.0.tgz", + "integrity": "sha512-o3Qa6DGg1CEXshSdvWNX2sN4QHqg03SPq7U6jPXRahlQdl5dK8oXjkU/2/sGrnOZKeGV1zLSO8qPwyKklPPE7w==" + }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, + "tslib": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.0.tgz", + "integrity": "sha512-+Zw5lu0D9tvBMjGP8LpvMb0u2WW2QV3y+D8mO6J+cNzCYIN4sVy43Bf9vl92nqFahutN0I8zHa7cc4vihIshnw==" + }, "tuyapi": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/tuyapi/-/tuyapi-5.3.1.tgz", - "integrity": "sha512-l0bbWxe4L8J7/bAQn0bJtBVbVDAEglC1T3a/YKYM3UvDXaKgFQUDVKhfQfHFAt0bzXVq1TeqU0zG4WIrxgiTHg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tuyapi/-/tuyapi-6.0.1.tgz", + "integrity": "sha512-2Qg0/avg3gtsDLRIktssFtxUA/WT6fvB+GCmQwb1uRQq6KoTsS9he2vDGzmExwaek8Fz3vgAACH7WNIRemCgDw==", "requires": { "debug": "4.1.1", + "p-queue": "6.6.1", "p-retry": "4.2.0", "p-timeout": "3.2.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + } } }, "type": { @@ -631,11 +2286,34 @@ "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" }, + "type-fest": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.10.0.tgz", + "integrity": "sha512-EUV9jo4sffrwlg8s0zDhP0T2WD3pru5Xi0+HTE3zTUmBaZNhfkite9PdSJwdXLwPVW0jnAHT56pZHIOYckPEiw==" + }, + "typed-function": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-2.0.0.tgz", + "integrity": "sha512-Hhy1Iwo/e4AtLZNK10ewVVcP2UEs408DS35ubP825w/YgSBK1KVLwALvvIG4yX75QJrxjCpcWkzkVRB0BwwYlA==" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "ultron": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==" + }, "unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", @@ -650,20 +2328,114 @@ "through2-filter": "^3.0.0" } }, + "unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "requires": { + "crypto-random-string": "^2.0.0" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + }, + "update-notifier": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", + "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", + "requires": { + "boxen": "^4.2.0", + "chalk": "^3.0.0", + "configstore": "^5.0.1", + "has-yarn": "^2.1.0", + "import-lazy": "^2.1.0", + "is-ci": "^2.0.0", + "is-installed-globally": "^0.3.1", + "is-npm": "^4.0.0", + "is-yarn-global": "^0.3.0", + "latest-version": "^5.0.0", + "pupa": "^2.0.1", + "semver-diff": "^3.1.1", + "xdg-basedir": "^4.0.0" + }, + "dependencies": { + "chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + } + } + }, + "url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", + "requires": { + "prepend-http": "^2.0.0" + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "requires": { + "defaults": "^1.0.3" + } + }, + "widest-line": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", + "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", + "requires": { + "string-width": "^4.0.0" + } + }, + "wordwrap": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz", + "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc=" + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "ws": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", + "requires": { + "async-limiter": "~1.0.0", + "safe-buffer": "~5.1.0", + "ultron": "~1.1.0" + } + }, + "xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==" }, "xtend": { "version": "4.0.2", diff --git a/package.json b/package.json index f889d3b..44d48e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tuya-mqtt", - "version": "2.1.0", + "version": "3.0.0", "description": "Control Tuya devices locally via MQTT", "homepage": "https://github.com/TheAgentK/tuya-mqtt#readme", "main": "tuya-mqtt.js", @@ -13,10 +13,14 @@ }, "license": "ISC", "dependencies": { + "@tuyapi/cli": "^1.13.4", "color-convert": "^2.0.1", "debug": "^4.1.1", + "json5": "^2.1.3", "mqtt": "^4.2.1", - "tuyapi": "^5.3.1" + "supports-color": "^7.2.0", + "tuyapi": "^6.0.1", + "mathjs": "7.5.1" }, "repository": { "type": "git", diff --git a/tuya-color.js b/tuya-color.js deleted file mode 100644 index ea2374b..0000000 --- a/tuya-color.js +++ /dev/null @@ -1,348 +0,0 @@ -const convert = require('color-convert'); -const debug = require('debug')('TuyaColor'); - -/** - * Class to calculate settings for Tuya colors - */ -function TuyaColorLight() { - - this.colorMode = 'white'; // or 'colour' - this.brightness = 100; // percentage value use _convertValToPercentage functions below. - - this.color = { - H: 130, - S: 100, - L: 50 - }; - - this.hue = this.color.H; - this.saturation = this.color.S; - this.lightness = this.color.L; - - this.colorTemperature = 255; - this.colorTempMin = 153; - this.colorTempMax = 500; - - this.dps = {}; -} - -/** - * calculate color value from given brightness percentage - * @param (Integer) percentage 0-100 percentage value - * @returns (Integer) color value from 25 - 255 - * @private - */ -TuyaColorLight.prototype._convertBrightnessPercentageToVal = function(brt_percentage){ - // the brightness scale does not start at 0 but starts at 25 - 255 - // this linear equation is a better fit to the conversion to 255 scale - var tmp = Math.round(2.3206*brt_percentage+22.56); - debug('Converted brightness percentage ' + brt_percentage + ' to: ' + tmp); - return tmp; -} - -/** - * calculate percentage from brightness color value - * @param brt_val 25 - 255 brightness color value - * @returns {Integer} 0 - 100 integer percent - * @private - */ -TuyaColorLight.prototype._convertValtoBrightnessPercentage = function(brt_val){ - var tmp = Math.round( (brt_val-22.56)/2.3206); - debug('Converted brightness value ' + brt_val + ' to: ' + tmp); - return tmp; -} - -/** - * calculate color value from given saturation percentage OR color temperature percentage - * @param (Integer) temp_percentage 0-100 percentage value - * @returns {Integer} saturation or color temperature value from 0 - 255 - * @private - */ -TuyaColorLight.prototype._convertSATorColorTempPercentageToVal = function(temp_percentage){ - // the saturation OR temperature scale does start at 0 - 255 - // this is a perfect linear equation fit for the saturation OR temperature scale conversion - var tmp = Math.round(((2.5498*temp_percentage)-0.4601)); - debug('Converted saturation OR temperature percentage ' + temp_percentage + ' to: ' + tmp); - return tmp; -} - -/** - * calculate percentage from saturation value OR color temperature value - * @param temp_val 0 - 255 saturation or color temperature value - * @returns {Integer} 0 - 100 integer percent - * @private - */ -TuyaColorLight.prototype._convertValtoSATorColorTempPercentage = function(temp_val){ - var tmp = Math.round( (temp_val+0.4601/2.5498)); - debug('Converted saturation OR temperature value ' + temp_val + ' to: ' + tmp); - return tmp; -} - -/** - * calculate color value from given percentage - * @param {Integer} percentage 0-100 percentage value - * @returns {Integer} color value from 0-255 - */ -TuyaColorLight.prototype._convertPercentageToVal = function (percentage) { - var tmp = Math.round(255 * (percentage / 100)); - debug('Converted ' + percentage + ' to: ' + tmp); - return tmp; -}; - -/** - * calculate percentage from color value - * @param {Integer} val 0-255 color value - * @returns {Integer} HK-Value - */ -TuyaColorLight.prototype._convertValToPercentage = function (val) { - var tmp = Math.round((val / 255) * 100); - debug('Converted ' + val + ' to: ' + tmp); - return tmp; -}; - -/** - * converts color value to color temperature - * @param {Integer} val - * @returns {Integer} percentage from 0-100 - */ -TuyaColorLight.prototype._convertColorTemperature = function (val) { - var tmpRange = this.colorTempMax - this.colorTempMin; - var tmpCalc = Math.round((val / this.colorTempMax) * 100); - - debug('HK colorTemp Value: ' + val); - debug('HK colorTemp scale min : ' + this.colorTempMin); - debug('HK colorTemp scale max : ' + this.colorTempMax); - debug('HK colorTemp range (tmpRange): ' + tmpRange); - debug('HK colorTemp % tmpCalc: ' + tmpCalc); - - var tuyaColorTemp = this._convertPercentageToVal(tmpCalc); - - debug('HK tuyaColorTemp: ' + tuyaColorTemp); - - return tuyaColorTemp; -}; - -/** - * Convert color temperature to HK - * @param {Integer} val - * @returns {Integer} HK-Value - */ -TuyaColorLight.prototype._convertColorTemperatureToHK = function (val) { - - var tuyaColorTempPercent = this._convertValToPercentage(this.colorTemperature); - var tmpRange = this.colorTempMax - this.colorTempMin; - var tmpCalc = Math.round((tmpRange * (tuyaColorTempPercent / 100)) + this.colorTempMin); - var hkValue = Math.round(tmpCalc); - - debug('Tuya color Temperature : ' + val); - debug('Tuya color temp Percent of 255: ' + tuyaColorTempPercent + '%'); - - debug('HK colorTemp scale min : ' + this.colorTempMin); - debug('HK colorTemp scale max : ' + this.colorTempMax); - - debug('HK Color Temp Range: ' + tmpRange); - debug('HK range %: ' + tuyaColorTempPercent); - debug('HK Value: ' + hkValue); - - return hkValue; -}; - -/** - * check if given String is HEX - * @param {String} h - * @returns {boolean} - */ -TuyaColorLight.prototype._ValIsHex = function (h) { - debug("Check if value is hex", h); - return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(h) -}; - -/** - * get width Hex digits from given value - * @param (Integer) value, decimal value to convert to hex string - * @param (Integer) width, the number of hex digits to return - * @returns {string} value as HEX containing (width) number of hex digits - * @private - */ -TuyaColorLight.prototype._getHex = function (value,width){ - var hex = (value+Math.pow(16, width)).toString(16).slice(-width).toLowerCase(); - debug('value: ' + value + ' hex: ' + hex); - return hex; -} -/** - * get AlphaHex from percentage brightness - * @param {Integer} brightness - * @return {string} brightness as HEX value - */ -TuyaColorLight.prototype._getAlphaHex = function (brightness) { - var i = brightness / 100; - var alpha = Math.round(i * 255); - var hex = (alpha + 0x10000).toString(16).substr(-2); - var perc = Math.round(i * 100); - - debug('alpha percent: ' + perc + '% hex: ' + hex + ' alpha: ' + alpha); - return hex; -}; - -/** - * Set saturation from value - * @param {Integer} value - */ -TuyaColorLight.prototype.setSaturation = function (value) { - this.color.S = value; - this.saturation = value; - this.colorMode = 'colour'; - - debug('SET SATURATION: ' + value); -}; - -/** - * Set Brightness - * @param {Integer} value - */ -TuyaColorLight.prototype.setBrightness = function (value) { - this.brightness = value; - //var newValue = this._convertPercentageToVal(value); - var newValue = this._convertBrightnessPercentageToVal(value); - debug("BRIGHTNESS from UI: " + value + ' Converted from 100 to 255 scale: ' + newValue); -} - -/** - * @param {} value - */ -TuyaColorLight.prototype.setHue = function (value) { - debug('SET HUE: ' + value); - debug('Saturation Value: ' + this.color.S); - this.color.H = value; - - //check color and set colormode if necessary - debug("colormode", value, this.color.S); - if (value === 0 && this.color.S === 0) { - this.colorMode = 'white'; - debug('SET Color Mode: \'white\''); - } else { - this.colorMode = 'colour'; - debug('SET Color Mode: \'colour\' -- dahhhhhh british spelling \'coulour\' really is annoying... why you gotta be special?'); - } - - - return { - color: this.color, - colorMode: this.colorMode, - hue: this.color.H, - saturation: this.saturation - }; -}; - -/** - * Set HSL color - * @param {Integer} hue - * @param {Integer} saturation - * @param {Integer} brightness - */ -TuyaColorLight.prototype.setHSL = function (hue, saturation, brightness) { - this.setSaturation(saturation); - this.setBrightness(brightness); - this.setHue(hue); -} - -/** - * Set color from given string - * @param {String} colorValue could be HEX or HSL color type - * @returns {Object} dps settings for given color - */ -TuyaColorLight.prototype.setColor = function (colorValue) { - debug("Recieved color", colorValue); - - if (this._ValIsHex(colorValue)) { - debug("Color is Hex"); - var color = convert.hex.hsl(colorValue); - } else { - debug("Color is HSL"); - var color = colorValue.split(","); - // convert strings to numbers - color.forEach(function (element, key) { - color[key] = parseInt(element, 10); - }); - } - debug("Converted color as HSL", { - 0: color[0] + " - " + typeof color[0], - 1: color[1] + " - " + typeof color[1], - 2: color[2] + " - " + typeof color[2] - }) - - this.setHSL(color[0], color[1], color[2]); - return this.getDps(); -} - -/** - * get dps settings for current color - * @returns {Object} dps settings - */ -TuyaColorLight.prototype.getDps = function () { - var color = this.color; - - var lightness = Math.round(this.brightness / 2); - var brightness = this.brightness; - //var apiBrightness = this._convertPercentageToVal(brightness); - var apiBrightness = this._convertBrightnessPercentageToVal(brightness); - - //var alphaBrightness = this._getAlphaHex(brightness); - var alphaBrightness = this._getHex(apiBrightness,2); - - var hexColor1 = convert.hsl.hex(color.H, color.S, lightness); - - //var hexColor2 = convert.hsl.hex(0, 0, lightness); - var hexColor2 = this._getHex(color.H,4); - hexColor2 = hexColor2 + this._getHex(this._convertSATorColorTempPercentageToVal(color.S),2); - - var colorTemperature = this.colorTemperature; - - var lightColor = (hexColor1 + hexColor2 + alphaBrightness).toLowerCase(); - - //var temperature = (this.colorMode === 'colour') ? 255 : this._convertColorTemperature(colorTemperature); - // color temperature percentage is at a fixed 51% - var temperature = this._convertSATorColorTempPercentageToVal(51); - - // if the bulb is in colour mode than the dps 3 and dps 4 are ignored by the bulb but if you set it now - // some tuya bulbs will ignore dps 5 because you set dps 3 or dps 4 - // FOR colour mode the bulb looks at dps 1, dps 2, and dps 5. - // DPS 5 is in the following format: - // HSL to HEX format are the leftmost hex digits (hex digits 14 - 9) - // hex digits 8 - 5 are the HSB/HSL Hue value in HEX format - // hex digits 4 - 3 are the HSB/HSL Saturation percentage as a value (converted to 0-255 scale) in HEX format - // hex digits 2 - 1 are the HSB Brightness percentage as a value (converted to 25-255 scale) in HEX format - - if (this.colorMode === 'colour') { - dpsTmp = { - '1': true, - '2': this.colorMode, - //'3': apiBrightness, - //'4': temperature, - '5': lightColor - // '6' : hexColor + hexColor + 'ff' - }; - debug("dps", dpsTmp); - return dpsTmp; - } - - // if the bulb is in white mode then the dps 5 value is ignored by the bulb but if you set dps 5 value now - // you may not get a response back from the bulb on the dps values - // FOR white mode the bulb looks at dps 1, dps 2, dps 3 and dps 4 - // DPS 3 is the HSB/HSL Brightness percentage converted to a value from 25 to 255 in decimal format - // DPS 4 is the HSB/HSL Saturation percentage converted to a value from 0 to 255 in decimal format - if (this.colorMode === 'white'){ - dpsTmp = { - '1': true, - '2': this.colorMode, - '3': apiBrightness, - '4': temperature, - //'5': lightColor - // '6' : hexColor + hexColor + 'ff' - }; - debug("dps", dpsTmp); - return dpsTmp; - } -} - -module.exports = TuyaColorLight; \ No newline at end of file diff --git a/tuya-device.js b/tuya-device.js deleted file mode 100644 index c0ae76d..0000000 --- a/tuya-device.js +++ /dev/null @@ -1,270 +0,0 @@ -const TuyAPI = require('tuyapi'); -const TuyColor = require('./tuya-color'); -const debug = require('debug')('TuyAPI:device'); -const debugError = require('debug')('TuyAPI:device:error'); -const debugColor = require('debug')('TuyAPI:device:color'); - -/** - * - var steckdose = new TuyaDevice({ - id: '03200240600194781244', - key: 'b8bdebab418f5b55', - ip: '192.168.178.45', - type: "ver33" - }); - */ - -var TuyaDevice = (function () { - var devices = []; - var events = {}; - - function checkExisiting(id) { - var existing = false; - // Check for existing instance - devices.forEach(device => { - if (device.hasOwnProperty("options")) { - if (id === device.options.id) { - existing = device; - } - } - }); - return existing; - } - - function deleteDevice(id) { - devices.forEach((device, key) => { - if (device.hasOwnProperty("options")) { - if (id === device.options.id) { - debug("delete Device", devices[key].toString()); - delete devices[key]; - } - } - }); - } - - function TuyaDevice(options, callback) { - var device = this; - // Check for existing instance - if (existing = checkExisiting(options.id)) { - return new Promise((resolve, reject) => { - resolve({ - status: "connected", - device: existing - }); - }); - } - - if (!(this instanceof TuyaDevice)) { - return new TuyaDevice(options); - } - - options.type = options.type || undefined; - - this.type = options.type; - this.options = options; - - Object.defineProperty(this, 'device', { - value: new TuyAPI(JSON.parse(JSON.stringify(this.options))) - }); - - this.device.on('data', data => { - if (typeof data == "string") { - debugError('Data from device not encrypted:', data.replace(/[^a-zA-Z0-9 ]/g, "")); - } else { - debug('Data from device:', data); - device.triggerAll('data', data); - } - }); - - devices.push(this); - // Find device on network - debug("Search device in network"); - this.find().then(() => { - debug("Device found in network"); - // Connect to device - this.device.connect(); - }); - - /** - * @return promis to wait for connection - */ - return new Promise((resolve, reject) => { - this.device.on('connected', () => { - device.triggerAll('connected'); - device.connected = true; - debug('Connected to device.', device.toString()); - resolve({ - status: "connected", - device: this - }); - }); - this.device.on('disconnected', () => { - device.triggerAll('disconnected'); - device.connected = false; - debug('Disconnected from device.', device.toString()); - deleteDevice(options.id); - return reject({ - status: "disconnect", - device: null - }); - }); - - this.device.on('error', (err) => { - debugError(err); - device.triggerAll('error', err); - return reject({ - error: err, - device: this - }); - }); - }); - } - - TuyaDevice.prototype.toString = function () { - if (typeof this.type != "undefined") { - return this.type + " (" + this.options.ip + ", " + this.options.id + ", " + this.options.key + ")"; - } else { - return " (" + this.options.ip + ", " + this.options.id + ", " + this.options.key + ")"; - } - } - - TuyaDevice.prototype.triggerAll = function (name, argument) { - var device = this; - var e = events[name] || []; - e.forEach(event => { - event.call(device, argument); - }); - } - - TuyaDevice.prototype.on = function (name, callback) { - if (!this.connected) return; - var device = this; - this.device.on(name, function () { - callback.apply(device, arguments); - }); - } - - TuyaDevice.prototype.find = function () { - return this.device.find(); - } - - TuyaDevice.prototype.get = function () { - return this.device.get(); - } - - TuyaDevice.prototype.set = function (options) { - debug('set:', options); - return new Promise((resolve, reject) => { - this.device.set(options).then((result) => { - this.get().then(() => { - debug("set completed "); - resolve(result); - }); - }); - }); - } - - TuyaDevice.prototype.switch = function (newStatus, callback) { - if (!this.connected) return; - newStatus = newStatus.toLowerCase(); - if (newStatus == "on") { - return this.switchOn(callback); - } - if (newStatus == "off") { - return this.switchOff(callback); - } - if (newStatus == "toggle") { - return this.toggle(callback); - } - } - - TuyaDevice.prototype.switchOn = function () { - if (!this.connected) return; - debug("switch -> ON"); - - return this.set({ - set: true - }); - } - - TuyaDevice.prototype.switchOff = function () { - if (!this.connected) return; - debug("switch -> OFF"); - - return this.set({ - set: false - }); - } - - TuyaDevice.prototype.toggle = function () { - if (!this.connected) return; - return new Promise((resolve, reject) => { - this.get().then((status) => { - debug("toogle state", status); - this.set({ - set: !status - }); - }); - }); - } - - TuyaDevice.prototype.schema = function(obj){ - return this.get(obj).then((status) => { - debug("get", obj); - }); - } - - TuyaDevice.prototype.setColor = function (hexColor) { - if (!this.connected) return; - debugColor("Set color to: ", hexColor); - var tuya = this.device; - var color = new TuyColor(tuya); - var dps = color.setColor(hexColor); - debugColor("dps values:", dps); - - return this.set({ - multiple: true, - data: dps - }); - } - - TuyaDevice.prototype.connect = function (callback) { - debug("Connect to TuyAPI Device"); - return this.device.connect(callback); - } - - TuyaDevice.prototype.disconnect = function (callback) { - debug("Disconnect from TuyAPI Device"); - return this.device.disconnect(callback); - } - - Object.defineProperty(TuyaDevice, 'devices', { - value: devices - }); - - TuyaDevice.connectAll = function () { - devices.forEach(device => { - device.connect(); - }); - } - - TuyaDevice.disconnectAll = function () { - devices.forEach(device => { - device.disconnect(); - }); - } - - TuyaDevice.onAll = function (name, callback) { - if (events[name] == undefined) { - events[name] = []; - } - events[name].push(callback); - devices.forEach(device => { - device.triggerAll(name); - }); - } - - return TuyaDevice; -}()); - -module.exports = TuyaDevice; diff --git a/tuya-mqtt.js b/tuya-mqtt.js index ca802c2..048d8fa 100644 --- a/tuya-mqtt.js +++ b/tuya-mqtt.js @@ -1,374 +1,177 @@ -const mqtt = require('mqtt'); -const TuyaDevice = require('./tuya-device'); -const debug = require('debug')('TuyAPI:mqtt'); -const debugColor = require('debug')('TuyAPI:mqtt:color'); -const debugTuya = require('debug')('TuyAPI:mqtt:device'); -const debugError = require('debug')('TuyAPI:mqtt:error'); -var cleanup = require('./cleanup').Cleanup(onExit); - -var CONFIG = undefined; -var mqtt_client = undefined; - -function bmap(istate) { - return istate ? 'ON' : "OFF"; -} - -function boolToString(istate) { - return istate ? 'true' : "false"; +#!/usr/bin/env node +const fs = require('fs') +const mqtt = require('mqtt') +const json5 = require('json5') +const debug = require('debug')('tuya-mqtt:info') +const debugCommand = require('debug')('tuya-mqtt:command') +const debugError = require('debug')('tuya-mqtt:error') +const SimpleSwitch = require('./devices/simple-switch') +const SimpleDimmer = require('./devices/simple-dimmer') +const RGBTWLight = require('./devices/rgbtw-light') +const GenericDevice = require('./devices/generic-device') +const utils = require('./lib/utils') + +var CONFIG = undefined +var tuyaDevices = new Array() + +// Setup Exit Handlers +process.on('exit', processExit.bind(0)) +process.on('SIGINT', processExit.bind(0)) +process.on('SIGTERM', processExit.bind(0)) +process.on('uncaughtException', processExit.bind(1)) + +// Disconnect from and publish offline status for all devices on exit +async function processExit(exitCode) { + for (let tuyaDevice of tuyaDevices) { + tuyaDevice.device.disconnect() + } + if (exitCode || exitCode === 0) debug('Exit code: '+exitCode) + await utils.sleep(1) + process.exit() } -/* - * execute function on topic message - */ - -function IsJsonString(text) { - if (/^[\],:{}\s]*$/.test(text.replace(/\\["\\\/bfnrtu]/g, '@').replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { - //the json is ok - return true; +// Get new deivce based on configured type +function getDevice(configDevice, mqttClient) { + const deviceInfo = { + configDevice: configDevice, + mqttClient: mqttClient, + topic: CONFIG.topic + } + switch (configDevice.type) { + case 'SimpleSwitch': + return new SimpleSwitch(deviceInfo) + break; + case 'SimpleDimmer': + return new SimpleDimmer(deviceInfo) + break; + case 'RGBTWLight': + return new RGBTWLight(deviceInfo) + break; } - return false; + return new GenericDevice(deviceInfo) } -/** - * check mqtt-topic string for old notation with included device type - * @param {String} topic - */ -function checkTopicNotation(_topic) { - var topic = _topic.split("/"); - var type = topic[1]; - var result = (type == "socket" || type == "lightbulb" || type == "ver3.1" || type == "ver3.3"); - return result; +function initDevices(configDevices, mqttClient) { + for (let configDevice of configDevices) { + const newDevice = getDevice(configDevice, mqttClient) + tuyaDevices.push(newDevice) + } } -/** - * get action from mqtt-topic string - * @param {String} topic - * @returns {String} action type - */ -function getActionFromTopic(_topic) { - var topic = _topic.split("/"); - - if (checkTopicNotation(_topic)) { - return topic[5]; - } else { - return topic[4]; +// Republish devices 2x with 30 seconds sleep if restart of HA is detected +async function republishDevices() { + for (let i = 0; i < 2; i++) { + debug('Resending device config/state in 30 seconds') + await utils.sleep(30) + for (let device of tuyaDevices) { + device.publishMqtt(device.baseTopic+'status', 'online') + device.init() + } + await utils.sleep(2) } } -/** - * get device informations from mqtt-topic string - * @param {String} topic - * @returns {String} object.id - * @returns {String} object.key - * @returns {String} object.ip - */ -function getDeviceFromTopic(_topic) { - var topic = _topic.split("/"); - - if (checkTopicNotation(_topic)) { - // When there are 5 topic levels - // topic 2 is id, and topic 3 is key - var options = { - id: topic[2], - key: topic[3] - }; - - // 4th topic is IP address or "discover" keyword - if (topic[4] !== "discover") { - options.ip = topic[4] - // If IP is manually specified check if topic 1 - // is protocol version and set accordingly - if (topic[1] == "ver3.3") { - options.version = "3.3" - } else if (topic[1] == "ver3.1") { - options.version = "3.1" - } else { - // If topic is not version then it's device type - // Not used anymore but still supported for legacy setups - options.type = topic[1] - }; - }; - - return options; - } else { - // When there are 4 topic levels - // topic 1 is id, topic 2 is key - var options = { - id: topic[1], - key: topic[2] - }; - - // If topic 3 is not discover assume it is IP address - // Todo: Validate it is an IP address - if (topic[3] !== "discover") { - options.ip = topic[3] - }; +// Main code function +const main = async() => { + let configDevices + let mqttClient - return options; + try { + CONFIG = require('./config') + } catch (e) { + console.error('Configuration file not found!') + debugError(e) + process.exit(1) } -} -/** - * get command from mqtt - topic string - * converts simple commands to TuyAPI JSON commands - * @param {String} topic - * @returns {Object} - */ -function getCommandFromTopic(_topic, _message) { - var topic = _topic.split("/"); - var command = null; + if (typeof CONFIG.qos == 'undefined') { + CONFIG.qos = 1 + } + if (typeof CONFIG.retain == 'undefined') { + CONFIG.retain = false + } - if (checkTopicNotation(_topic)) { - command = topic[6]; - } else { - command = topic[5]; + try { + configDevices = fs.readFileSync('./devices.conf', 'utf8') + configDevices = json5.parse(configDevices) + } catch (e) { + console.error('Devices file not found!') + debugError(e) + process.exit(1) } - if (command == null) { - command = _message; + if (!configDevices.length) { + console.error('No devices found in devices file!') + process.exit(1) } - if (command != "1" && command != "0" && IsJsonString(command)) { - debug("command is JSON"); - command = JSON.parse(command); - } else { - if (command.toLowerCase() != "toggle") { - // convert simple commands (on, off, 1, 0) to TuyAPI-Commands - var convertString = command.toLowerCase() == "on" || command == "1" || command == 1 ? true : false; - command = { - set: convertString - } + mqttClient = mqtt.connect({ + host: CONFIG.host, + port: CONFIG.port, + username: CONFIG.mqtt_user, + password: CONFIG.mqtt_pass, + }) + + mqttClient.on('connect', function (err) { + debug('Connection established to MQTT server') + let topic = CONFIG.topic + '#' + mqttClient.subscribe(topic) + mqttClient.subscribe('homeassistant/status') + mqttClient.subscribe('hass/status') + initDevices(configDevices, mqttClient) + }) + + mqttClient.on('reconnect', function (error) { + if (mqttClient.connected) { + debug('Connection to MQTT server lost. Attempting to reconnect...') } else { - command = command.toLowerCase(); + debug('Unable to connect to MQTT server') } - } + }) - return command; -} + mqttClient.on('error', function (error) { + debug('Unable to connect to MQTT server', error) + }) -/** - * Publish current TuyaDevice state to MQTT-Topic - * @param {TuyaDevice} device - * @param {boolean} status - */ -function publishStatus(device, status) { - if (mqtt_client.connected == true) { + mqttClient.on('message', function (topic, message) { try { - var type = device.type; - var tuyaID = device.options.id; - var tuyaKey = device.options.key; - var tuyaIP = device.options.ip; - - if (typeof tuyaIP == "undefined") { - tuyaIP = "discover" - } - - if (typeof tuyaID != "undefined" && typeof tuyaKey != "undefined") { - var topic = CONFIG.topic; - if (typeof type != "undefined") { - topic += type + "/"; + message = message.toString() + const splitTopic = topic.split('/') + const topicLength = splitTopic.length + const commandTopic = splitTopic[topicLength - 1] + const deviceTopicLevel = splitTopic[1] + + if (topic === 'homeassistant/status' || topic === 'hass/status' ) { + debug('Home Assistant state topic '+topic+' received message: '+message) + if (message === 'online') { + republishDevices() } - topic += tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/state"; - - mqtt_client.publish(topic, status, { - retain: CONFIG.retain, - qos: CONFIG.qos - }); - debugTuya("mqtt status updated to:" + topic + " -> " + status); - } else { - debugTuya("mqtt status not updated"); - } - } catch (e) { - debugError(e); - } - } -} - -function publishColorState(device, state) { - -} - -/** - * publish all dps-values to topic - * @param {TuyaDevice} device - * @param {Object} dps - */ -function publishDPS(device, dps) { - if (mqtt_client.connected == true) { - try { - var type = device.type; - var tuyaID = device.options.id; - var tuyaKey = device.options.key; - var tuyaIP = device.options.ip; - - if (typeof tuyaIP == "undefined") { - tuyaIP = "discover" - } - - if (typeof tuyaID != "undefined" && typeof tuyaKey != "undefined") { - var baseTopic = CONFIG.topic; - if (typeof type != "undefined") { - baseTopic += type + "/"; + } else if (commandTopic.includes('command')) { + // If it looks like a valid command topic try to process it + debugCommand('Received MQTT message -> ', JSON.stringify({ + topic: topic, + message: message + })) + + // Use device topic level to find matching device + const device = tuyaDevices.find(d => d.options.name === deviceTopicLevel || d.options.id === deviceTopicLevel) + switch (topicLength) { + case 3: + device.processCommand(message, commandTopic) + break; + case 4: + device.processDpsCommand(message) + break; + case 5: + const dpsKey = splitTopic[topicLength-2] + device.processDpsKeyCommand(message, dpsKey) + break; } - baseTopic += tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/dps"; - - var topic = baseTopic; - var data = JSON.stringify(dps); - debugTuya("mqtt dps updated to:" + topic + " -> ", data); - mqtt_client.publish(topic, data, { - retain: CONFIG.retain, - qos: CONFIG.qos - }); - - Object.keys(dps).forEach(function (key) { - var topic = baseTopic + "/" + key; - var data = JSON.stringify(dps[key]); - debugTuya("mqtt dps updated to:" + topic + " -> dps[" + key + "]", data); - mqtt_client.publish(topic, data, { - retain: CONFIG.retain, - qos: CONFIG.qos - }); - }); - } else { - debugTuya("mqtt dps not updated"); } } catch (e) { - debugError(e); + debugError(e) } - } -} - -/** - * event fires if TuyaDevice sends data - * @see TuyAPI (https://github.com/codetheweb/tuyapi) - */ -TuyaDevice.onAll('data', function (data) { - try { - if (typeof data.dps != "undefined") { - debugTuya('Data from device ' + this.tuyID + ' :', data); - var status = data.dps['1']; - if (typeof status != "undefined") { - publishStatus(this, bmap(status)); - } - publishDPS(this, data.dps); - } - } catch (e) { - debugError(e); - } -}); - -/** - * Function call on script exit - */ -function onExit() { - TuyaDevice.disconnectAll(); -}; - -// Simple sleep to pause in async functions -function sleep(sec) { - return new Promise(res => setTimeout(res, sec*1000)); -} - -// Main code function -const main = async() => { - - try { - CONFIG = require("./config"); - } catch (e) { - console.error("Configuration file not found") - debugError(e) - process.exit(1) - } - - if (typeof CONFIG.qos == "undefined") { - CONFIG.qos = 2; - } - if (typeof CONFIG.retain == "undefined") { - CONFIG.retain = false; - } - - mqtt_client = mqtt.connect({ - host: CONFIG.host, - port: CONFIG.port, - username: CONFIG.mqtt_user, - password: CONFIG.mqtt_pass, - }); - - mqtt_client.on('connect', function (err) { - debug("Connection established to MQTT server"); - var topic = CONFIG.topic + '#'; - mqtt_client.subscribe(topic, { - retain: CONFIG.retain, - qos: CONFIG.qos - }); - }); - - mqtt_client.on("reconnect", function (error) { - if (mqtt_client.connected) { - debug("Connection to MQTT server lost. Attempting to reconnect..."); - } else { - debug("Unable to connect to MQTT server"); - } - }); - - mqtt_client.on("error", function (error) { - debug("Unable to connect to MQTT server", error); - }); - - mqtt_client.on('message', function (topic, message) { - try { - message = message.toString(); - var action = getActionFromTopic(topic); - var options = getDeviceFromTopic(topic); - - debug("receive settings", JSON.stringify({ - topic: topic, - action: action, - message: message, - options: options - })); - - var device = new TuyaDevice(options); - - device.then(function (params) { - var device = params.device; - - switch (action) { - case "command": - var command = getCommandFromTopic(topic, message); - debug("Received command: ", command); - if (command == "toggle") { - device.switch(command).then((data) => { - debug("Set device status completed: ", data); - }); - } - if (command.schema === true) { - // Trigger device schema update to update state - device.schema(command).then((data) => { - }); - debug("Get schema status command complete"); - } else { - device.set(command).then((data) => { - debug("Set device status completed: ", data); - }); - } - break; - case "color": - var color = message.toLowerCase(); - debugColor("Set color: ", color); - device.setColor(color).then((data) => { - debug("Set device color completed: ", data); - }); - break; - } - - }).catch((err) => { - debugError(err); - }); - } catch (e) { - debugError(e); - } - }); + }) } // Call the main code