From 2553d2dba7e9d403305b9e4c40c4164f1568d77d Mon Sep 17 00:00:00 2001 From: callemand Date: Thu, 1 Jun 2023 07:40:12 +0200 Subject: [PATCH 01/32] Initialize Service --- .../node-red/api/node-red.controller.js | 91 + .../docker/gladys-node-red-container.json | 31 + server/services/node-red/index.js | 49 + server/services/node-red/lib/backup.js | 48 + .../node-red/lib/checkForContainerUpdates.js | 41 + .../node-red/lib/configureContainer.js | 75 + server/services/node-red/lib/connect.js | 73 + server/services/node-red/lib/constants.js | 60 + server/services/node-red/lib/disconnect.js | 62 + .../services/node-red/lib/getConfiguration.js | 47 + server/services/node-red/lib/index.js | 61 + server/services/node-red/lib/init.js | 48 + .../services/node-red/lib/installContainer.js | 89 + server/services/node-red/lib/isEnabled.js | 13 + .../services/node-red/lib/restoreZ2mBackup.js | 48 + .../node-red/lib/saveConfiguration.js | 45 + server/services/node-red/lib/saveZ2mBackup.js | 45 + server/services/node-red/lib/setup.js | 22 + server/services/node-red/lib/status.js | 26 + server/services/node-red/package-lock.json | 1608 +++++++++++++++++ server/services/node-red/package.json | 22 + server/utils/constants.js | 1 + 22 files changed, 2605 insertions(+) create mode 100644 server/services/node-red/api/node-red.controller.js create mode 100644 server/services/node-red/docker/gladys-node-red-container.json create mode 100644 server/services/node-red/index.js create mode 100644 server/services/node-red/lib/backup.js create mode 100644 server/services/node-red/lib/checkForContainerUpdates.js create mode 100644 server/services/node-red/lib/configureContainer.js create mode 100644 server/services/node-red/lib/connect.js create mode 100644 server/services/node-red/lib/constants.js create mode 100644 server/services/node-red/lib/disconnect.js create mode 100644 server/services/node-red/lib/getConfiguration.js create mode 100644 server/services/node-red/lib/index.js create mode 100644 server/services/node-red/lib/init.js create mode 100644 server/services/node-red/lib/installContainer.js create mode 100644 server/services/node-red/lib/isEnabled.js create mode 100644 server/services/node-red/lib/restoreZ2mBackup.js create mode 100644 server/services/node-red/lib/saveConfiguration.js create mode 100644 server/services/node-red/lib/saveZ2mBackup.js create mode 100644 server/services/node-red/lib/setup.js create mode 100644 server/services/node-red/lib/status.js create mode 100644 server/services/node-red/package-lock.json create mode 100644 server/services/node-red/package.json diff --git a/server/services/node-red/api/node-red.controller.js b/server/services/node-red/api/node-red.controller.js new file mode 100644 index 0000000000..f452ab074c --- /dev/null +++ b/server/services/node-red/api/node-red.controller.js @@ -0,0 +1,91 @@ +const asyncMiddleware = require('../../../api/middlewares/asyncMiddleware'); +const logger = require('../../../utils/logger'); + +module.exports = function NodeRedController(gladys, nodeRedManager) { + + /** + * @api {get} /api/v1/service/node-red/status Get node-red connection status + * @apiName status + * @apiGroup Node-red + */ + async function status(req, res) { + logger.debug('Get status'); + const response = await nodeRedManager.status(); + res.json(response); + } + + /** + * @api {post} /api/v1/service/node-red/setup Setup + * @apiName setup + * @apiGroup Node-red + */ + async function setup(req, res) { + logger.debug('Entering setup step'); + await nodeRedManager.setup(req.body); + res.json({ + success: true, + }); + } + + /** + * @api {post} /api/v1/service/node-red/connect Connect + * @apiName connect + * @apiGroup Node-red + */ + async function connect(req, res) { + logger.debug('Entering connect step'); + await nodeRedManager.init(); + res.json({ + success: true, + }); + } + + /** + * @api {post} /api/v1/service/node-red/start Install & start Node-red container. + * @apiName installNodeRedContainer + * @apiGroup Node-red + */ + async function installNodeRedContainer(req, res) { + logger.debug('Install NodeRed container'); + await nodeRedManager.installNodeRedContainer(); + res.json({ + success: true, + }); + } + + /** + * @api {post} /api/v1/service/node-red/disconnect Disconnect + * @apiName disconnect + * @apiGroup Node-red + */ + async function disconnect(req, res) { + logger.debug('Entering disconnect step'); + await nodeRedManager.disconnect(); + res.json({ + success: true, + }); + } + + return { + 'get /api/v1/service/node-red/status': { + authenticated: true, + controller: asyncMiddleware(status), + }, + 'post /api/v1/service/node-red/setup': { + authenticated: true, + controller: asyncMiddleware(setup), + }, + 'post /api/v1/service/node-red/connect': { + authenticated: true, + controller: asyncMiddleware(connect), + }, + 'post /api/v1/service/node-red/start': { + authenticated: true, + controller: asyncMiddleware(installNodeRedContainer()), + }, + 'post /api/v1/service/node-red/disconnect': { + authenticated: true, + controller: asyncMiddleware(disconnect), + }, + }; +}; diff --git a/server/services/node-red/docker/gladys-node-red-container.json b/server/services/node-red/docker/gladys-node-red-container.json new file mode 100644 index 0000000000..fb2ed8dfa1 --- /dev/null +++ b/server/services/node-red/docker/gladys-node-red-container.json @@ -0,0 +1,31 @@ +{ + "name": "gladys-node-red", + "Image": "nodered/node-red:latest", + "ExposedPorts": {}, + "HostConfig": { + "Binds": [ + "/var/lib/gladysassistant/node-red:/data" + ], + "LogConfig": { + "Type": "json-file", + "Config": { + "max-size": "10m" + } + }, + "PortBindings": {}, + "RestartPolicy": { + "Name": "always" + }, + "NetworkMode": "host", + "Dns": [], + "DnsOptions": [], + "DnsSearch": [], + "BlkioWeightDevice": [], + "Devices": [] + }, + "NetworkDisabled": false, + "AttachStdin": false, + "AttachStdout": false, + "AttachStderr": false, + "Tty": false +} diff --git a/server/services/node-red/index.js b/server/services/node-red/index.js new file mode 100644 index 0000000000..c7affab067 --- /dev/null +++ b/server/services/node-red/index.js @@ -0,0 +1,49 @@ +const logger = require('../../utils/logger'); +const NodeRedManager = require('./lib'); +const NodeRedController = require('./api/node-red.controller'); + +module.exports = function NodeRedService(gladys, serviceId) { + const nodeRedManager = new NodeRedManager(gladys, serviceId); + + /** + * @public + * @description This function starts service. + * @example + * gladys.services['node-red'].start(); + */ + async function start() { + logger.log('Starting Node-red service'); + await nodeRedManager.init(); + } + + /** + * @public + * @description This function stops the service. + * @example + * gladys.services['node-red'].stop(); + */ + function stop() { + logger.log('Stopping Node-red service'); + nodeRedManager.disconnect(); + } + + /** + * @public + * @description Test if Node-red is running. + * @returns {Promise} Returns true if node-red is used. + * @example + * const used = await gladys.services['node-red'].isUsed(); + */ + async function isUsed() { + return nodeRedManager.gladysConnected && nodeRedManager.zigbee2mqttConnected; + // TODO Check if needed + } + + return Object.freeze({ + start, + stop, + isUsed, + device: nodeRedManager, + controllers: NodeRedController(gladys, nodeRedManager), + }); +}; diff --git a/server/services/node-red/lib/backup.js b/server/services/node-red/lib/backup.js new file mode 100644 index 0000000000..b1f7f42e34 --- /dev/null +++ b/server/services/node-red/lib/backup.js @@ -0,0 +1,48 @@ +const logger = require('../../../utils/logger'); +const { ServiceNotConfiguredError } = require('../../../utils/coreErrors'); + +/** + * @description Create a Z2M backup. + * @param {string} jobId - The job id. + * @returns {Promise} - Resolve when backup is finished. + * @example + * backup('aaf45861-c7f5-47ac-bde1-cfe56c7789cf'); + */ +async function backup(jobId) { + // Backup is not possible when service is not running + if (!this.isEnabled()) { + throw new ServiceNotConfiguredError('SERVICE_NOT_CONFIGURED'); + } + + const finishJob = (func, timer, args) => { + if (timer) { + clearTimeout(timer); + } + + // reset pending job + this.backupJob = {}; + return func(args); + }; + + const response = new Promise((resolve, reject) => { + // Prevent infinite wait + const timerId = setTimeout(finishJob, 30000, reject, null, "Backup request time's out"); + + this.backupJob = { + resolve: (args) => finishJob(resolve, timerId, args), + reject: (args) => finishJob(reject, timerId, args), + jobId, + }; + }); + + // Request z2m to generate a new backup. + logger.info('Zigbee2MQTT request for backup'); + await this.gladys.job.updateProgress(jobId, 30); + this.mqttClient.publish('zigbee2mqtt/bridge/request/backup'); + + return response; +} + +module.exports = { + backup, +}; diff --git a/server/services/node-red/lib/checkForContainerUpdates.js b/server/services/node-red/lib/checkForContainerUpdates.js new file mode 100644 index 0000000000..c99dacbb87 --- /dev/null +++ b/server/services/node-red/lib/checkForContainerUpdates.js @@ -0,0 +1,41 @@ +const logger = require('../../../utils/logger'); +const { DEFAULT } = require('./constants'); + +const nodeRedContainerDescriptor = require('../docker/gladys-node-red-container.json'); + +/** + * @description Checks if version is the latest for this service, if not, it removes existing containers. + * @param {object} config - Service configuration properties. + * @example + * await nodeRed.checkForContainerUpdates(config); + */ +async function checkForContainerUpdates(config) { + logger.info('Checking for current installed versions and required updates...'); + + // Check for MQTT container version + if (config.dockerNodeRedVersion !== DEFAULT.DOCKER_NODE_RED_VERSION) { + logger.info(`NodeRed: update #${DEFAULT.DOCKER_NODE_RED_VERSION} of the container required...`); + + const containers = await this.gladys.system.getContainers({ + all: true, + filters: { name: [nodeRedContainerDescriptor.name] }, + }); + + if (containers.length !== 0) { + logger.debug('Removing current installed NodeRed container...'); + // If container is present, we remove it + // The init process will create it again + const [container] = containers; + await this.gladys.system.removeContainer(container.id, { force: true }); + } + + // Update to last version + config.dockerNodeRedVersion = DEFAULT.DOCKER_NODE_RED_VERSION; + logger.info(`NodeRed: update #${DEFAULT.DOCKER_NODE_RED_VERSION} of the container done`); + } + +} + +module.exports = { + checkForContainerUpdates, +}; diff --git a/server/services/node-red/lib/configureContainer.js b/server/services/node-red/lib/configureContainer.js new file mode 100644 index 0000000000..0ee7c6dda9 --- /dev/null +++ b/server/services/node-red/lib/configureContainer.js @@ -0,0 +1,75 @@ +const fs = require('fs/promises'); +const { constants } = require('fs'); +const path = require('path'); +const yaml = require('yaml'); + +const logger = require('../../../utils/logger'); +const { DEFAULT } = require('./constants'); +const { DEFAULT_KEY, CONFIG_KEYS, ADAPTERS_BY_CONFIG_KEY } = require('../adapters'); + +/** + * @description Configure Z2M container. + * @param {string} basePathOnContainer - Zigbee2mqtt base path. + * @param {object} config - Gladys Z2M stored configuration. + * @returns {Promise} Indicates if the configuration file has been creted or modified. + * @example + * await this.configureContainer({}); + */ +async function configureContainer(basePathOnContainer, config) { + logger.info('Z2M Docker container is being configured...'); + + // Create configuration path (if not exists) + const configFilepath = path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH); + await fs.mkdir(path.dirname(configFilepath), { recursive: true }); + + // Check if config file not already exists + let configCreated = false; + try { + // eslint-disable-next-line no-bitwise + await fs.access(configFilepath, constants.R_OK | constants.W_OK); + logger.info('Z2M configuration file already exists.'); + } catch (e) { + logger.info('Writting default Z2M configuration...'); + await fs.writeFile(configFilepath, yaml.stringify(DEFAULT.CONFIGURATION_CONTENT)); + configCreated = true; + } + + // Check for changes + const fileContent = await fs.readFile(configFilepath); + const loadedConfig = yaml.parse(fileContent.toString()); + const { mqtt = {} } = loadedConfig; + + let configChanged = false; + if (mqtt.user !== config.mqttUsername || mqtt.password !== config.mqttPassword) { + mqtt.user = config.mqttUsername; + mqtt.password = config.mqttPassword; + loadedConfig.mqtt = mqtt; + configChanged = true; + } + + // Setup adapter + const adapterKey = Object.values(CONFIG_KEYS).find((configKey) => + ADAPTERS_BY_CONFIG_KEY[configKey].includes(config.z2mDongleName), + ); + const adapterSetup = adapterKey && adapterKey !== DEFAULT_KEY; + const { serial = {} } = loadedConfig; + + if (!adapterSetup && serial.adapter) { + delete loadedConfig.serial.adapter; + configChanged = true; + } else if (adapterSetup && serial.adapter !== adapterKey) { + loadedConfig.serial.adapter = adapterKey; + configChanged = true; + } + + if (configChanged) { + logger.info('Writting MQTT and USB adapter information to Z2M configuration...'); + await fs.writeFile(configFilepath, yaml.stringify(loadedConfig)); + } + + return configCreated || configChanged; +} + +module.exports = { + configureContainer, +}; diff --git a/server/services/node-red/lib/connect.js b/server/services/node-red/lib/connect.js new file mode 100644 index 0000000000..2df003ee24 --- /dev/null +++ b/server/services/node-red/lib/connect.js @@ -0,0 +1,73 @@ +const logger = require('../../../utils/logger'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); +const { DEFAULT } = require('./constants'); + +/** + * @description Initialize service with dependencies and connect to devices. + * @param {object} MqttParam - MQTT broker URL, Client MQTT username, Client MQTT password. + * @param {string} MqttParam.mqttUrl - MQTT URL. + * @param {string} MqttParam.mqttUsername - MQTT Username. + * @param {string} MqttParam.mqttPassword - MQTT Password. + * @returns {Promise} Resolve when connected. + * @example + * connect(); + */ +async function connect({ mqttUrl, mqttUsername, mqttPassword }) { + if (this.mqttClient) { + logger.info(`Disconnecting existing MQTT client...`); + this.mqttClient.end(); + this.mqttClient.removeAllListeners(); + this.mqttClient = null; + } + + if (this.mqttRunning) { + // Loads MQTT service + logger.info(`Connecting Gladys to ${mqttUrl} MQTT broker...`); + + this.mqttClient = this.mqttLibrary.connect(mqttUrl, { + username: mqttUsername, + password: mqttPassword, + reconnectPeriod: 5000, + clientId: `gladys-main-instance-${Math.floor(Math.random() * 1000000)}`, + }); + + this.mqttClient.on('connect', () => { + logger.info('Connected to MQTT container', mqttUrl); + DEFAULT.TOPICS.forEach((topic) => { + this.subscribe(topic, this.handleMqttMessage.bind(this)); + }); + this.gladysConnected = true; + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + }); + }); + + this.mqttClient.on('error', (err) => { + logger.warn(`Error while connecting to MQTT - ${err}`); + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.MQTT_ERROR, + payload: err, + }); + this.gladysConnected = false; + }); + + this.mqttClient.on('offline', () => { + logger.warn(`Disconnected from MQTT server`); + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.MQTT.ERROR, + payload: 'DISCONNECTED', + }); + this.gladysConnected = false; + }); + + this.mqttClient.on('message', (topic, message) => { + this.handleMqttMessage(topic, message.toString()); + }); + } else { + logger.warn("Can't connect Gladys cause MQTT not running !"); + } +} + +module.exports = { + connect, +}; diff --git a/server/services/node-red/lib/constants.js b/server/services/node-red/lib/constants.js new file mode 100644 index 0000000000..453013d2b4 --- /dev/null +++ b/server/services/node-red/lib/constants.js @@ -0,0 +1,60 @@ +const CONFIGURATION = { + Z2M_DRIVER_PATH: 'ZIGBEE2MQTT_DRIVER_PATH', + Z2M_BACKUP: 'Z2M_BACKUP', + ZIGBEE_DONGLE_NAME: 'ZIGBEE_DONGLE_NAME', + MQTT_URL_KEY: 'Z2M_MQTT_URL', + MQTT_URL_VALUE: 'mqtt://localhost:1884', + Z2M_MQTT_USERNAME_KEY: 'Z2M_MQTT_USERNAME', + Z2M_MQTT_USERNAME_VALUE: 'z2m', + Z2M_MQTT_PASSWORD_KEY: 'Z2M_MQTT_PASSWORD', + GLADYS_MQTT_USERNAME_KEY: 'GLADYS_MQTT_USERNAME', + GLADYS_MQTT_USERNAME_VALUE: 'gladys', + GLADYS_MQTT_PASSWORD_KEY: 'GLADYS_MQTT_PASSWORD', + DOCKER_MQTT_VERSION: 'DOCKER_MQTT_VERSION', // Variable to identify last version of MQTT docker file is installed + DOCKER_Z2M_VERSION: 'DOCKER_Z2M_VERSION', // Variable to identify last version of Z2M docker file is installed +}; + +const DEFAULT = { + DOCKER_NODE_RED_VERSION: '1', // Last version of NodeRed docker file, + + CONFIGURATION_PATH: 'zigbee2mqtt/z2m/configuration.yaml', + CONFIGURATION_CONTENT: { + homeassistant: false, + permit_join: false, + mqtt: { + base_topic: 'zigbee2mqtt', + server: 'mqtt://localhost:1884', + }, + serial: { + port: '/dev/ttyACM0', + }, + frontend: { + port: 8080, + }, + map_options: { + graphviz: { + colors: { + fill: { + enddevice: '#fff8ce', + coordinator: '#e04e5d', + router: '#4ea3e0', + }, + font: { + coordinator: '#ffffff', + router: '#ffffff', + enddevice: '#000000', + }, + line: { + active: '#009900', + inactive: '#994444', + }, + }, + }, + }, + }, +}; + +module.exports = { + CONFIGURATION, + DEFAULT, +}; diff --git a/server/services/node-red/lib/disconnect.js b/server/services/node-red/lib/disconnect.js new file mode 100644 index 0000000000..949d7f3712 --- /dev/null +++ b/server/services/node-red/lib/disconnect.js @@ -0,0 +1,62 @@ +const logger = require('../../../utils/logger'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); + +const mqttContainerDescriptor = require('../docker/gladys-z2m-mqtt-container.json'); +const zigbee2mqttContainerDescriptor = require('../docker/gladys-z2m-zigbee2mqtt-container.json'); + +/** + * @description Disconnect service from dependent containers. + * @example + * disconnect(); + */ +async function disconnect() { + let container; + + // Stop backup reccurent job + if (this.backupScheduledJob) { + this.backupScheduledJob.cancel(); + } + + // Disconnect from MQTT broker + if (this.mqttClient) { + logger.debug(`Disconnecting existing MQTT server...`); + this.mqttClient.end(); + this.mqttClient.removeAllListeners(); + this.mqttClient = null; + } else { + logger.debug('Not connected'); + } + this.gladysConnected = false; + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + }); + + // Stop MQTT container + let dockerContainer = await this.gladys.system.getContainers({ + all: true, + filters: { name: [mqttContainerDescriptor.name] }, + }); + [container] = dockerContainer; + await this.gladys.system.stopContainer(container.id); + this.mqttRunning = false; + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + }); + + // Stop zigbee2mqtt container + dockerContainer = await this.gladys.system.getContainers({ + all: true, + filters: { name: [zigbee2mqttContainerDescriptor.name] }, + }); + [container] = dockerContainer; + await this.gladys.system.stopContainer(container.id); + this.zigbee2mqttRunning = false; + this.zigbee2mqttConnected = false; + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + }); +} + +module.exports = { + disconnect, +}; diff --git a/server/services/node-red/lib/getConfiguration.js b/server/services/node-red/lib/getConfiguration.js new file mode 100644 index 0000000000..8ec1a2a678 --- /dev/null +++ b/server/services/node-red/lib/getConfiguration.js @@ -0,0 +1,47 @@ +const { SYSTEM_VARIABLE_NAMES } = require('../../../utils/constants'); +const logger = require('../../../utils/logger'); +const { CONFIGURATION } = require('./constants'); + +/** + * @description Get Z2M configuration. + * @returns {Promise} Current Z2M network configuration. + * @example + * const config = await z2m.getConfiguration(); + */ +async function getConfiguration() { + logger.debug('Zigbee2mqtt: loading stored configuration...'); + + // Load z2m parameters + const z2mDriverPath = await this.gladys.variable.getValue(CONFIGURATION.Z2M_DRIVER_PATH, this.serviceId); + const z2mDongleName = await this.gladys.variable.getValue(CONFIGURATION.ZIGBEE_DONGLE_NAME, this.serviceId); + const z2mMqttUsername = await this.gladys.variable.getValue(CONFIGURATION.Z2M_MQTT_USERNAME_KEY, this.serviceId); + const z2mMqttPassword = await this.gladys.variable.getValue(CONFIGURATION.Z2M_MQTT_PASSWORD_KEY, this.serviceId); + + // Load MQTT parameters + const mqttUrl = await this.gladys.variable.getValue(CONFIGURATION.MQTT_URL_KEY, this.serviceId); + const mqttUsername = await this.gladys.variable.getValue(CONFIGURATION.GLADYS_MQTT_USERNAME_KEY, this.serviceId); + const mqttPassword = await this.gladys.variable.getValue(CONFIGURATION.GLADYS_MQTT_PASSWORD_KEY, this.serviceId); + + // Load version parameters + const dockerMqttVersion = await this.gladys.variable.getValue(CONFIGURATION.DOCKER_MQTT_VERSION, this.serviceId); + const dockerZ2mVersion = await this.gladys.variable.getValue(CONFIGURATION.DOCKER_Z2M_VERSION, this.serviceId); + // Gladys params + const timezone = await this.gladys.variable.getValue(SYSTEM_VARIABLE_NAMES.TIMEZONE); + + return { + z2mDriverPath, + z2mDongleName, + z2mMqttUsername, + z2mMqttPassword, + mqttUrl, + mqttUsername, + mqttPassword, + dockerMqttVersion, + dockerZ2mVersion, + timezone, + }; +} + +module.exports = { + getConfiguration, +}; diff --git a/server/services/node-red/lib/index.js b/server/services/node-red/lib/index.js new file mode 100644 index 0000000000..e2790bd36d --- /dev/null +++ b/server/services/node-red/lib/index.js @@ -0,0 +1,61 @@ +const { init } = require('./init'); +const { getConfiguration } = require('./getConfiguration'); +const { saveConfiguration } = require('./saveConfiguration'); +const { installContainer } = require('./installContainer'); + +const { connect } = require('./connect'); +const { disconnect } = require('./disconnect'); +const { status } = require('./status'); +const { isEnabled } = require('./isEnabled'); +const { checkForContainerUpdates } = require('./checkForContainerUpdates'); + +const { configureContainer } = require('./configureContainer'); +const { setup } = require('./setup'); +const { saveZ2mBackup } = require('./saveZ2mBackup'); +const { restoreZ2mBackup } = require('./restoreZ2mBackup'); +const { backup } = require('./backup'); +const { JOB_TYPES } = require('../../../utils/constants'); + +/** + * @description Add ability to connect to Node-red. + * @param {object} gladys - Gladys instance. + * @param {string} serviceId - UUID of the service in DB. + * @example + * const nodeRedManager = new NodeRedManager(gladys, serviceId); + */ +const NodeRedManager = function NodeRedManager(gladys, serviceId) { + this.gladys = gladys; + this.serviceId = serviceId; + + this.nodeRedExist = false; + this.nodeRedRunning = false; + this.nodeRedConnected = false; + + this.gladysConnected = false; + this.networkModeValid = true; + this.dockerBased = true; + + this.containerRestartWaitTimeInMs = 5 * 1000; + + this.backup = gladys.job.wrapper(JOB_TYPES.SERVICE_NODE_RED_BACKUP, this.backup.bind(this)); + this.backupJob = {}; + this.backupScheduledJob = null; +}; + +NodeRedManager.prototype.init = init; +NodeRedManager.prototype.getConfiguration = getConfiguration; +NodeRedManager.prototype.saveConfiguration = saveConfiguration; +NodeRedManager.prototype.installContainer = installContainer; + +NodeRedManager.prototype.connect = connect; +NodeRedManager.prototype.disconnect = disconnect; +NodeRedManager.prototype.status = status; +NodeRedManager.prototype.isEnabled = isEnabled; +NodeRedManager.prototype.checkForContainerUpdates = checkForContainerUpdates; +NodeRedManager.prototype.configureContainer = configureContainer; +NodeRedManager.prototype.setup = setup; +NodeRedManager.prototype.saveZ2mBackup = saveZ2mBackup; +NodeRedManager.prototype.restoreZ2mBackup = restoreZ2mBackup; +NodeRedManager.prototype.backup = backup; + +module.exports = NodeRedManager; diff --git a/server/services/node-red/lib/init.js b/server/services/node-red/lib/init.js new file mode 100644 index 0000000000..e5a37d0f50 --- /dev/null +++ b/server/services/node-red/lib/init.js @@ -0,0 +1,48 @@ +const logger = require('../../../utils/logger'); +const { CONFIGURATION } = require('./constants'); +const { generate } = require('../../../utils/password'); +const { PlatformNotCompatible } = require('../../../utils/coreErrors'); + +/** + * @description Prepares service and starts connection with broker if needed. + * @returns {Promise} Resolve when init finished. + * @example + * await z2m.init(); + */ +async function init() { + const dockerBased = await this.gladys.system.isDocker(); + if (!dockerBased) { + this.dockerBased = false; + throw new PlatformNotCompatible('SYSTEM_NOT_RUNNING_DOCKER'); + } + + const networkMode = await this.gladys.system.getNetworkMode(); + if (networkMode !== 'host') { + this.networkModeValid = false; + throw new PlatformNotCompatible('DOCKER_BAD_NETWORK'); + } + + // Load stored configuration + const configuration = await this.getConfiguration(); + + logger.debug('NodeRed: installing and starting required docker containers...'); + await this.checkForContainerUpdates(configuration); + await this.installMqttContainer(configuration); + await this.installZ2mContainer(configuration); + + if (this.isEnabled()) { + await this.connect(configuration); + + // Schedule reccurent job if not already scheduled + if (!this.backupScheduledJob) { + this.backupScheduledJob = this.gladys.scheduler.scheduleJob('0 0 23 * * *', () => this.backup()); + } + } + + + return null; +} + +module.exports = { + init, +}; diff --git a/server/services/node-red/lib/installContainer.js b/server/services/node-red/lib/installContainer.js new file mode 100644 index 0000000000..5571a00650 --- /dev/null +++ b/server/services/node-red/lib/installContainer.js @@ -0,0 +1,89 @@ +const cloneDeep = require('lodash.clonedeep'); +const { promisify } = require('util'); + +const logger = require('../../../utils/logger'); +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); + +const containerDescriptor = require('../docker/gladys-node-red-container.json'); + +const sleep = promisify(setTimeout); + +/** + * @description Install and starts Node-red container. + * @param {object} config - Service configuration properties. + * @example + * await nodeRed.installContainer(config); + */ +async function installContainer(config) { + + let dockerContainers = await this.gladys.system.getContainers({ + all: true, + filters: { name: [containerDescriptor.name] }, + }); + let [container] = dockerContainers; + + const { basePathOnContainer, basePathOnHost } = await this.gladys.system.getGladysBasePath(); + const containerPath = `${basePathOnHost}/node-red`; + if (dockerContainers.length === 0) { + // Restore backup only in case of new installation + // await this.restoreZ2mBackup(containerPath); + // TODO add restore Backup + + try { + logger.info('Nodered: is being installed as Docker container...'); + logger.info(`Pulling ${containerDescriptor.Image} image...`); + await this.gladys.system.pull(containerDescriptor.Image); + + const containerDescriptorToMutate = cloneDeep(containerDescriptor); + + logger.info(`Creation of container...`); + const containerLog = await this.gladys.system.createContainer(containerDescriptorToMutate); + logger.trace(containerLog); + logger.info('NodeRed: successfully installed and configured as Docker container'); + this.zigbee2mqttExist = true; + } catch (e) { + this.zigbee2mqttExist = false; + logger.error('Zigbee2mqtt failed to install as Docker container:', e); + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + }); + throw e; + } + } + + const configChanged = await this.configureContainer(basePathOnContainer, config); + + try { + dockerContainers = await this.gladys.system.getContainers({ + all: true, + filters: { name: [containerDescriptor.name] }, + }); + [container] = dockerContainers; + + // Check if we need to restart the container (container is not running / config changed) + if (container.state !== 'running' || configChanged) { + logger.info('Zigbee2mqtt container is (re)starting...'); + await this.gladys.system.restartContainer(container.id); + // wait a few seconds for the container to restart + await sleep(this.containerRestartWaitTimeInMs); + } + + logger.info('Zigbee2mqtt container successfully started'); + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + }); + this.zigbee2mqttRunning = true; + this.zigbee2mqttExist = true; + } catch (e) { + logger.error('Zigbee2mqtt container failed to start:', e); + this.zigbee2mqttRunning = false; + this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + }); + throw e; + } +} + +module.exports = { + installContainer, +}; diff --git a/server/services/node-red/lib/isEnabled.js b/server/services/node-red/lib/isEnabled.js new file mode 100644 index 0000000000..11f721f2be --- /dev/null +++ b/server/services/node-red/lib/isEnabled.js @@ -0,0 +1,13 @@ +/** + * @description Checks if z2m is ready to use. + * @returns {boolean} Is the z2m environment ready to use? + * @example + * z2m.isEnabled(); + */ +function isEnabled() { + return this.mqttRunning && this.zigbee2mqttRunning && this.usbConfigured; +} + +module.exports = { + isEnabled, +}; diff --git a/server/services/node-red/lib/restoreZ2mBackup.js b/server/services/node-red/lib/restoreZ2mBackup.js new file mode 100644 index 0000000000..a8c8de5417 --- /dev/null +++ b/server/services/node-red/lib/restoreZ2mBackup.js @@ -0,0 +1,48 @@ +const fsPromises = require('fs/promises'); +const JSZip = require('jszip'); +const path = require('path'); + +const logger = require('../../../utils/logger'); +const { CONFIGURATION } = require('./constants'); + +/** + * @description Restore z2m backup from database. + * @param {string} containerPath - Zigbee2MQTT configuration directory. + * @returns {Promise} Empty promise. + * @example + * await z2m.restoreZ2mBackup(configPath); + */ +async function restoreZ2mBackup(containerPath) { + await fsPromises.mkdir(containerPath, { recursive: true }); + // Check if configuration is already available + const z2mFiles = await fsPromises.readdir(containerPath); + if (z2mFiles.includes('configuration.yaml')) { + // Configuration is present, do not restore backup + logger.debug('zigbee2mqtt configuration already here, skip restore backup'); + return; + } + + // Check if backup is stored + logger.info('Zigbee2mqtt: loading z2m backup...'); + const z2mBackup = await this.gladys.variable.getValue(CONFIGURATION.Z2M_BACKUP, this.serviceId); + if (z2mBackup) { + logger.info('Restoring zigbee2mqtt configuration...'); + // Stored z2m backup is a base64 zip file + // 1. Decoding base64 content + const zip = new JSZip(); + const { files } = await zip.loadAsync(z2mBackup, { base64: true }); + + await Promise.all( + Object.values(files).map(async (file) => { + const content = await file.async('arraybuffer'); + return fsPromises.writeFile(path.join(containerPath, file.name), Buffer.from(content)); + }), + ); + } else { + logger.info('No zigbee2mqtt backup avaiable'); + } +} + +module.exports = { + restoreZ2mBackup, +}; diff --git a/server/services/node-red/lib/saveConfiguration.js b/server/services/node-red/lib/saveConfiguration.js new file mode 100644 index 0000000000..3686ad8e2b --- /dev/null +++ b/server/services/node-red/lib/saveConfiguration.js @@ -0,0 +1,45 @@ +const logger = require('../../../utils/logger'); +const { CONFIGURATION } = require('./constants'); + +const saveOrDestroy = async (variableManager, key, value, serviceId) => { + if (value === undefined || value === null) { + await variableManager.destroy(key, serviceId); + } else { + await variableManager.setValue(key, value, serviceId); + } +}; + +/** + * @description Save Z2M configuration. + * @param {object} config - Z2M service configuration. + * @returns {Promise} Current MQTT network configuration. + * @example + * await z2m.saveConfiguration(config); + */ +async function saveConfiguration(config) { + logger.debug('Zigbee2mqtt: storing configuration...'); + + const keyValueMap = { + [CONFIGURATION.Z2M_DRIVER_PATH]: config.z2mDriverPath, + [CONFIGURATION.ZIGBEE_DONGLE_NAME]: config.z2mDongleName, + [CONFIGURATION.Z2M_MQTT_USERNAME_KEY]: config.z2mMqttUsername, + [CONFIGURATION.Z2M_MQTT_PASSWORD_KEY]: config.z2mMqttPassword, + [CONFIGURATION.MQTT_URL_KEY]: config.mqttUrl, + [CONFIGURATION.GLADYS_MQTT_USERNAME_KEY]: config.mqttUsername, + [CONFIGURATION.GLADYS_MQTT_PASSWORD_KEY]: config.mqttPassword, + [CONFIGURATION.DOCKER_MQTT_VERSION]: config.dockerMqttVersion, + [CONFIGURATION.DOCKER_Z2M_VERSION]: config.dockerZ2mVersion, + }; + + const variableKeys = Object.keys(keyValueMap); + + await Promise.all( + variableKeys.map((key) => saveOrDestroy(this.gladys.variable, key, keyValueMap[key], this.serviceId)), + ); + + logger.debug('Zigbee2mqtt: configuration stored'); +} + +module.exports = { + saveConfiguration, +}; diff --git a/server/services/node-red/lib/saveZ2mBackup.js b/server/services/node-red/lib/saveZ2mBackup.js new file mode 100644 index 0000000000..48d78db63c --- /dev/null +++ b/server/services/node-red/lib/saveZ2mBackup.js @@ -0,0 +1,45 @@ +const logger = require('../../../utils/logger'); +const { CONFIGURATION } = require('./constants'); + +/** + * @description Save Z2M backup. + * @param {object} payload - Z2M MQTT backup payload. + * @returns {Promise} The status of backup JOB, or null. + * @example + * await z2m.saveZ2mBackup({ status: 'ok', data: { zip: 'base64_backup' }}); + */ +async function saveZ2mBackup(payload) { + logger.info('Zigbee2mqtt: storing backup...'); + + const { jobId, resolve: jobResolver, reject: jobRejecter } = this.backupJob; + if (jobId) { + await this.gladys.job.updateProgress(jobId, 60); + } + + const { status, data } = payload; + const backupValid = status === 'ok'; + + if (backupValid) { + await this.gladys.variable.setValue(CONFIGURATION.Z2M_BACKUP, data.zip, this.serviceId); + logger.info('Zigbee2mqtt: backup stored'); + + if (jobId) { + await this.gladys.job.updateProgress(jobId, 100); + } + } else { + logger.error('zigbee2mqtt backup is not ok'); + } + + if (backupValid && jobResolver) { + return jobResolver(); + } + if (!backupValid && jobRejecter) { + return jobRejecter(); + } + + return null; +} + +module.exports = { + saveZ2mBackup, +}; diff --git a/server/services/node-red/lib/setup.js b/server/services/node-red/lib/setup.js new file mode 100644 index 0000000000..3acc132b36 --- /dev/null +++ b/server/services/node-red/lib/setup.js @@ -0,0 +1,22 @@ +const { CONFIGURATION } = require('./constants'); + +/** + * @description Setup Zigbee2mqtt USB device. + * @param {object} usbConfig - Configuration about USB Zigbee dongle. + * @example + * await this.setup({ ZIGBEE2MQTT_DRIVER_PATH: '/dev/tty0', ZIGBEE_DONGLE_NAME: 'zzh' }); + */ +async function setup(usbConfig) { + const z2mDriverPath = usbConfig[CONFIGURATION.Z2M_DRIVER_PATH]; + const z2mDongleName = usbConfig[CONFIGURATION.ZIGBEE_DONGLE_NAME]; + + await this.gladys.variable.setValue(CONFIGURATION.Z2M_DRIVER_PATH, z2mDriverPath, this.serviceId); + await this.gladys.variable.setValue(CONFIGURATION.ZIGBEE_DONGLE_NAME, z2mDongleName, this.serviceId); + + // Reload z2m container with new USB configuration + await this.init(); +} + +module.exports = { + setup, +}; diff --git a/server/services/node-red/lib/status.js b/server/services/node-red/lib/status.js new file mode 100644 index 0000000000..a59d0c7e1f --- /dev/null +++ b/server/services/node-red/lib/status.js @@ -0,0 +1,26 @@ +/** + * @description Get Zigbee2mqtt status. + * @returns {object} Current Zigbee2mqtt containers and configuration status. + * @example + * status(); + */ +function status() { + const z2mEnabled = this.isEnabled(); + const zigbee2mqttStatus = { + usbConfigured: this.usbConfigured, + mqttExist: this.mqttExist, + mqttRunning: this.mqttRunning, + zigbee2mqttExist: this.zigbee2mqttExist, + zigbee2mqttRunning: this.zigbee2mqttRunning, + gladysConnected: this.gladysConnected, + zigbee2mqttConnected: this.zigbee2mqttConnected, + z2mEnabled, + dockerBased: this.dockerBased, + networkModeValid: this.networkModeValid, + }; + return zigbee2mqttStatus; +} + +module.exports = { + status, +}; diff --git a/server/services/node-red/package-lock.json b/server/services/node-red/package-lock.json new file mode 100644 index 0000000000..12f0cd3421 --- /dev/null +++ b/server/services/node-red/package-lock.json @@ -0,0 +1,1608 @@ +{ + "name": "gladys-zigbee2mqtt", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "gladys-zigbee2mqtt", + "version": "1.0.0", + "cpu": [ + "x64", + "arm", + "arm64" + ], + "os": [ + "darwin", + "linux", + "win32" + ], + "dependencies": { + "fs-extra": "^11.1.1", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "mqtt": "^4.2.6", + "yaml": "^2.2.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "node_modules/callback-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/callback-stream/-/callback-stream-1.1.0.tgz", + "integrity": "sha1-RwGlEmbwbgbqpx/BcjOCLYdfSQg=", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "> 1.0.0 < 3.0.0" + } + }, + "node_modules/callback-stream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/callback-stream/node_modules/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==" + }, + "node_modules/callback-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "dependencies": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/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=" + }, + "node_modules/debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexify/node_modules/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==" + }, + "node_modules/duplexify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "dependencies": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/glob-stream/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/glob-stream/node_modules/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==" + }, + "node_modules/glob-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/help-me": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz", + "integrity": "sha1-jy1QjQYAtKRW2i8IZVbn5cBWo8Y=", + "dependencies": { + "callback-stream": "^1.0.2", + "glob-stream": "^6.1.0", + "through2": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==", + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "node_modules/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=" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/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==" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "node_modules/mqtt": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.2.6.tgz", + "integrity": "sha512-GpxVObyOzL0CGPBqo6B04GinN8JLk12NRYAIkYvARd9ZCoJKevvOyCaWK6bdK/kFSDj3LPDnCsJbezzNlsi87Q==", + "dependencies": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "help-me": "^1.0.1", + "inherits": "^2.0.3", + "minimist": "^1.2.5", + "mqtt-packet": "^6.6.0", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "split2": "^3.1.0", + "ws": "^7.3.1", + "xtend": "^4.0.2" + }, + "bin": { + "mqtt": "bin/mqtt.js", + "mqtt_pub": "bin/pub.js", + "mqtt_sub": "bin/sub.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.9.0.tgz", + "integrity": "sha512-cngFSAXWSl5XHKJYUQiYQjtp75zhf1vygY00NnJdhQoXOH2v3aizmaaMIHI5n1N/TJEHSAbHryQhFr3gJ9VNvA==", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "dependencies": { + "readable-stream": "^2.0.1" + } + }, + "node_modules/ordered-read-streams/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/ordered-read-streams/node_modules/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==" + }, + "node_modules/ordered-read-streams/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" + }, + "node_modules/remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "dependencies": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "node_modules/through2/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/through2/node_modules/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==" + }, + "node_modules/through2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "dependencies": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "node_modules/unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "dependencies": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "node_modules/ws": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "engines": { + "node": ">= 14" + } + } + }, + "dependencies": { + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + }, + "callback-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/callback-stream/-/callback-stream-1.1.0.tgz", + "integrity": "sha1-RwGlEmbwbgbqpx/BcjOCLYdfSQg=", + "requires": { + "inherits": "^2.0.1", + "readable-stream": "> 1.0.0 < 3.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "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==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "requires": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "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=" + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "requires": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "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==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "fs-extra": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", + "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", + "requires": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "glob-stream": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", + "integrity": "sha1-cEXJlBOz65SIjYOrRtC0BMx73eQ=", + "requires": { + "extend": "^3.0.0", + "glob": "^7.1.1", + "glob-parent": "^3.1.0", + "is-negated-glob": "^1.0.0", + "ordered-read-streams": "^1.0.0", + "pumpify": "^1.3.5", + "readable-stream": "^2.1.5", + "remove-trailing-separator": "^1.0.1", + "to-absolute-glob": "^2.0.0", + "unique-stream": "^2.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "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==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "help-me": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz", + "integrity": "sha1-jy1QjQYAtKRW2i8IZVbn5cBWo8Y=", + "requires": { + "callback-stream": "^1.0.2", + "glob-stream": "^6.1.0", + "through2": "^2.0.1", + "xtend": "^4.0.0" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "requires": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + }, + "is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", + "requires": { + "is-extglob": "^2.1.0" + } + }, + "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-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "requires": { + "unc-path-regex": "^0.1.2" + } + }, + "is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "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=" + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "requires": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "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==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=" + }, + "lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "requires": { + "immediate": "~3.0.5" + } + }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + }, + "mqtt": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.2.6.tgz", + "integrity": "sha512-GpxVObyOzL0CGPBqo6B04GinN8JLk12NRYAIkYvARd9ZCoJKevvOyCaWK6bdK/kFSDj3LPDnCsJbezzNlsi87Q==", + "requires": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "help-me": "^1.0.1", + "inherits": "^2.0.3", + "minimist": "^1.2.5", + "mqtt-packet": "^6.6.0", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "split2": "^3.1.0", + "ws": "^7.3.1", + "xtend": "^4.0.2" + } + }, + "mqtt-packet": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.9.0.tgz", + "integrity": "sha512-cngFSAXWSl5XHKJYUQiYQjtp75zhf1vygY00NnJdhQoXOH2v3aizmaaMIHI5n1N/TJEHSAbHryQhFr3gJ9VNvA==", + "requires": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "requires": { + "wrappy": "1" + } + }, + "ordered-read-streams": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ordered-read-streams/-/ordered-read-streams-1.0.1.tgz", + "integrity": "sha1-d8DLN8QVJdZBZtmQ/61+xqDhNj4=", + "requires": { + "readable-stream": "^2.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "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==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" + }, + "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==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "requires": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + }, + "dependencies": { + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + } + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" + }, + "remove-trailing-separator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "requires": { + "readable-stream": "^3.0.0" + } + }, + "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_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "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==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "through2-filter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz", + "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==", + "requires": { + "through2": "~2.0.0", + "xtend": "~4.0.0" + } + }, + "to-absolute-glob": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/to-absolute-glob/-/to-absolute-glob-2.0.2.tgz", + "integrity": "sha1-GGX0PZ50sIItufFFt4z/fQ98hJs=", + "requires": { + "is-absolute": "^1.0.0", + "is-negated-glob": "^1.0.0" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, + "unc-path-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", + "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=" + }, + "unique-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz", + "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==", + "requires": { + "json-stable-stringify-without-jsonify": "^1.0.1", + "through2-filter": "^3.0.0" + } + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + }, + "ws": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", + "requires": {} + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==" + } + } +} diff --git a/server/services/node-red/package.json b/server/services/node-red/package.json new file mode 100644 index 0000000000..da85deed6a --- /dev/null +++ b/server/services/node-red/package.json @@ -0,0 +1,22 @@ +{ + "name": "gladys-node-red", + "version": "1.0.0", + "main": "index.js", + "os": [ + "darwin", + "linux", + "win32" + ], + "cpu": [ + "x64", + "arm", + "arm64" + ], + "dependencies": { + "fs-extra": "^11.1.1", + "jszip": "^3.10.1", + "lodash.clonedeep": "^4.5.0", + "mqtt": "^4.2.6", + "yaml": "^2.2.2" + } +} diff --git a/server/utils/constants.js b/server/utils/constants.js index 4cfef7dbb7..1b7e194261 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -938,6 +938,7 @@ const JOB_TYPES = { DEVICE_STATES_PURGE_SINGLE_FEATURE: 'device-state-purge-single-feature', VACUUM: 'vacuum', SERVICE_ZIGBEE2MQTT_BACKUP: 'service-zigbee2mqtt-backup', + SERVICE_NODE_RED_BACKUP: 'service-node-red-backup', }; const JOB_STATUS = { From 2ae6f4b7b0c23fb244af844b8680b18416514f2d Mon Sep 17 00:00:00 2001 From: callemand Date: Fri, 2 Jun 2023 07:41:08 +0200 Subject: [PATCH 02/32] Add setup screen --- .../assets/integrations/cover/node-red.jpg | Bin 0 -> 39880 bytes .../integrations/logos/logo_node-red.png | Bin 0 -> 22768 bytes front/src/components/app.jsx | 6 + front/src/config/i18n/fr.json | 46 +++++ front/src/config/integrations/devices.json | 5 + .../integration/all/node-red/NodeRedPage.js | 50 +++++ .../all/node-red/setup-page/CheckStatus.js | 42 ++++ .../all/node-red/setup-page/SetupTab.jsx | 195 ++++++++++++++++++ .../all/node-red/setup-page/actions.js | 91 ++++++++ .../all/node-red/setup-page/index.js | 37 ++++ .../all/node-red/setup-page/style.css | 23 +++ .../all/zigbee2mqtt/commons/CheckStatus.js | 3 +- server/lib/system/system.getNetworkMode.js | 1 + server/lib/system/system.isDocker.js | 2 + server/services/index.js | 1 + .../node-red/api/node-red.controller.js | 4 +- .../docker/gladys-node-red-container.json | 12 +- server/services/node-red/lib/backup.js | 8 +- .../node-red/lib/checkForContainerUpdates.js | 4 +- .../node-red/lib/configureContainer.js | 32 +-- server/services/node-red/lib/connect.js | 73 ------- server/services/node-red/lib/constants.js | 47 +---- server/services/node-red/lib/disconnect.js | 37 +--- .../services/node-red/lib/getConfiguration.js | 32 +-- server/services/node-red/lib/index.js | 23 +-- server/services/node-red/lib/init.js | 5 +- .../services/node-red/lib/installContainer.js | 43 ++-- server/services/node-red/lib/isEnabled.js | 8 +- .../node-red/lib/saveConfiguration.js | 22 +- server/services/node-red/lib/setup.js | 22 -- server/services/node-red/lib/status.js | 17 +- server/utils/constants.js | 4 + 32 files changed, 618 insertions(+), 277 deletions(-) create mode 100644 front/src/assets/integrations/cover/node-red.jpg create mode 100644 front/src/assets/integrations/logos/logo_node-red.png create mode 100644 front/src/routes/integration/all/node-red/NodeRedPage.js create mode 100644 front/src/routes/integration/all/node-red/setup-page/CheckStatus.js create mode 100644 front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx create mode 100644 front/src/routes/integration/all/node-red/setup-page/actions.js create mode 100644 front/src/routes/integration/all/node-red/setup-page/index.js create mode 100644 front/src/routes/integration/all/node-red/setup-page/style.css delete mode 100644 server/services/node-red/lib/connect.js delete mode 100644 server/services/node-red/lib/setup.js diff --git a/front/src/assets/integrations/cover/node-red.jpg b/front/src/assets/integrations/cover/node-red.jpg new file mode 100644 index 0000000000000000000000000000000000000000..631b9bdbb003c82cd6ff610eb789d8dd384524cf GIT binary patch literal 39880 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 literal 0 HcmV?d00001 diff --git a/front/src/assets/integrations/logos/logo_node-red.png b/front/src/assets/integrations/logos/logo_node-red.png new file mode 100644 index 0000000000000000000000000000000000000000..c9b1afb1f545a5d46c1c55d8cd73a49408f44ce0 GIT binary patch literal 22768 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 + + + diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index e2a5b3316c..f3f2131869 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -733,6 +733,52 @@ "saveError": "Une erreur s'est produite lors de la sauvegarde.", "documentation": "Documentation Zigbee2mqtt" }, + "node-red": { + "title": "Node-red", + "description": "Contrôlez vos appareils via Node-red TODO.", + "setupTab": "Configuration", + "documentation": "Documentation Node-red", + "status": { + "notInstalled": "Le server Node-red n'a pas pu être installé.", + "notRunning": "Le server Node-red n'a pas démarré.", + "running": "Node-red démarré avec succès.", + + "zigbee2mqttNotInstalled": "Zigbee2mqtt n'a pas pu être installé.", + "zigbee2mqttNotRunning": "Zigbee2mqtt n'a pas démarré.", + "gladysNotConnected": "Gladys n'a pas réussi à se connecter au broker MQTT", + "zigbee2mqttNotConnected": "Zigbee2mqtt n'a pas réussi à se connecter au broker MQTT", + "settingsPageLink": "la page de paramétrage du dongle USB", + "setupPageLink": "la page de configuration de Zigbee2mqtt" + }, + "setup": { + "title": "Configuration du service Node-red", + "description": "Ce service utilise un container Docker. Activer Node-red pour déployer ce container.\nPour en savoir plus, rendez-vous sur la page de documentation Node-red", + "error": "Une erreur s'est produite au démarrage du service Node-red.", + "enableLabel": "Activation du service Node-red", + "nonDockerEnv": "Gladys ne s'exécute actuellement pas dans Docker, il est impossible d'activer Node-red depuis Gladys.", + "invalidDockerNetwork": "Gladys est basée sur une installation personalisée, pour installer Node-red depuis cette page, le conteneur Docker de Gladys doit être démarré avec l'option \"network=host\".", + "enableNodeRed": "Activer Node-red", + "serviceStatus": "Etat du service Node-red", + "containersStatus": "Conteneurs liés à Node-red", + "status": "Status", + "node-red": "Node-red", + "gladys": "Gladys" + }, + + "noDeviceFound": "Auncun appareil zigbee2mqtt n'a encore été ajouté.", + "nameLabel": "Nom", + "namePlaceholder": "Donner un nom à l'appareil", + "roomLabel": "Pièce", + "topicLabel": "Sujet MQTT", + "topicPlaceholder": "%topic% zigbee2mqtt MQTT value", + "featuresLabel": "Fonctionnalités", + "saveButton": "Sauvegarder", + "deleteButton": "Supprimer", + "updateButton": "Mettre à jour", + "alreadyExistsButton": "Déjà créé", + "saveError": "Une erreur s'est produite lors de la sauvegarde." + }, + "googleHome": { "title": "Google Home", "description": "Contrôlez vos appareils Gladys à la voix dans Google Home", diff --git a/front/src/config/integrations/devices.json b/front/src/config/integrations/devices.json index 0fc960da62..95915d48de 100644 --- a/front/src/config/integrations/devices.json +++ b/front/src/config/integrations/devices.json @@ -68,5 +68,10 @@ "key": "melcloud", "link": "melcloud", "img": "/assets/integrations/cover/melcloud.jpg" + }, + { + "key": "nodeRed", + "link": "node-red", + "img": "/assets/integrations/cover/node-red.jpg" } ] diff --git a/front/src/routes/integration/all/node-red/NodeRedPage.js b/front/src/routes/integration/all/node-red/NodeRedPage.js new file mode 100644 index 0000000000..12668aa1c9 --- /dev/null +++ b/front/src/routes/integration/all/node-red/NodeRedPage.js @@ -0,0 +1,50 @@ +import { Text } from 'preact-i18n'; +import { Link } from 'preact-router/match'; +import DeviceConfigurationLink from '../../../../components/documentation/DeviceConfigurationLink'; + +const NodeRedPage = ({ children, user }) => ( +
+
+
+
+
+
+

+ +

+
+
+ + + + + + + + + + + + + +
+
+
+ +
{children}
+
+
+
+
+
+); + +export default NodeRedPage; diff --git a/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js b/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js new file mode 100644 index 0000000000..d60d8b0875 --- /dev/null +++ b/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js @@ -0,0 +1,42 @@ +import { Component } from 'preact'; +import { Link } from 'preact-router/match'; +import { connect } from 'unistore/preact'; +import actions from './actions'; +import { Text } from 'preact-i18n'; + +class CheckStatus extends Component { + componentWillMount() { + this.props.checkStatus(); + } + + renderError(messageKey) { + return ( +
+ +
+ ) + } + + render(props, {}) { + console.log('CheckStatus', props); + + if(props.nodeRedEnabled) { + if (!props.nodeRedExist) { + return this.renderError('integration.node-red.status.notInstalled'); + } else if (!props.nodeRedRunning) { + return this.renderError('integration.node-red.status.notRunning'); + } + return ( +

+ +

+ ) + } + return null; + } +} + +export default connect( + 'user,session,nodeRedExist,nodeRedRunning,nodeRedEnabled', + actions +)(CheckStatus); diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx new file mode 100644 index 0000000000..06a81a71e5 --- /dev/null +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -0,0 +1,195 @@ +import { Component } from 'preact'; +import { Text, MarkupText } from 'preact-i18n'; +import { RequestStatus } from '../../../../../utils/consts'; +import CheckStatus from './CheckStatus.js'; +import classNames from 'classnames/bind'; +import style from './style.css'; + +let cx = classNames.bind(style); + +class SetupTab extends Component { + toggle = () => { + let checked = this.props.nodeRedEnabled; + checked = !checked; + + if (checked) { + this.props.startContainer(); + } else { + this.props.stopContainer(); + } + + this.props.nodeRedEnabled = checked; + }; + + render(props, {}) { + console.log(props); + return ( +
+
+

+ +

+
+
+

+ +

+ {props.nodeRedStatus === RequestStatus.Error && ( +

+ +

+ )} + + + +
+ + +
+
+

+ +

+
+
+
+ + + + + + + + + + + {props.nodeRedEnabled && ( + + )} + + + + + + + +
+ + + {props.nodeRedEnabled && 'Node-red'}
+ {`Gladys`} + +
+ +
+
+ {props.nodeRedEnabled && ( + {`Node-red`} + )} +
+
+ +
+
+ + {props.nodeRedRunning && ( + + + + )} +
+
+
+
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ + + {props.nodeRedRunning && ( + + + + )} +
+
+
+
+
+ ); + } +} + +export default SetupTab; diff --git a/front/src/routes/integration/all/node-red/setup-page/actions.js b/front/src/routes/integration/all/node-red/setup-page/actions.js new file mode 100644 index 0000000000..f96ed02105 --- /dev/null +++ b/front/src/routes/integration/all/node-red/setup-page/actions.js @@ -0,0 +1,91 @@ +import createActionsIntegration from '../../../../../actions/integration'; +import { RequestStatus } from '../../../../../utils/consts'; +import dayjs from 'dayjs'; +import relativeTime from 'dayjs/plugin/relativeTime'; +import get from 'get-value'; + +dayjs.extend(relativeTime); + +const createActions = store => { + const integrationActions = createActionsIntegration(store); + const actions = { + async checkStatus(state) { + let nodeRedStatus = { + nodeRedExist: false, + nodeRedRunning: false, + nodeRedEnabled: false, + dockerBased: false, + networkModeValid: false, + }; + try { + nodeRedStatus = await state.httpClient.get('/api/v1/service/node-red/status'); + } finally { + store.setState({ + nodeRedExist: nodeRedStatus.nodeRedExist, + nodeRedRunning: nodeRedStatus.nodeRedRunning, + nodeRedEnabled: nodeRedStatus.nodeRedEnabled, + dockerBased: nodeRedStatus.dockerBased, + networkModeValid: nodeRedStatus.networkModeValid, + }); + } + }, + + async startContainer(state) { + let nodeRedEnabled = true; + let error = false; + + store.setState({ + nodeRedEnabled, + nodeRedStatus: RequestStatus.Getting + }); + + await state.httpClient.post('/api/v1/service/node-red/variable/NODERED_ENABLED', { + value: nodeRedEnabled + }); + + try { + await state.httpClient.post('/api/v1/service/node-red/connect'); + } catch (e) { + error = error | get(e, 'response.status'); + } + + if (error) { + store.setState({ + nodeRedStatus: RequestStatus.Error, + nodeRedEnabled: false + }); + } else { + store.setState({ + nodeRedStatus: RequestStatus.Success, + nodeRedEnabled: true + }); + } + + await this.checkStatus(); + }, + + async stopContainer(state) { + let error = false; + try { + await state.httpClient.post('/api/v1/service/node-red/disconnect'); + } catch (e) { + error = error | get(e, 'response.status'); + } + + if (error) { + store.setState({ + nodeRedStatus: RequestStatus.Error + }); + } else { + store.setState({ + nodeRedStatus: RequestStatus.Success + }); + } + + await this.checkStatus(); + } + }; + return Object.assign({}, actions, integrationActions); +}; + +export default createActions; diff --git a/front/src/routes/integration/all/node-red/setup-page/index.js b/front/src/routes/integration/all/node-red/setup-page/index.js new file mode 100644 index 0000000000..b382ef92c5 --- /dev/null +++ b/front/src/routes/integration/all/node-red/setup-page/index.js @@ -0,0 +1,37 @@ +import { Component } from 'preact'; +import { connect } from 'unistore/preact'; +import actions from './actions'; +import NodeRedPage from '../NodeRedPage'; +import SetupTab from './SetupTab'; +import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants'; + +class NodeRedSetupPage extends Component { + async componentWillMount() { + this.props.session.dispatcher.addListener( + WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, + this.props.checkStatus + ); + + await this.props.checkStatus(); + } + + componentWillUnmount() { + this.props.session.dispatcher.removeListener( + WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, + this.props.checkStatus + ); + } + + render(props, {}) { + return ( + + + + ); + } +} + +export default connect( + 'user,session,nodeRedExist,nodeRedRunning,nodeRedEnabled,gladysConnected,dockerBased,networkModeValid', + actions +)(NodeRedSetupPage); diff --git a/front/src/routes/integration/all/node-red/setup-page/style.css b/front/src/routes/integration/all/node-red/setup-page/style.css new file mode 100644 index 0000000000..117e83421d --- /dev/null +++ b/front/src/routes/integration/all/node-red/setup-page/style.css @@ -0,0 +1,23 @@ +.tdCenter { + vertical-align: middle; + display: flex; + align-items: center; +} + +.greenIcon { + color: #5eba00;; + font-size: 24px; +} + +.redIcon { + color: #cd201f; + font-size: 24px; +} + +.line { + color: #555; + background-color: #555; + border-color: #555; + height: 1px; + width: 40px; +} \ No newline at end of file diff --git a/front/src/routes/integration/all/zigbee2mqtt/commons/CheckStatus.js b/front/src/routes/integration/all/zigbee2mqtt/commons/CheckStatus.js index 534a27567e..17c380ca47 100644 --- a/front/src/routes/integration/all/zigbee2mqtt/commons/CheckStatus.js +++ b/front/src/routes/integration/all/zigbee2mqtt/commons/CheckStatus.js @@ -4,7 +4,8 @@ import { connect } from 'unistore/preact'; import actions from './actions'; import { Text } from 'preact-i18n'; -class CheckStatus extends Component { +class +CheckStatus extends Component { componentWillMount() { this.props.checkStatus(); } diff --git a/server/lib/system/system.getNetworkMode.js b/server/lib/system/system.getNetworkMode.js index 5b8be9e5d0..db05965093 100644 --- a/server/lib/system/system.getNetworkMode.js +++ b/server/lib/system/system.getNetworkMode.js @@ -12,6 +12,7 @@ async function getNetworkMode() { throw new PlatformNotCompatible('SYSTEM_NOT_RUNNING_DOCKER'); } + return 'host'; if (!this.networkMode) { const containerId = await this.getGladysContainerId(); const gladysContainer = this.dockerode.getContainer(containerId); diff --git a/server/lib/system/system.isDocker.js b/server/lib/system/system.isDocker.js index 5f1699ec7c..a47e0f1c9b 100644 --- a/server/lib/system/system.isDocker.js +++ b/server/lib/system/system.isDocker.js @@ -7,6 +7,8 @@ const fs = require('fs'); * isDocker(); */ function isDocker() { + return Promise.resolve(true); + return new Promise((resolve) => { fs.access('/.dockerenv', fs.constants.F_OK, (err) => { if (err) { diff --git a/server/services/index.js b/server/services/index.js index c335d2850e..83c929aee9 100644 --- a/server/services/index.js +++ b/server/services/index.js @@ -22,3 +22,4 @@ module.exports['lan-manager'] = require('./lan-manager'); module.exports['nextcloud-talk'] = require('./nextcloud-talk'); module.exports.tuya = require('./tuya'); module.exports.melcloud = require('./melcloud'); +module.exports['node-red'] = require('./node-red'); diff --git a/server/services/node-red/api/node-red.controller.js b/server/services/node-red/api/node-red.controller.js index f452ab074c..2f44412f2f 100644 --- a/server/services/node-red/api/node-red.controller.js +++ b/server/services/node-red/api/node-red.controller.js @@ -47,7 +47,7 @@ module.exports = function NodeRedController(gladys, nodeRedManager) { */ async function installNodeRedContainer(req, res) { logger.debug('Install NodeRed container'); - await nodeRedManager.installNodeRedContainer(); + await nodeRedManager.installContainer(); res.json({ success: true, }); @@ -81,7 +81,7 @@ module.exports = function NodeRedController(gladys, nodeRedManager) { }, 'post /api/v1/service/node-red/start': { authenticated: true, - controller: asyncMiddleware(installNodeRedContainer()), + controller: asyncMiddleware(installNodeRedContainer), }, 'post /api/v1/service/node-red/disconnect': { authenticated: true, diff --git a/server/services/node-red/docker/gladys-node-red-container.json b/server/services/node-red/docker/gladys-node-red-container.json index fb2ed8dfa1..ab4e10d958 100644 --- a/server/services/node-red/docker/gladys-node-red-container.json +++ b/server/services/node-red/docker/gladys-node-red-container.json @@ -1,7 +1,9 @@ { "name": "gladys-node-red", "Image": "nodered/node-red:latest", - "ExposedPorts": {}, + "ExposedPorts": { + "1880/tcp": {} + }, "HostConfig": { "Binds": [ "/var/lib/gladysassistant/node-red:/data" @@ -12,7 +14,13 @@ "max-size": "10m" } }, - "PortBindings": {}, + "PortBindings": { + "1880/tcp": [ + { + "HostPort": "1880" + } + ] + }, "RestartPolicy": { "Name": "always" }, diff --git a/server/services/node-red/lib/backup.js b/server/services/node-red/lib/backup.js index b1f7f42e34..66b1085a41 100644 --- a/server/services/node-red/lib/backup.js +++ b/server/services/node-red/lib/backup.js @@ -2,13 +2,15 @@ const logger = require('../../../utils/logger'); const { ServiceNotConfiguredError } = require('../../../utils/coreErrors'); /** - * @description Create a Z2M backup. + * @description Create a Node-red backup. * @param {string} jobId - The job id. * @returns {Promise} - Resolve when backup is finished. * @example * backup('aaf45861-c7f5-47ac-bde1-cfe56c7789cf'); */ async function backup(jobId) { + // TODO Make backup + // Backup is not possible when service is not running if (!this.isEnabled()) { throw new ServiceNotConfiguredError('SERVICE_NOT_CONFIGURED'); @@ -36,9 +38,9 @@ async function backup(jobId) { }); // Request z2m to generate a new backup. - logger.info('Zigbee2MQTT request for backup'); + logger.info('Node-red: request for backup'); await this.gladys.job.updateProgress(jobId, 30); - this.mqttClient.publish('zigbee2mqtt/bridge/request/backup'); + return response; } diff --git a/server/services/node-red/lib/checkForContainerUpdates.js b/server/services/node-red/lib/checkForContainerUpdates.js index c99dacbb87..144e9c5a52 100644 --- a/server/services/node-red/lib/checkForContainerUpdates.js +++ b/server/services/node-red/lib/checkForContainerUpdates.js @@ -10,7 +10,7 @@ const nodeRedContainerDescriptor = require('../docker/gladys-node-red-container. * await nodeRed.checkForContainerUpdates(config); */ async function checkForContainerUpdates(config) { - logger.info('Checking for current installed versions and required updates...'); + logger.info('NodeRed: Checking for current installed versions and required updates...'); // Check for MQTT container version if (config.dockerNodeRedVersion !== DEFAULT.DOCKER_NODE_RED_VERSION) { @@ -22,7 +22,7 @@ async function checkForContainerUpdates(config) { }); if (containers.length !== 0) { - logger.debug('Removing current installed NodeRed container...'); + logger.debug('NodeRed: Removing current installed NodeRed container...'); // If container is present, we remove it // The init process will create it again const [container] = containers; diff --git a/server/services/node-red/lib/configureContainer.js b/server/services/node-red/lib/configureContainer.js index 0ee7c6dda9..34e58517e1 100644 --- a/server/services/node-red/lib/configureContainer.js +++ b/server/services/node-red/lib/configureContainer.js @@ -1,23 +1,25 @@ -const fs = require('fs/promises'); -const { constants } = require('fs'); -const path = require('path'); -const yaml = require('yaml'); +// const fs = require('fs/promises'); +// const { constants } = require('fs'); +// const path = require('path'); +// const yaml = require('yaml'); const logger = require('../../../utils/logger'); -const { DEFAULT } = require('./constants'); -const { DEFAULT_KEY, CONFIG_KEYS, ADAPTERS_BY_CONFIG_KEY } = require('../adapters'); +// const { DEFAULT } = require('./constants'); +// const { DEFAULT_KEY, CONFIG_KEYS, ADAPTERS_BY_CONFIG_KEY } = require('../adapters'); /** - * @description Configure Z2M container. - * @param {string} basePathOnContainer - Zigbee2mqtt base path. - * @param {object} config - Gladys Z2M stored configuration. - * @returns {Promise} Indicates if the configuration file has been creted or modified. + * @description Configure Node-red container. + * @param {string} basePathOnContainer - Node-red base path. + * @param {object} config - Gladys Node-red stored configuration. + * @returns {Promise} Indicates if the configuration file has been created or modified. * @example * await this.configureContainer({}); */ async function configureContainer(basePathOnContainer, config) { - logger.info('Z2M Docker container is being configured...'); + logger.info('NodeRed: Docker container is being configured...'); + // TODO NEEDED ? + /* // Create configuration path (if not exists) const configFilepath = path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH); await fs.mkdir(path.dirname(configFilepath), { recursive: true }); @@ -27,9 +29,9 @@ async function configureContainer(basePathOnContainer, config) { try { // eslint-disable-next-line no-bitwise await fs.access(configFilepath, constants.R_OK | constants.W_OK); - logger.info('Z2M configuration file already exists.'); + logger.info('NodeRed: configuration file already exists.'); } catch (e) { - logger.info('Writting default Z2M configuration...'); + logger.info('NodeRed: Writting default configuration...'); await fs.writeFile(configFilepath, yaml.stringify(DEFAULT.CONFIGURATION_CONTENT)); configCreated = true; } @@ -63,11 +65,13 @@ async function configureContainer(basePathOnContainer, config) { } if (configChanged) { - logger.info('Writting MQTT and USB adapter information to Z2M configuration...'); + logger.info('NodeRed: Writting MQTT and USB adapter information to configuration...'); await fs.writeFile(configFilepath, yaml.stringify(loadedConfig)); } return configCreated || configChanged; + */ + return false; } module.exports = { diff --git a/server/services/node-red/lib/connect.js b/server/services/node-red/lib/connect.js deleted file mode 100644 index 2df003ee24..0000000000 --- a/server/services/node-red/lib/connect.js +++ /dev/null @@ -1,73 +0,0 @@ -const logger = require('../../../utils/logger'); -const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); -const { DEFAULT } = require('./constants'); - -/** - * @description Initialize service with dependencies and connect to devices. - * @param {object} MqttParam - MQTT broker URL, Client MQTT username, Client MQTT password. - * @param {string} MqttParam.mqttUrl - MQTT URL. - * @param {string} MqttParam.mqttUsername - MQTT Username. - * @param {string} MqttParam.mqttPassword - MQTT Password. - * @returns {Promise} Resolve when connected. - * @example - * connect(); - */ -async function connect({ mqttUrl, mqttUsername, mqttPassword }) { - if (this.mqttClient) { - logger.info(`Disconnecting existing MQTT client...`); - this.mqttClient.end(); - this.mqttClient.removeAllListeners(); - this.mqttClient = null; - } - - if (this.mqttRunning) { - // Loads MQTT service - logger.info(`Connecting Gladys to ${mqttUrl} MQTT broker...`); - - this.mqttClient = this.mqttLibrary.connect(mqttUrl, { - username: mqttUsername, - password: mqttPassword, - reconnectPeriod: 5000, - clientId: `gladys-main-instance-${Math.floor(Math.random() * 1000000)}`, - }); - - this.mqttClient.on('connect', () => { - logger.info('Connected to MQTT container', mqttUrl); - DEFAULT.TOPICS.forEach((topic) => { - this.subscribe(topic, this.handleMqttMessage.bind(this)); - }); - this.gladysConnected = true; - this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, - }); - }); - - this.mqttClient.on('error', (err) => { - logger.warn(`Error while connecting to MQTT - ${err}`); - this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.MQTT_ERROR, - payload: err, - }); - this.gladysConnected = false; - }); - - this.mqttClient.on('offline', () => { - logger.warn(`Disconnected from MQTT server`); - this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.MQTT.ERROR, - payload: 'DISCONNECTED', - }); - this.gladysConnected = false; - }); - - this.mqttClient.on('message', (topic, message) => { - this.handleMqttMessage(topic, message.toString()); - }); - } else { - logger.warn("Can't connect Gladys cause MQTT not running !"); - } -} - -module.exports = { - connect, -}; diff --git a/server/services/node-red/lib/constants.js b/server/services/node-red/lib/constants.js index 453013d2b4..361a61aaf8 100644 --- a/server/services/node-red/lib/constants.js +++ b/server/services/node-red/lib/constants.js @@ -4,54 +4,13 @@ const CONFIGURATION = { ZIGBEE_DONGLE_NAME: 'ZIGBEE_DONGLE_NAME', MQTT_URL_KEY: 'Z2M_MQTT_URL', MQTT_URL_VALUE: 'mqtt://localhost:1884', - Z2M_MQTT_USERNAME_KEY: 'Z2M_MQTT_USERNAME', - Z2M_MQTT_USERNAME_VALUE: 'z2m', - Z2M_MQTT_PASSWORD_KEY: 'Z2M_MQTT_PASSWORD', - GLADYS_MQTT_USERNAME_KEY: 'GLADYS_MQTT_USERNAME', - GLADYS_MQTT_USERNAME_VALUE: 'gladys', - GLADYS_MQTT_PASSWORD_KEY: 'GLADYS_MQTT_PASSWORD', - DOCKER_MQTT_VERSION: 'DOCKER_MQTT_VERSION', // Variable to identify last version of MQTT docker file is installed - DOCKER_Z2M_VERSION: 'DOCKER_Z2M_VERSION', // Variable to identify last version of Z2M docker file is installed + + DOCKER_NODE_RED_VERSION: 'DOCKER_NODE_RED_VERSION', // Variable to identify last version of NodeRed docker file is installed }; const DEFAULT = { - DOCKER_NODE_RED_VERSION: '1', // Last version of NodeRed docker file, + DOCKER_NODE_RED_VERSION: '2 ', // Last version of NodeRed docker file, - CONFIGURATION_PATH: 'zigbee2mqtt/z2m/configuration.yaml', - CONFIGURATION_CONTENT: { - homeassistant: false, - permit_join: false, - mqtt: { - base_topic: 'zigbee2mqtt', - server: 'mqtt://localhost:1884', - }, - serial: { - port: '/dev/ttyACM0', - }, - frontend: { - port: 8080, - }, - map_options: { - graphviz: { - colors: { - fill: { - enddevice: '#fff8ce', - coordinator: '#e04e5d', - router: '#4ea3e0', - }, - font: { - coordinator: '#ffffff', - router: '#ffffff', - enddevice: '#000000', - }, - line: { - active: '#009900', - inactive: '#994444', - }, - }, - }, - }, - }, }; module.exports = { diff --git a/server/services/node-red/lib/disconnect.js b/server/services/node-red/lib/disconnect.js index 949d7f3712..09123cd472 100644 --- a/server/services/node-red/lib/disconnect.js +++ b/server/services/node-red/lib/disconnect.js @@ -1,8 +1,7 @@ const logger = require('../../../utils/logger'); const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); -const mqttContainerDescriptor = require('../docker/gladys-z2m-mqtt-container.json'); -const zigbee2mqttContainerDescriptor = require('../docker/gladys-z2m-zigbee2mqtt-container.json'); +const nodeRedContainerDescriptor = require('../docker/gladys-node-red-container.json'); /** * @description Disconnect service from dependent containers. @@ -17,44 +16,24 @@ async function disconnect() { this.backupScheduledJob.cancel(); } - // Disconnect from MQTT broker - if (this.mqttClient) { - logger.debug(`Disconnecting existing MQTT server...`); - this.mqttClient.end(); - this.mqttClient.removeAllListeners(); - this.mqttClient = null; - } else { - logger.debug('Not connected'); - } this.gladysConnected = false; this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); - // Stop MQTT container - let dockerContainer = await this.gladys.system.getContainers({ + // Stop NodeRed container + const dockerContainer = await this.gladys.system.getContainers({ all: true, - filters: { name: [mqttContainerDescriptor.name] }, + filters: { name: [nodeRedContainerDescriptor.name] }, }); [container] = dockerContainer; await this.gladys.system.stopContainer(container.id); - this.mqttRunning = false; + this.nodeRedRunning = false; this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); - // Stop zigbee2mqtt container - dockerContainer = await this.gladys.system.getContainers({ - all: true, - filters: { name: [zigbee2mqttContainerDescriptor.name] }, - }); - [container] = dockerContainer; - await this.gladys.system.stopContainer(container.id); - this.zigbee2mqttRunning = false; - this.zigbee2mqttConnected = false; - this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, - }); + } module.exports = { diff --git a/server/services/node-red/lib/getConfiguration.js b/server/services/node-red/lib/getConfiguration.js index 8ec1a2a678..23f521a9f5 100644 --- a/server/services/node-red/lib/getConfiguration.js +++ b/server/services/node-red/lib/getConfiguration.js @@ -3,41 +3,21 @@ const logger = require('../../../utils/logger'); const { CONFIGURATION } = require('./constants'); /** - * @description Get Z2M configuration. - * @returns {Promise} Current Z2M network configuration. + * @description Get Node-red configuration. + * @returns {Promise} Current Node-red network configuration. * @example - * const config = await z2m.getConfiguration(); + * const config = await nodeRed.getConfiguration(); */ async function getConfiguration() { - logger.debug('Zigbee2mqtt: loading stored configuration...'); - - // Load z2m parameters - const z2mDriverPath = await this.gladys.variable.getValue(CONFIGURATION.Z2M_DRIVER_PATH, this.serviceId); - const z2mDongleName = await this.gladys.variable.getValue(CONFIGURATION.ZIGBEE_DONGLE_NAME, this.serviceId); - const z2mMqttUsername = await this.gladys.variable.getValue(CONFIGURATION.Z2M_MQTT_USERNAME_KEY, this.serviceId); - const z2mMqttPassword = await this.gladys.variable.getValue(CONFIGURATION.Z2M_MQTT_PASSWORD_KEY, this.serviceId); - - // Load MQTT parameters - const mqttUrl = await this.gladys.variable.getValue(CONFIGURATION.MQTT_URL_KEY, this.serviceId); - const mqttUsername = await this.gladys.variable.getValue(CONFIGURATION.GLADYS_MQTT_USERNAME_KEY, this.serviceId); - const mqttPassword = await this.gladys.variable.getValue(CONFIGURATION.GLADYS_MQTT_PASSWORD_KEY, this.serviceId); + logger.debug('NodeRed: loading stored configuration...'); // Load version parameters - const dockerMqttVersion = await this.gladys.variable.getValue(CONFIGURATION.DOCKER_MQTT_VERSION, this.serviceId); - const dockerZ2mVersion = await this.gladys.variable.getValue(CONFIGURATION.DOCKER_Z2M_VERSION, this.serviceId); + const dockerNodeRedVersion = await this.gladys.variable.getValue(CONFIGURATION.DOCKER_NODE_RED_VERSION, this.serviceId); // Gladys params const timezone = await this.gladys.variable.getValue(SYSTEM_VARIABLE_NAMES.TIMEZONE); return { - z2mDriverPath, - z2mDongleName, - z2mMqttUsername, - z2mMqttPassword, - mqttUrl, - mqttUsername, - mqttPassword, - dockerMqttVersion, - dockerZ2mVersion, + dockerNodeRedVersion, timezone, }; } diff --git a/server/services/node-red/lib/index.js b/server/services/node-red/lib/index.js index e2790bd36d..18314c83c0 100644 --- a/server/services/node-red/lib/index.js +++ b/server/services/node-red/lib/index.js @@ -2,18 +2,17 @@ const { init } = require('./init'); const { getConfiguration } = require('./getConfiguration'); const { saveConfiguration } = require('./saveConfiguration'); const { installContainer } = require('./installContainer'); - -const { connect } = require('./connect'); +const { backup } = require('./backup'); +const { checkForContainerUpdates } = require('./checkForContainerUpdates'); const { disconnect } = require('./disconnect'); -const { status } = require('./status'); const { isEnabled } = require('./isEnabled'); -const { checkForContainerUpdates } = require('./checkForContainerUpdates'); +const { status } = require('./status'); + const { configureContainer } = require('./configureContainer'); -const { setup } = require('./setup'); const { saveZ2mBackup } = require('./saveZ2mBackup'); const { restoreZ2mBackup } = require('./restoreZ2mBackup'); -const { backup } = require('./backup'); + const { JOB_TYPES } = require('../../../utils/constants'); /** @@ -42,20 +41,20 @@ const NodeRedManager = function NodeRedManager(gladys, serviceId) { this.backupScheduledJob = null; }; + NodeRedManager.prototype.init = init; NodeRedManager.prototype.getConfiguration = getConfiguration; NodeRedManager.prototype.saveConfiguration = saveConfiguration; NodeRedManager.prototype.installContainer = installContainer; - -NodeRedManager.prototype.connect = connect; +NodeRedManager.prototype.backup = backup; +NodeRedManager.prototype.checkForContainerUpdates = checkForContainerUpdates; NodeRedManager.prototype.disconnect = disconnect; -NodeRedManager.prototype.status = status; NodeRedManager.prototype.isEnabled = isEnabled; -NodeRedManager.prototype.checkForContainerUpdates = checkForContainerUpdates; +NodeRedManager.prototype.status = status; + NodeRedManager.prototype.configureContainer = configureContainer; -NodeRedManager.prototype.setup = setup; NodeRedManager.prototype.saveZ2mBackup = saveZ2mBackup; NodeRedManager.prototype.restoreZ2mBackup = restoreZ2mBackup; -NodeRedManager.prototype.backup = backup; + module.exports = NodeRedManager; diff --git a/server/services/node-red/lib/init.js b/server/services/node-red/lib/init.js index e5a37d0f50..417145feb0 100644 --- a/server/services/node-red/lib/init.js +++ b/server/services/node-red/lib/init.js @@ -27,12 +27,9 @@ async function init() { logger.debug('NodeRed: installing and starting required docker containers...'); await this.checkForContainerUpdates(configuration); - await this.installMqttContainer(configuration); - await this.installZ2mContainer(configuration); + await this.installContainer(configuration); if (this.isEnabled()) { - await this.connect(configuration); - // Schedule reccurent job if not already scheduled if (!this.backupScheduledJob) { this.backupScheduledJob = this.gladys.scheduler.scheduleJob('0 0 23 * * *', () => this.backup()); diff --git a/server/services/node-red/lib/installContainer.js b/server/services/node-red/lib/installContainer.js index 5571a00650..fdfee77315 100644 --- a/server/services/node-red/lib/installContainer.js +++ b/server/services/node-red/lib/installContainer.js @@ -5,6 +5,7 @@ const logger = require('../../../utils/logger'); const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); const containerDescriptor = require('../docker/gladys-node-red-container.json'); +const {PlatformNotCompatible} = require('../../../utils/coreErrors'); const sleep = promisify(setTimeout); @@ -15,6 +16,18 @@ const sleep = promisify(setTimeout); * await nodeRed.installContainer(config); */ async function installContainer(config) { + const dockerBased = await this.gladys.system.isDocker(); + if (!dockerBased) { + this.dockerBased = false; + throw new PlatformNotCompatible('SYSTEM_NOT_RUNNING_DOCKER'); + } + + const networkMode = await this.gladys.system.getNetworkMode(); + if (networkMode !== 'host') { + this.networkModeValid = false; + throw new PlatformNotCompatible('DOCKER_BAD_NETWORK'); + } + let dockerContainers = await this.gladys.system.getContainers({ all: true, @@ -22,8 +35,9 @@ async function installContainer(config) { }); let [container] = dockerContainers; - const { basePathOnContainer, basePathOnHost } = await this.gladys.system.getGladysBasePath(); - const containerPath = `${basePathOnHost}/node-red`; + // const { basePathOnContainer, basePathOnHost } = await this.gladys.system.getGladysBasePath(); + // const containerPath = `${basePathOnHost}/node-red`; + const basePathOnContainer = ``;// TODO CHange if (dockerContainers.length === 0) { // Restore backup only in case of new installation // await this.restoreZ2mBackup(containerPath); @@ -35,17 +49,18 @@ async function installContainer(config) { await this.gladys.system.pull(containerDescriptor.Image); const containerDescriptorToMutate = cloneDeep(containerDescriptor); + console.log(containerDescriptorToMutate); logger.info(`Creation of container...`); const containerLog = await this.gladys.system.createContainer(containerDescriptorToMutate); logger.trace(containerLog); logger.info('NodeRed: successfully installed and configured as Docker container'); - this.zigbee2mqttExist = true; + this.nodeRedExist = true; } catch (e) { - this.zigbee2mqttExist = false; - logger.error('Zigbee2mqtt failed to install as Docker container:', e); + this.nodeRedExist = false; + logger.error('NodeRed: failed to install as Docker container:', e); this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); throw e; } @@ -62,23 +77,23 @@ async function installContainer(config) { // Check if we need to restart the container (container is not running / config changed) if (container.state !== 'running' || configChanged) { - logger.info('Zigbee2mqtt container is (re)starting...'); + logger.info('NodeRed: container is (re)starting...'); await this.gladys.system.restartContainer(container.id); // wait a few seconds for the container to restart await sleep(this.containerRestartWaitTimeInMs); } - logger.info('Zigbee2mqtt container successfully started'); + logger.info('NodeRed: container successfully started'); this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); - this.zigbee2mqttRunning = true; - this.zigbee2mqttExist = true; + this.nodeRedRunning = true; + this.nodeRedExist = true; } catch (e) { - logger.error('Zigbee2mqtt container failed to start:', e); - this.zigbee2mqttRunning = false; + logger.error('NodeRed: container failed to start:', e); + this.nodeRedRunning = false; this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.ZIGBEE2MQTT.STATUS_CHANGE, + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); throw e; } diff --git a/server/services/node-red/lib/isEnabled.js b/server/services/node-red/lib/isEnabled.js index 11f721f2be..4007ad91b9 100644 --- a/server/services/node-red/lib/isEnabled.js +++ b/server/services/node-red/lib/isEnabled.js @@ -1,11 +1,11 @@ /** - * @description Checks if z2m is ready to use. - * @returns {boolean} Is the z2m environment ready to use? + * @description Checks if Node-red is ready to use. + * @returns {boolean} Is the Node-red environment ready to use? * @example - * z2m.isEnabled(); + * nodeRed.isEnabled(); */ function isEnabled() { - return this.mqttRunning && this.zigbee2mqttRunning && this.usbConfigured; + return this.nodeRedRunning; } module.exports = { diff --git a/server/services/node-red/lib/saveConfiguration.js b/server/services/node-red/lib/saveConfiguration.js index 3686ad8e2b..1b5ed8a952 100644 --- a/server/services/node-red/lib/saveConfiguration.js +++ b/server/services/node-red/lib/saveConfiguration.js @@ -10,25 +10,17 @@ const saveOrDestroy = async (variableManager, key, value, serviceId) => { }; /** - * @description Save Z2M configuration. - * @param {object} config - Z2M service configuration. - * @returns {Promise} Current MQTT network configuration. + * @description Save Node-red configuration. + * @param {object} config - Node-red service configuration. + * @returns {Promise} Current Node-red configuration. * @example - * await z2m.saveConfiguration(config); + * await nodeRed.saveConfiguration(config); */ async function saveConfiguration(config) { - logger.debug('Zigbee2mqtt: storing configuration...'); + logger.debug('NodeRed: storing configuration...'); const keyValueMap = { - [CONFIGURATION.Z2M_DRIVER_PATH]: config.z2mDriverPath, - [CONFIGURATION.ZIGBEE_DONGLE_NAME]: config.z2mDongleName, - [CONFIGURATION.Z2M_MQTT_USERNAME_KEY]: config.z2mMqttUsername, - [CONFIGURATION.Z2M_MQTT_PASSWORD_KEY]: config.z2mMqttPassword, - [CONFIGURATION.MQTT_URL_KEY]: config.mqttUrl, - [CONFIGURATION.GLADYS_MQTT_USERNAME_KEY]: config.mqttUsername, - [CONFIGURATION.GLADYS_MQTT_PASSWORD_KEY]: config.mqttPassword, - [CONFIGURATION.DOCKER_MQTT_VERSION]: config.dockerMqttVersion, - [CONFIGURATION.DOCKER_Z2M_VERSION]: config.dockerZ2mVersion, + [CONFIGURATION.DOCKER_NODE_RED_VERSION]: config.dockerNodeRedVersion, }; const variableKeys = Object.keys(keyValueMap); @@ -37,7 +29,7 @@ async function saveConfiguration(config) { variableKeys.map((key) => saveOrDestroy(this.gladys.variable, key, keyValueMap[key], this.serviceId)), ); - logger.debug('Zigbee2mqtt: configuration stored'); + logger.debug('NodeRed: configuration stored'); } module.exports = { diff --git a/server/services/node-red/lib/setup.js b/server/services/node-red/lib/setup.js deleted file mode 100644 index 3acc132b36..0000000000 --- a/server/services/node-red/lib/setup.js +++ /dev/null @@ -1,22 +0,0 @@ -const { CONFIGURATION } = require('./constants'); - -/** - * @description Setup Zigbee2mqtt USB device. - * @param {object} usbConfig - Configuration about USB Zigbee dongle. - * @example - * await this.setup({ ZIGBEE2MQTT_DRIVER_PATH: '/dev/tty0', ZIGBEE_DONGLE_NAME: 'zzh' }); - */ -async function setup(usbConfig) { - const z2mDriverPath = usbConfig[CONFIGURATION.Z2M_DRIVER_PATH]; - const z2mDongleName = usbConfig[CONFIGURATION.ZIGBEE_DONGLE_NAME]; - - await this.gladys.variable.setValue(CONFIGURATION.Z2M_DRIVER_PATH, z2mDriverPath, this.serviceId); - await this.gladys.variable.setValue(CONFIGURATION.ZIGBEE_DONGLE_NAME, z2mDongleName, this.serviceId); - - // Reload z2m container with new USB configuration - await this.init(); -} - -module.exports = { - setup, -}; diff --git a/server/services/node-red/lib/status.js b/server/services/node-red/lib/status.js index a59d0c7e1f..633826e2b3 100644 --- a/server/services/node-red/lib/status.js +++ b/server/services/node-red/lib/status.js @@ -1,20 +1,17 @@ /** - * @description Get Zigbee2mqtt status. - * @returns {object} Current Zigbee2mqtt containers and configuration status. + * @description Get Node-red status. + * @returns {object} Current Node-red containers and configuration status. * @example * status(); */ function status() { - const z2mEnabled = this.isEnabled(); + const nodeRedEnabled = this.isEnabled(); const zigbee2mqttStatus = { - usbConfigured: this.usbConfigured, - mqttExist: this.mqttExist, - mqttRunning: this.mqttRunning, - zigbee2mqttExist: this.zigbee2mqttExist, - zigbee2mqttRunning: this.zigbee2mqttRunning, + + nodeRedExist: this.nodeRedExist, + nodeRedRunning: this.nodeRedRunning, + nodeRedEnabled, gladysConnected: this.gladysConnected, - zigbee2mqttConnected: this.zigbee2mqttConnected, - z2mEnabled, dockerBased: this.dockerBased, networkModeValid: this.networkModeValid, }; diff --git a/server/utils/constants.js b/server/utils/constants.js index 1b7e194261..37d17668e3 100644 --- a/server/utils/constants.js +++ b/server/utils/constants.js @@ -890,6 +890,10 @@ const WEBSOCKET_MESSAGE_TYPES = { STATUS: 'melcloud.status', DISCOVER: 'melcloud.discover', }, + NODERED: { + STATUS_CHANGE: 'nodered.status-change', + MQTT_ERROR: 'nodered.mqtt-error', + }, }; const DASHBOARD_TYPE = { From 91fae9c8e1ada590a447230ae8514acfdd4c1b7a Mon Sep 17 00:00:00 2001 From: callemand Date: Thu, 8 Jun 2023 15:31:46 +0200 Subject: [PATCH 03/32] Add service to enable and install node-red --- front/src/config/i18n/en.json | 33 + front/src/config/i18n/fr.json | 41 +- .../integration/all/node-red/NodeRedPage.js | 6 +- .../all/node-red/setup-page/CheckStatus.js | 96 ++- .../all/node-red/setup-page/SetupTab.jsx | 366 +++++++---- .../all/node-red/setup-page/actions.js | 91 --- .../all/node-red/setup-page/index.js | 23 +- .../all/node-red/setup-page/style.css | 11 +- .../all/zigbee2mqtt/commons/CheckStatus.js | 3 +- server/lib/system/system.getGladysBasePath.js | 24 +- server/lib/system/system.getNetworkMode.js | 1 - server/lib/system/system.isDocker.js | 2 - .../node-red/api/node-red.controller.js | 18 - .../docker/gladys-node-red-container.json | 6 +- server/services/node-red/docker/settings.txt | 597 ++++++++++++++++++ server/services/node-red/lib/backup.js | 50 -- .../node-red/lib/checkForContainerUpdates.js | 3 +- .../node-red/lib/configureContainer.js | 58 +- server/services/node-red/lib/constants.js | 14 +- server/services/node-red/lib/disconnect.js | 24 +- .../services/node-red/lib/getConfiguration.js | 10 +- server/services/node-red/lib/index.js | 18 - server/services/node-red/lib/init.js | 13 +- .../services/node-red/lib/installContainer.js | 18 +- .../services/node-red/lib/restoreZ2mBackup.js | 48 -- .../node-red/lib/saveConfiguration.js | 3 + server/services/node-red/lib/saveZ2mBackup.js | 45 -- server/services/node-red/lib/status.js | 1 - server/services/node-red/package-lock.json | 31 +- server/services/node-red/package.json | 4 +- 30 files changed, 1070 insertions(+), 588 deletions(-) delete mode 100644 front/src/routes/integration/all/node-red/setup-page/actions.js create mode 100644 server/services/node-red/docker/settings.txt delete mode 100644 server/services/node-red/lib/backup.js delete mode 100644 server/services/node-red/lib/restoreZ2mBackup.js delete mode 100644 server/services/node-red/lib/saveZ2mBackup.js diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 1a1115b968..0cc44fc978 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -607,6 +607,39 @@ "saveError": "There was an error saving the device.", "documentation": "Zigbee2mqtt documentation" }, + "nodeRed": { + "title": "Node-red", + "description": "\"Control your devices with Node-red.", + "setupTab": "Setup", + "documentation": "Node-red documentation", + + "status": { + "notInstalled": "Node-red server failed to install.", + "notRunning": "Node-red server failed to start.", + "running": "Node-red successfully started.", + "notEnabled": "Node-red is not activated.", + "nonDockerEnv": "Gladys is not running on Docker, you cannot install a Node-red server from here.", + "invalidDockerNetwork": "Gladys is under custom installation, to install server from here, Gladys container should be configured with \"host\" network mode." + }, + "setup": { + "title": "Node-red configuration", + "description": "This service uses docker container. Enable Node-red for deploying this container.\nLearn more on the node-red documentation page", + "error": "An error occured while starting Node-red.", + "enableLabel": "Node-red activation", + "enableNodeRed": "Enable", + "disableNodeRed": "Disable", + "activationNodeRed": "Activating...", + "serviceStatus": "Node-red Service Status", + "containersStatus": "Containers related to Node-red", + "status": "Status", + "node-red": "Node-red", + "gladys": "Gladys", + "usernameLabel": "Username", + "passwordLabel": "Password", + "urlLabel": "Node-red interface url: {{nodeRedUrl}}" + } + }, + "googleHome": { "title": "Google Home", "description": "Control your Gladys device with your voice in Google Home", diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index f3f2131869..1c8f6bea42 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -733,50 +733,37 @@ "saveError": "Une erreur s'est produite lors de la sauvegarde.", "documentation": "Documentation Zigbee2mqtt" }, - "node-red": { + "nodeRed": { "title": "Node-red", - "description": "Contrôlez vos appareils via Node-red TODO.", + "description": "Contrôlez vos appareils via Node-red.", "setupTab": "Configuration", "documentation": "Documentation Node-red", "status": { "notInstalled": "Le server Node-red n'a pas pu être installé.", "notRunning": "Le server Node-red n'a pas démarré.", "running": "Node-red démarré avec succès.", - - "zigbee2mqttNotInstalled": "Zigbee2mqtt n'a pas pu être installé.", - "zigbee2mqttNotRunning": "Zigbee2mqtt n'a pas démarré.", - "gladysNotConnected": "Gladys n'a pas réussi à se connecter au broker MQTT", - "zigbee2mqttNotConnected": "Zigbee2mqtt n'a pas réussi à se connecter au broker MQTT", - "settingsPageLink": "la page de paramétrage du dongle USB", - "setupPageLink": "la page de configuration de Zigbee2mqtt" + "notEnabled": "Node-red n'est pas activé.", + "nonDockerEnv": "Gladys ne s'exécute actuellement pas dans Docker, il est impossible d'activer Node-red depuis Gladys.", + "invalidDockerNetwork": "Gladys est basée sur une installation personalisée, pour installer Node-red depuis cette page, le conteneur Docker de Gladys doit être démarré avec l'option \"network=host\"." }, "setup": { "title": "Configuration du service Node-red", "description": "Ce service utilise un container Docker. Activer Node-red pour déployer ce container.\nPour en savoir plus, rendez-vous sur la page de documentation Node-red", "error": "Une erreur s'est produite au démarrage du service Node-red.", "enableLabel": "Activation du service Node-red", - "nonDockerEnv": "Gladys ne s'exécute actuellement pas dans Docker, il est impossible d'activer Node-red depuis Gladys.", - "invalidDockerNetwork": "Gladys est basée sur une installation personalisée, pour installer Node-red depuis cette page, le conteneur Docker de Gladys doit être démarré avec l'option \"network=host\".", - "enableNodeRed": "Activer Node-red", + + "enableNodeRed": "Activer", + "disableNodeRed": "Desactiver", + "activationNodeRed": "Activation...", "serviceStatus": "Etat du service Node-red", "containersStatus": "Conteneurs liés à Node-red", "status": "Status", "node-red": "Node-red", - "gladys": "Gladys" - }, - - "noDeviceFound": "Auncun appareil zigbee2mqtt n'a encore été ajouté.", - "nameLabel": "Nom", - "namePlaceholder": "Donner un nom à l'appareil", - "roomLabel": "Pièce", - "topicLabel": "Sujet MQTT", - "topicPlaceholder": "%topic% zigbee2mqtt MQTT value", - "featuresLabel": "Fonctionnalités", - "saveButton": "Sauvegarder", - "deleteButton": "Supprimer", - "updateButton": "Mettre à jour", - "alreadyExistsButton": "Déjà créé", - "saveError": "Une erreur s'est produite lors de la sauvegarde." + "gladys": "Gladys", + "usernameLabel": "Nom d'utilisateur", + "passwordLabel": "Mot de passe", + "urlLabel": "Url de l'interface Node-red : {{nodeRedUrl}}" + } }, "googleHome": { diff --git a/front/src/routes/integration/all/node-red/NodeRedPage.js b/front/src/routes/integration/all/node-red/NodeRedPage.js index 12668aa1c9..6ec3ef10e7 100644 --- a/front/src/routes/integration/all/node-red/NodeRedPage.js +++ b/front/src/routes/integration/all/node-red/NodeRedPage.js @@ -10,7 +10,7 @@ const NodeRedPage = ({ children, user }) => (

- +

@@ -22,7 +22,7 @@ const NodeRedPage = ({ children, user }) => ( - + ( - +
diff --git a/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js b/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js index d60d8b0875..181c341053 100644 --- a/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js +++ b/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js @@ -1,42 +1,74 @@ import { Component } from 'preact'; -import { Link } from 'preact-router/match'; -import { connect } from 'unistore/preact'; -import actions from './actions'; import { Text } from 'preact-i18n'; +import { RequestStatus } from '../../../../../utils/consts'; +import style from './style.css'; +import classNames from 'classnames/bind'; + +let cx = classNames.bind(style); class CheckStatus extends Component { - componentWillMount() { - this.props.checkStatus(); - } + render({ nodeRedEnabled, nodeRedExist, nodeRedRunning, toggle, dockerBased, networkModeValid, nodeRedStatus }, {}) { + /* + {props.nodeRedStatus === RequestStatus.Error && ( +

+ +

+ )} + */ + let buttonLabel = ''; + let textLabel = ''; + if (nodeRedStatus === RequestStatus.Getting) { + buttonLabel = 'integration.nodeRed.setup.activationNodeRed'; + textLabel = 'integration.nodeRed.setup.activationNodeRed'; + } else if (!dockerBased) { + buttonLabel = 'integration.nodeRed.setup.nonDockerEnv'; + textLabel = 'integration.nodeRed.status.nonDockerEnv'; + } else if (!networkModeValid) { + buttonLabel = 'integration.nodeRed.setup.invalidDockerNetwork'; + textLabel = 'integration.nodeRed.status.invalidDockerNetwork'; + } else if (nodeRedEnabled) { + buttonLabel = 'integration.nodeRed.setup.disableNodeRed'; + if (!nodeRedExist) { + textLabel = 'integration.nodeRed.status.notInstalled'; + } else if (!nodeRedRunning) { + textLabel = 'integration.nodeRed.status.notRunning'; + } else { + textLabel = 'integration.nodeRed.status.running'; + } + } else { + buttonLabel = 'integration.nodeRed.setup.enableNodeRed'; + textLabel = 'integration.nodeRed.status.notEnabled'; + } - renderError(messageKey) { return ( -
- -
- ) - } - - render(props, {}) { - console.log('CheckStatus', props); +
+
+
+ + + +
- if(props.nodeRedEnabled) { - if (!props.nodeRedExist) { - return this.renderError('integration.node-red.status.notInstalled'); - } else if (!props.nodeRedRunning) { - return this.renderError('integration.node-red.status.notRunning'); - } - return ( -

- -

- ) - } - return null; +
+ +
+
+
+ ); } } -export default connect( - 'user,session,nodeRedExist,nodeRedRunning,nodeRedEnabled', - actions -)(CheckStatus); +export default CheckStatus; diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx index 06a81a71e5..b679818b84 100644 --- a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -1,187 +1,321 @@ import { Component } from 'preact'; -import { Text, MarkupText } from 'preact-i18n'; +import { Text, MarkupText, Localizer } from 'preact-i18n'; import { RequestStatus } from '../../../../../utils/consts'; import CheckStatus from './CheckStatus.js'; import classNames from 'classnames/bind'; import style from './style.css'; +import get from 'get-value'; +import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants'; let cx = classNames.bind(style); class SetupTab extends Component { + componentDidMount = () => { + this.checkStatus(); + this.getConfiguration(); + }; + + getConfiguration = async () => { + try { + const nodeRedUsernameVariable = await this.props.httpClient.get( + '/api/v1/service/node-red/variable/NODE_RED_USERNAME' + ); + const nodeRedPasswordVariable = await this.props.httpClient.get( + '/api/v1/service/node-red/variable/NODE_RED_PASSWORD' + ); + const nodeRedUrlVariable = await this.props.httpClient.get('/api/v1/service/node-red/variable/NODE_RED_URL'); + this.setState({ + nodeRedUsername: nodeRedUsernameVariable.value, + nodeRedPassword: nodeRedPasswordVariable.value, + nodeRedUrl: nodeRedUrlVariable.value + }); + } catch (e) { + // Variable is not set yet + } + }; + + async componentWillMount() { + this.props.session.dispatcher.addListener(WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, this.checkStatus); + } + + componentWillUnmount = () => { + this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, this.checkStatus); + }; + toggle = () => { - let checked = this.props.nodeRedEnabled; + let checked = this.state.nodeRedEnabled; checked = !checked; if (checked) { - this.props.startContainer(); + this.startContainer(); } else { - this.props.stopContainer(); + this.stopContainer(); } + }; + + startContainer = async () => { + let error = false; + + this.setState({ + nodeRedStatus: RequestStatus.Getting + }); + + await this.props.httpClient.post('/api/v1/service/node-red/variable/NODERED_ENABLED', { + value: true + }); + + try { + await this.props.httpClient.post('/api/v1/service/node-red/connect'); + } catch (e) { + error = error | get(e, 'response.status'); + } + + if (error) { + this.setState({ + nodeRedStatus: RequestStatus.Error + }); + } else { + this.setState({ + nodeRedStatus: RequestStatus.Success + }); + } + + await this.checkStatus(); + }; + + stopContainer = async () => { + let error = false; + try { + await this.props.httpClient.post('/api/v1/service/node-red/disconnect'); + } catch (e) { + error = error | get(e, 'response.status'); + } + + if (error) { + this.setState({ + nodeRedStatus: RequestStatus.Error + }); + } else { + this.setState({ + nodeRedStatus: RequestStatus.Success + }); + } + + await this.checkStatus(); + }; - this.props.nodeRedEnabled = checked; + checkStatus = async () => { + let nodeRedStatus = { + nodeRedExist: false, + nodeRedRunning: false, + nodeRedEnabled: false, + dockerBased: false, + networkModeValid: false + }; + try { + nodeRedStatus = await this.props.httpClient.get('/api/v1/service/node-red/status'); + } finally { + this.setState({ + nodeRedExist: nodeRedStatus.nodeRedExist, + nodeRedRunning: nodeRedStatus.nodeRedRunning, + nodeRedEnabled: nodeRedStatus.nodeRedEnabled, + dockerBased: nodeRedStatus.dockerBased, + networkModeValid: nodeRedStatus.networkModeValid + }); + } }; - render(props, {}) { - console.log(props); + render( + props, + { + nodeRedEnabled, + dockerBased, + networkModeValid, + nodeRedExist, + nodeRedRunning, + nodeRedUsername, + nodeRedPassword, + nodeRedUrl, + nodeRedStatus + } + ) { return (

- +

- +

- {props.nodeRedStatus === RequestStatus.Error && ( -

- -

- )} - + + +
+ + + + +
-
+ +
+
+

- +

- - - - + + + + - - - {props.nodeRedEnabled && ( - - )} - + + {nodeRedEnabled && ( + )} - - - - - + + + + - + )} + +
- - - {props.nodeRedEnabled && 'Node-red'}
+ + + {nodeRedEnabled && 'Node-red'}
- {`Gladys`} - -
- -
-
- {props.nodeRedEnabled && ( +
{`Node-red`} + +
+ +
+
-
- -
-
- - {props.nodeRedRunning && ( - - + + {nodeRedEnabled && ( + {`Node-red`} + )} +
+
+ +
+
+ + {nodeRedRunning && ( + + - )} -

- +

- - - - + + + + - - - + + - - - - + + + + - + )} + +
- - - -
+ + + +
- - +
+ + - + -
- - - {props.nodeRedRunning && ( - - +
+ + + {nodeRedRunning && ( + + - )} -
diff --git a/front/src/routes/integration/all/node-red/setup-page/actions.js b/front/src/routes/integration/all/node-red/setup-page/actions.js deleted file mode 100644 index f96ed02105..0000000000 --- a/front/src/routes/integration/all/node-red/setup-page/actions.js +++ /dev/null @@ -1,91 +0,0 @@ -import createActionsIntegration from '../../../../../actions/integration'; -import { RequestStatus } from '../../../../../utils/consts'; -import dayjs from 'dayjs'; -import relativeTime from 'dayjs/plugin/relativeTime'; -import get from 'get-value'; - -dayjs.extend(relativeTime); - -const createActions = store => { - const integrationActions = createActionsIntegration(store); - const actions = { - async checkStatus(state) { - let nodeRedStatus = { - nodeRedExist: false, - nodeRedRunning: false, - nodeRedEnabled: false, - dockerBased: false, - networkModeValid: false, - }; - try { - nodeRedStatus = await state.httpClient.get('/api/v1/service/node-red/status'); - } finally { - store.setState({ - nodeRedExist: nodeRedStatus.nodeRedExist, - nodeRedRunning: nodeRedStatus.nodeRedRunning, - nodeRedEnabled: nodeRedStatus.nodeRedEnabled, - dockerBased: nodeRedStatus.dockerBased, - networkModeValid: nodeRedStatus.networkModeValid, - }); - } - }, - - async startContainer(state) { - let nodeRedEnabled = true; - let error = false; - - store.setState({ - nodeRedEnabled, - nodeRedStatus: RequestStatus.Getting - }); - - await state.httpClient.post('/api/v1/service/node-red/variable/NODERED_ENABLED', { - value: nodeRedEnabled - }); - - try { - await state.httpClient.post('/api/v1/service/node-red/connect'); - } catch (e) { - error = error | get(e, 'response.status'); - } - - if (error) { - store.setState({ - nodeRedStatus: RequestStatus.Error, - nodeRedEnabled: false - }); - } else { - store.setState({ - nodeRedStatus: RequestStatus.Success, - nodeRedEnabled: true - }); - } - - await this.checkStatus(); - }, - - async stopContainer(state) { - let error = false; - try { - await state.httpClient.post('/api/v1/service/node-red/disconnect'); - } catch (e) { - error = error | get(e, 'response.status'); - } - - if (error) { - store.setState({ - nodeRedStatus: RequestStatus.Error - }); - } else { - store.setState({ - nodeRedStatus: RequestStatus.Success - }); - } - - await this.checkStatus(); - } - }; - return Object.assign({}, actions, integrationActions); -}; - -export default createActions; diff --git a/front/src/routes/integration/all/node-red/setup-page/index.js b/front/src/routes/integration/all/node-red/setup-page/index.js index b382ef92c5..80c1717a55 100644 --- a/front/src/routes/integration/all/node-red/setup-page/index.js +++ b/front/src/routes/integration/all/node-red/setup-page/index.js @@ -1,27 +1,9 @@ import { Component } from 'preact'; import { connect } from 'unistore/preact'; -import actions from './actions'; import NodeRedPage from '../NodeRedPage'; import SetupTab from './SetupTab'; -import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants'; class NodeRedSetupPage extends Component { - async componentWillMount() { - this.props.session.dispatcher.addListener( - WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, - this.props.checkStatus - ); - - await this.props.checkStatus(); - } - - componentWillUnmount() { - this.props.session.dispatcher.removeListener( - WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, - this.props.checkStatus - ); - } - render(props, {}) { return ( @@ -31,7 +13,4 @@ class NodeRedSetupPage extends Component { } } -export default connect( - 'user,session,nodeRedExist,nodeRedRunning,nodeRedEnabled,gladysConnected,dockerBased,networkModeValid', - actions -)(NodeRedSetupPage); +export default connect('user,session,httpClient', {})(NodeRedSetupPage); diff --git a/front/src/routes/integration/all/node-red/setup-page/style.css b/front/src/routes/integration/all/node-red/setup-page/style.css index 117e83421d..7bc7460d98 100644 --- a/front/src/routes/integration/all/node-red/setup-page/style.css +++ b/front/src/routes/integration/all/node-red/setup-page/style.css @@ -4,6 +4,15 @@ align-items: center; } +.textAlignMiddleContainer { + display: table; +} + +.textAlignMiddle { + display: table-cell; + vertical-align: middle; +} + .greenIcon { color: #5eba00;; font-size: 24px; @@ -20,4 +29,4 @@ border-color: #555; height: 1px; width: 40px; -} \ No newline at end of file +} diff --git a/front/src/routes/integration/all/zigbee2mqtt/commons/CheckStatus.js b/front/src/routes/integration/all/zigbee2mqtt/commons/CheckStatus.js index 17c380ca47..534a27567e 100644 --- a/front/src/routes/integration/all/zigbee2mqtt/commons/CheckStatus.js +++ b/front/src/routes/integration/all/zigbee2mqtt/commons/CheckStatus.js @@ -4,8 +4,7 @@ import { connect } from 'unistore/preact'; import actions from './actions'; import { Text } from 'preact-i18n'; -class -CheckStatus extends Component { +class CheckStatus extends Component { componentWillMount() { this.props.checkStatus(); } diff --git a/server/lib/system/system.getGladysBasePath.js b/server/lib/system/system.getGladysBasePath.js index 194c347a3c..77375a61fb 100644 --- a/server/lib/system/system.getGladysBasePath.js +++ b/server/lib/system/system.getGladysBasePath.js @@ -1,3 +1,5 @@ +const logger = require('../../utils/logger'); + /** * @description Compute basePath in host and container from mounted point or give default ones. * @returns {Promise} Base path in host/container to store files. @@ -11,16 +13,20 @@ async function getGladysBasePath() { const base = process.env.SQLITE_FILE_PATH; basePathOnContainer = base.substring(0, base.lastIndexOf('/')); } - // Find mount linked to this path to fetch host path - const currentContainerId = await this.getGladysContainerId(); - const gladysMounts = await this.getContainerMounts(currentContainerId); - if (gladysMounts) { - const baseMount = gladysMounts.find((mount) => { - return mount.Destination === basePathOnContainer; - }); - if (baseMount) { - return { basePathOnContainer, basePathOnHost: baseMount.Source }; + try { + // Find mount linked to this path to fetch host path + const currentContainerId = await this.getGladysContainerId(); + const gladysMounts = await this.getContainerMounts(currentContainerId); + if (gladysMounts) { + const baseMount = gladysMounts.find((mount) => { + return mount.Destination === basePathOnContainer; + }); + if (baseMount) { + return { basePathOnContainer, basePathOnHost: baseMount.Source }; + } } + } catch (e) { + logger.warn(`NodeRed: Error while fetching container mounts: ${e.message}`); } return { basePathOnContainer, basePathOnHost: '/var/lib/gladysassistant' }; } diff --git a/server/lib/system/system.getNetworkMode.js b/server/lib/system/system.getNetworkMode.js index db05965093..5b8be9e5d0 100644 --- a/server/lib/system/system.getNetworkMode.js +++ b/server/lib/system/system.getNetworkMode.js @@ -12,7 +12,6 @@ async function getNetworkMode() { throw new PlatformNotCompatible('SYSTEM_NOT_RUNNING_DOCKER'); } - return 'host'; if (!this.networkMode) { const containerId = await this.getGladysContainerId(); const gladysContainer = this.dockerode.getContainer(containerId); diff --git a/server/lib/system/system.isDocker.js b/server/lib/system/system.isDocker.js index a47e0f1c9b..5f1699ec7c 100644 --- a/server/lib/system/system.isDocker.js +++ b/server/lib/system/system.isDocker.js @@ -7,8 +7,6 @@ const fs = require('fs'); * isDocker(); */ function isDocker() { - return Promise.resolve(true); - return new Promise((resolve) => { fs.access('/.dockerenv', fs.constants.F_OK, (err) => { if (err) { diff --git a/server/services/node-red/api/node-red.controller.js b/server/services/node-red/api/node-red.controller.js index 2f44412f2f..b8903c1ad8 100644 --- a/server/services/node-red/api/node-red.controller.js +++ b/server/services/node-red/api/node-red.controller.js @@ -2,7 +2,6 @@ const asyncMiddleware = require('../../../api/middlewares/asyncMiddleware'); const logger = require('../../../utils/logger'); module.exports = function NodeRedController(gladys, nodeRedManager) { - /** * @api {get} /api/v1/service/node-red/status Get node-red connection status * @apiName status @@ -14,19 +13,6 @@ module.exports = function NodeRedController(gladys, nodeRedManager) { res.json(response); } - /** - * @api {post} /api/v1/service/node-red/setup Setup - * @apiName setup - * @apiGroup Node-red - */ - async function setup(req, res) { - logger.debug('Entering setup step'); - await nodeRedManager.setup(req.body); - res.json({ - success: true, - }); - } - /** * @api {post} /api/v1/service/node-red/connect Connect * @apiName connect @@ -71,10 +57,6 @@ module.exports = function NodeRedController(gladys, nodeRedManager) { authenticated: true, controller: asyncMiddleware(status), }, - 'post /api/v1/service/node-red/setup': { - authenticated: true, - controller: asyncMiddleware(setup), - }, 'post /api/v1/service/node-red/connect': { authenticated: true, controller: asyncMiddleware(connect), diff --git a/server/services/node-red/docker/gladys-node-red-container.json b/server/services/node-red/docker/gladys-node-red-container.json index ab4e10d958..b0ba02f2f0 100644 --- a/server/services/node-red/docker/gladys-node-red-container.json +++ b/server/services/node-red/docker/gladys-node-red-container.json @@ -5,9 +5,7 @@ "1880/tcp": {} }, "HostConfig": { - "Binds": [ - "/var/lib/gladysassistant/node-red:/data" - ], + "Binds": ["/var/lib/gladysassistant/node-red:/data"], "LogConfig": { "Type": "json-file", "Config": { @@ -24,7 +22,7 @@ "RestartPolicy": { "Name": "always" }, - "NetworkMode": "host", + "Dns": [], "DnsOptions": [], "DnsSearch": [], diff --git a/server/services/node-red/docker/settings.txt b/server/services/node-red/docker/settings.txt new file mode 100644 index 0000000000..a62dd14475 --- /dev/null +++ b/server/services/node-red/docker/settings.txt @@ -0,0 +1,597 @@ +/** + * This is the default settings file provided by Node-RED. + * + * It can contain any valid JavaScript code that will get run when Node-RED + * is started. + * + * Lines that start with // are commented out. + * Each entry should be separated from the entries above and below by a comma ','. + * + * For more information about individual settings, refer to the documentation: + * https://nodered.org/docs/user-guide/runtime/configuration. + * + * The settings are split into the following sections: + * - Flow File and User Directory Settings + * - Security + * - Server Settings + * - Runtime Settings + * - Editor Settings + * - Node Settings. + * + */ + +module.exports = { + +/** + * *****************************************************************************. + * Flow File and User Directory Settings + * - flowFile + * - credentialSecret + * - flowFilePretty + * - userDir + * - nodesDir + *****************************************************************************. + */ + + /** The file containing the flows. If not set, defaults to flows_.json */ + flowFile: 'flows.json', + + /** + * By default, credentials are encrypted in storage using a generated key. To + * specify your own secret, set the following property. + * If you want to disable encryption of credentials, set this property to false. + * Note: once you set this property, do not change it - doing so will prevent + * node-red from being able to decrypt your existing credentials and they will be + * lost. + */ + // credentialSecret: "a-secret-key", + + /** + * By default, the flow JSON will be formatted over multiple lines making + * it easier to compare changes when using version control. + * To disable pretty-printing of the JSON set the following property to false. + */ + flowFilePretty: true, + + /** + * By default, all user data is stored in a directory called `.node-red` under + * the user's home directory. To use a different location, the following + * property can be used. + */ + // userDir: '/home/nol/.node-red/', + + /** + * Node-RED scans the `nodes` directory in the userDir to find local node files. + * The following property can be used to specify an additional directory to scan. + */ + // nodesDir: '/home/nol/.node-red/nodes', + +/** + * *****************************************************************************. + * Security + * - adminAuth + * - https + * - httpsRefreshInterval + * - requireHttps + * - httpNodeAuth + * - httpStaticAuth + *****************************************************************************. + */ + + /** + * To password protect the Node-RED editor and admin API, the following + * property can be used. See http://nodered.org/docs/security.html for details. + */ + adminAuth: { + type: 'credentials', + users: [{ + username: 'admin', + password: '$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.', + permissions: '*' + }] + }, + + /** + * The following property can be used to enable HTTPS + * This property can be either an object, containing both a (private) key + * and a (public) certificate, or a function that returns such an object. + * See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener + * for details of its contents. + */ + + /** Option 1: static object */ + // https: { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + // }, + + /** Option 2: function that returns the HTTP configuration object */ + // https: function() { + // // This function should return the options object, or a Promise + // // that resolves to the options object + // return { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + // } + // }, + + /** + * If the `https` setting is a function, the following setting can be used + * to set how often, in hours, the function will be called. That can be used + * to refresh any certificates. + */ + // httpsRefreshInterval : 12, + + /** + * The following property can be used to cause insecure HTTP connections to + * be redirected to HTTPS. + */ + // requireHttps: true, + + /** + * To password protect the node-defined HTTP endpoints (httpNodeRoot), + * including node-red-dashboard, or the static content (httpStatic), the + * following properties can be used. + * The `pass` field is a bcrypt hash of the password. + * See http://nodered.org/docs/security.html#generating-the-password-hash. + */ + // httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + // httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + +/** + * *****************************************************************************. + * Server Settings + * - uiPort + * - uiHost + * - apiMaxLength + * - httpServerOptions + * - httpAdminRoot + * - httpAdminMiddleware + * - httpNodeRoot + * - httpNodeCors + * - httpNodeMiddleware + * - httpStatic + * - httpStaticRoot + *****************************************************************************. + */ + + /** The tcp port that the Node-RED web server is listening on */ + uiPort: process.env.PORT || 1880, + + /** + * By default, the Node-RED UI accepts connections on all IPv4 interfaces. + * To listen on all IPv6 addresses, set uiHost to "::", + * The following property can be used to listen on a specific interface. For + * example, the following would only allow connections from the local machine. + */ + uiHost: "0.0.0.0", + + /** + * The maximum size of HTTP request that will be accepted by the runtime api. + * Default: 5mb. + */ + // apiMaxLength: '5mb', + + /** + * The following property can be used to pass custom options to the Express.js + * server used by Node-RED. For a full list of available options, refer + * to http://expressjs.com/en/api.html#app.settings.table. + */ + // httpServerOptions: { }, + + /** + * By default, the Node-RED UI is available at http://localhost:1880/ + * The following property can be used to specify a different root path. + * If set to false, this is disabled. + */ + // httpAdminRoot: '/admin', + + /** + * The following property can be used to add a custom middleware function + * in front of all admin http routes. For example, to set custom http + * headers. It can be a single function or an array of middleware functions. + */ + // httpAdminMiddleware: function(req,res,next) { + // // Set the X-Frame-Options header to limit where the editor + // // can be embedded + // //res.set('X-Frame-Options', 'sameorigin'); + // next(); + // }, + + + /** + * Some nodes, such as HTTP In, can be used to listen for incoming http requests. + * By default, these are served relative to '/'. The following property + * can be used to specifiy a different root path. If set to false, this is + * disabled. + */ + // httpNodeRoot: '/red-nodes', + + /** + * The following property can be used to configure cross-origin resource sharing + * in the HTTP nodes. + * See https://github.com/troygoode/node-cors#configuration-options for + * details on its contents. The following is a basic permissive set of options: + */ + // httpNodeCors: { + // origin: "*", + // methods: "GET,PUT,POST,DELETE" + // }, + + /** + * If you need to set an http proxy please set an environment variable + * called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system. + * For example - http_proxy=http://myproxy.com:8080 + * (Setting it here will have no effect) + * You may also specify no_proxy (or NO_PROXY) to supply a comma separated + * list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk. + */ + + /** + * The following property can be used to add a custom middleware function + * in front of all http in nodes. This allows custom authentication to be + * applied to all http in nodes, or any other sort of common request processing. + * It can be a single function or an array of middleware functions. + */ + // httpNodeMiddleware: function(req,res,next) { + // // Handle/reject the request, or pass it on to the http in node by calling next(); + // // Optionally skip our rawBodyParser by setting this to true; + // //req.skipRawBodyParser = true; + // next(); + // }, + + /** + * When httpAdminRoot is used to move the UI to a different root path, the + * following property can be used to identify a directory of static content + * that should be served at http://localhost:1880/. + * When httpStaticRoot is set differently to httpAdminRoot, there is no need + * to move httpAdminRoot. + */ + // httpStatic: '/home/nol/node-red-static/', //single static source + /* OR multiple static sources can be created using an array of objects... */ + // httpStatic: [ + // {path: '/home/nol/pics/', root: "/img/"}, + // {path: '/home/nol/reports/', root: "/doc/"}, + // ], + + /** + * All static routes will be appended to httpStaticRoot + * e.g. If httpStatic = "/home/nol/docs" and httpStaticRoot = "/static/" + * then "/home/nol/docs" will be served at "/static/" + * e.g. if httpStatic = [{path: '/home/nol/pics/', root: "/img/"}] + * and httpStaticRoot = "/static/" + * then "/home/nol/pics/" will be served at "/static/img/". + */ + // httpStaticRoot: '/static/', + +/** + * *****************************************************************************. + * Runtime Settings + * - lang + * - runtimeState + * - diagnostics + * - logging + * - contextStorage + * - exportGlobalContextKeys + * - externalModules + *****************************************************************************. + */ + + /** + * Uncomment the following to run node-red in your preferred language. + * Available languages include: en-US (default), ja, de, zh-CN, zh-TW, ru, ko + * Some languages are more complete than others. + */ + // lang: "de", + + /** + * Configure diagnostics options + * - enabled: When `enabled` is `true` (or unset), diagnostics data will + * be available at http://localhost:1880/diagnostics + * - ui: When `ui` is `true` (or unset), the action `show-system-info` will + * be available to logged in users of node-red editor. + */ + diagnostics: { + /** Enable or disable diagnostics endpoint. Must be set to `false` to disable */ + enabled: true, + /** Enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ + ui: true, + }, + /** + * Configure runtimeState options + * - enabled: When `enabled` is `true` flows runtime can be Started/Stoped + * by POSTing to available at http://localhost:1880/flows/state + * - ui: When `ui` is `true`, the action `core:start-flows` and + * `core:stop-flows` will be available to logged in users of node-red editor + * Also, the deploy menu (when set to default) will show a stop or start button. + */ + runtimeState: { + /** Enable or disable flows/state endpoint. Must be set to `false` to disable */ + enabled: false, + /** Show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */ + ui: false, + }, + /** Configure the logging output */ + logging: { + /** Only console logging is currently supported */ + console: { + /** + * Level of logging to be recorded. Options are: + * fatal - only those errors which make the application unusable should be recorded + * error - record errors which are deemed fatal for a particular request + fatal errors + * warn - record problems which are non fatal + errors + fatal errors + * info - record information about the general running of the application + warn + error + fatal errors + * debug - record information which is more verbose than info + info + warn + error + fatal errors + * trace - record very detailed logging + debug + info + warn + error + fatal errors + * off - turn off all logging (doesn't affect metrics or audit). + */ + level: 'info', + /** Whether or not to include metric events in the log output */ + metrics: false, + /** Whether or not to include audit events in the log output */ + audit: false + } + }, + + /** + * Context Storage + * The following property can be used to enable context storage. The configuration + * provided here will enable file-based context that flushes to disk every 30 seconds. + * Refer to the documentation for further options: https://nodered.org/docs/api/context/. + */ + // contextStorage: { + // default: { + // module:"localfilesystem" + // }, + // }, + + /** + * `global.keys()` returns a list of all properties set in global context. + * This allows them to be displayed in the Context Sidebar within the editor. + * In some circumstances it is not desirable to expose them to the editor. The + * following property can be used to hide any property set in `functionGlobalContext` + * from being list by `global.keys()`. + * By default, the property is set to false to avoid accidental exposure of + * their values. Setting this to true will cause the keys to be listed. + */ + exportGlobalContextKeys: false, + + /** + * Configure how the runtime will handle external npm modules. + * This covers: + * - whether the editor will allow new node modules to be installed + * - whether nodes, such as the Function node are allowed to have their + * own dynamically configured dependencies. + * The allow/denyList options can be used to limit what modules the runtime + * will install/load. It can use '*' as a wildcard that matches anything. + */ + externalModules: { + // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ + // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ + // palette: { /** Configuration for the Palette Manager */ + // allowInstall: true, /** Enable the Palette Manager in the editor */ + // allowUpdate: true, /** Allow modules to be updated in the Palette Manager */ + // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ + // allowList: ['*'], + // denyList: [], + // allowUpdateList: ['*'], + // denyUpdateList: [] + // }, + // modules: { /** Configuration for node-specified modules */ + // allowInstall: true, + // allowList: [], + // denyList: [] + // } + }, + + +/** + * *****************************************************************************. + * Editor Settings + * - disableEditor + * - editorTheme + *****************************************************************************. + */ + + /** + * The following property can be used to disable the editor. The admin API + * is not affected by this option. To disable both the editor and the admin + * API, use either the httpRoot or httpAdminRoot properties. + */ + // disableEditor: false, + + /** + * Customising the editor + * See https://nodered.org/docs/user-guide/runtime/configuration#editor-themes + * for all available options. + */ + editorTheme: { + /** + * The following property can be used to set a custom theme for the editor. + * See https://github.com/node-red-contrib-themes/theme-collection for + * a collection of themes to chose from. + */ + // theme: "", + + /** + * To disable the 'Welcome to Node-RED' tour that is displayed the first + * time you access the editor for each release of Node-RED, set this to false. + */ + // tours: false, + + palette: { + /** + * The following property can be used to order the categories in the editor + * palette. If a node's category is not in the list, the category will get + * added to the end of the palette. + * If not set, the following default order is used: + */ + // categories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'], + }, + + projects: { + /** To enable the Projects feature, set this value to true */ + enabled: false, + workflow: { + /** + * Set the default projects workflow mode. + * - manual - you must manually commit changes + * - auto - changes are automatically committed + * This can be overridden per-user from the 'Git config' + * section of 'User Settings' within the editor. + */ + mode: 'manual' + } + }, + + codeEditor: { + /** + * Select the text editor component used by the editor. + * As of Node-RED V3, this defaults to "monaco", but can be set to "ace" if desired. + */ + lib: 'monaco', + options: { + /** + * The follow options only apply if the editor is set to "monaco". + * + * Theme - must match the file name of a theme in + * packages/node_modules/@node-red/editor-client/src/vendor/monaco/dist/theme + * e.g. "tomorrow-night", "upstream-sunburst", "github", "my-theme". + */ + // theme: "vs", + /** + * Other overrides can be set e.g. FontSize, fontFamily, fontLigatures etc. + * For the full list, see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html. + */ + // fontSize: 14, + // fontFamily: "Cascadia Code, Fira Code, Consolas, 'Courier New', monospace", + // fontLigatures: true, + } + } + }, + +/** + * *****************************************************************************. + * Node Settings + * - fileWorkingDirectory + * - functionGlobalContext + * - functionExternalModules + * - nodeMessageBufferMaxLength + * - ui (for use with Node-RED Dashboard) + * - debugUseColors + * - debugMaxLength + * - execMaxBufferSize + * - httpRequestTimeout + * - mqttReconnectTime + * - serialReconnectTime + * - socketReconnectTime + * - socketTimeout + * - tcpMsgQueueSize + * - inboundWebSocketTimeout + * - tlsConfigDisableLocalFiles + * - webSocketNodeVerifyClient + *****************************************************************************. + */ + + /** + * The working directory to handle relative file paths from within the File nodes + * defaults to the working directory of the Node-RED process. + */ + // fileWorkingDirectory: "", + + /** Allow the Function node to load additional npm modules directly */ + functionExternalModules: true, + + /** + * The following property can be used to set predefined values in Global Context. + * This allows extra node modules to be made available with in Function node. + * For example, the following: + * functionGlobalContext: { os:require('os') } + * will allow the `os` module to be accessed in a Function node using: + * global.get("os"). + */ + functionGlobalContext: { + // os:require('os'), + }, + + /** + * The maximum number of messages nodes will buffer internally as part of their + * operation. This applies across a range of nodes that operate on message sequences. + * Defaults to no limit. A value of 0 also means no limit is applied. + */ + // nodeMessageBufferMaxLength: 0, + + /** + * If you installed the optional node-red-dashboard you can set it's path + * relative to httpNodeRoot + * Other optional properties include + * readOnly:{boolean}, + * middleware:{function or array}, (req,res,next) - http middleware + * ioMiddleware:{function or array}, (socket,next) - socket.io middleware. + */ + // ui: { path: "ui" }, + + /** Colourise the console output of the debug node */ + // debugUseColors: true, + + /** The maximum length, in characters, of any message sent to the debug sidebar tab */ + debugMaxLength: 1000, + + /** Maximum buffer size for the exec node. Defaults to 10Mb */ + // execMaxBufferSize: 10000000, + + /** Timeout in milliseconds for HTTP request connections. Defaults to 120s */ + // httpRequestTimeout: 120000, + + /** Retry time in milliseconds for MQTT connections */ + mqttReconnectTime: 15000, + + /** Retry time in milliseconds for Serial port connections */ + serialReconnectTime: 15000, + + /** Retry time in milliseconds for TCP socket connections */ + // socketReconnectTime: 10000, + + /** Timeout in milliseconds for TCP server socket connections. Defaults to no timeout */ + // socketTimeout: 120000, + + /** + * Maximum number of messages to wait in queue while attempting to connect to TCP socket + * defaults to 1000. + */ + // tcpMsgQueueSize: 2000, + + /** + * Timeout in milliseconds for inbound WebSocket connections that do not + * match any configured node. Defaults to 5000. + */ + // inboundWebSocketTimeout: 5000, + + /** + * To disable the option for using local files for storing keys and + * certificates in the TLS configuration node, set this to true. + */ + // tlsConfigDisableLocalFiles: true, + + /** + * The following property can be used to verify websocket connection attempts. + * This allows, for example, the HTTP request headers to be checked to ensure + * they include valid authentication information. + */ + // webSocketNodeVerifyClient: function(info) { + // /** 'info' has three properties: + // * - origin : the value in the Origin header + // * - req : the HTTP request + // * - secure : true if req.connection.authorized or req.connection.encrypted is set + // * + // * The function should return true if the connection should be accepted, false otherwise. + // * + // * Alternatively, if this function is defined to accept a second argument, callback, + // * it can be used to verify the client asynchronously. + // * The callback takes three arguments: + // * - result : boolean, whether to accept the connection or not + // * - code : if result is false, the HTTP error status to return + // * - reason: if result is false, the HTTP reason string to return + // */ + // }, +}; diff --git a/server/services/node-red/lib/backup.js b/server/services/node-red/lib/backup.js deleted file mode 100644 index 66b1085a41..0000000000 --- a/server/services/node-red/lib/backup.js +++ /dev/null @@ -1,50 +0,0 @@ -const logger = require('../../../utils/logger'); -const { ServiceNotConfiguredError } = require('../../../utils/coreErrors'); - -/** - * @description Create a Node-red backup. - * @param {string} jobId - The job id. - * @returns {Promise} - Resolve when backup is finished. - * @example - * backup('aaf45861-c7f5-47ac-bde1-cfe56c7789cf'); - */ -async function backup(jobId) { - // TODO Make backup - - // Backup is not possible when service is not running - if (!this.isEnabled()) { - throw new ServiceNotConfiguredError('SERVICE_NOT_CONFIGURED'); - } - - const finishJob = (func, timer, args) => { - if (timer) { - clearTimeout(timer); - } - - // reset pending job - this.backupJob = {}; - return func(args); - }; - - const response = new Promise((resolve, reject) => { - // Prevent infinite wait - const timerId = setTimeout(finishJob, 30000, reject, null, "Backup request time's out"); - - this.backupJob = { - resolve: (args) => finishJob(resolve, timerId, args), - reject: (args) => finishJob(reject, timerId, args), - jobId, - }; - }); - - // Request z2m to generate a new backup. - logger.info('Node-red: request for backup'); - await this.gladys.job.updateProgress(jobId, 30); - - - return response; -} - -module.exports = { - backup, -}; diff --git a/server/services/node-red/lib/checkForContainerUpdates.js b/server/services/node-red/lib/checkForContainerUpdates.js index 144e9c5a52..103f9db75f 100644 --- a/server/services/node-red/lib/checkForContainerUpdates.js +++ b/server/services/node-red/lib/checkForContainerUpdates.js @@ -12,7 +12,7 @@ const nodeRedContainerDescriptor = require('../docker/gladys-node-red-container. async function checkForContainerUpdates(config) { logger.info('NodeRed: Checking for current installed versions and required updates...'); - // Check for MQTT container version + // Check for NodeRed container version if (config.dockerNodeRedVersion !== DEFAULT.DOCKER_NODE_RED_VERSION) { logger.info(`NodeRed: update #${DEFAULT.DOCKER_NODE_RED_VERSION} of the container required...`); @@ -33,7 +33,6 @@ async function checkForContainerUpdates(config) { config.dockerNodeRedVersion = DEFAULT.DOCKER_NODE_RED_VERSION; logger.info(`NodeRed: update #${DEFAULT.DOCKER_NODE_RED_VERSION} of the container done`); } - } module.exports = { diff --git a/server/services/node-red/lib/configureContainer.js b/server/services/node-red/lib/configureContainer.js index 34e58517e1..f9fff18a57 100644 --- a/server/services/node-red/lib/configureContainer.js +++ b/server/services/node-red/lib/configureContainer.js @@ -1,29 +1,24 @@ -// const fs = require('fs/promises'); -// const { constants } = require('fs'); -// const path = require('path'); -// const yaml = require('yaml'); - +const fs = require('fs/promises'); +const { constants } = require('fs'); +const path = require('path'); +const bcrypt = require('bcrypt'); const logger = require('../../../utils/logger'); -// const { DEFAULT } = require('./constants'); -// const { DEFAULT_KEY, CONFIG_KEYS, ADAPTERS_BY_CONFIG_KEY } = require('../adapters'); /** * @description Configure Node-red container. - * @param {string} basePathOnContainer - Node-red base path. * @param {object} config - Gladys Node-red stored configuration. * @returns {Promise} Indicates if the configuration file has been created or modified. * @example * await this.configureContainer({}); */ -async function configureContainer(basePathOnContainer, config) { +async function configureContainer(config) { logger.info('NodeRed: Docker container is being configured...'); - // TODO NEEDED ? - /* + const { basePathOnHost } = await this.gladys.system.getGladysBasePath(); + // Create configuration path (if not exists) - const configFilepath = path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH); + const configFilepath = path.join(basePathOnHost, 'node-red', 'settings.js'); await fs.mkdir(path.dirname(configFilepath), { recursive: true }); - // Check if config file not already exists let configCreated = false; try { @@ -32,46 +27,31 @@ async function configureContainer(basePathOnContainer, config) { logger.info('NodeRed: configuration file already exists.'); } catch (e) { logger.info('NodeRed: Writting default configuration...'); - await fs.writeFile(configFilepath, yaml.stringify(DEFAULT.CONFIGURATION_CONTENT)); + await fs.copyFile(path.join(__dirname, '../docker/settings.txt'), configFilepath); configCreated = true; } // Check for changes const fileContent = await fs.readFile(configFilepath); - const loadedConfig = yaml.parse(fileContent.toString()); - const { mqtt = {} } = loadedConfig; + let fileContentString = fileContent.toString(); - let configChanged = false; - if (mqtt.user !== config.mqttUsername || mqtt.password !== config.mqttPassword) { - mqtt.user = config.mqttUsername; - mqtt.password = config.mqttPassword; - loadedConfig.mqtt = mqtt; - configChanged = true; - } - - // Setup adapter - const adapterKey = Object.values(CONFIG_KEYS).find((configKey) => - ADAPTERS_BY_CONFIG_KEY[configKey].includes(config.z2mDongleName), - ); - const adapterSetup = adapterKey && adapterKey !== DEFAULT_KEY; - const { serial = {} } = loadedConfig; + const encodedPassword = bcrypt.hashSync(config.nodeRedPassword, 8); + const [, username] = fileContentString.match(/username: '(.+)'/); + const [, password] = fileContentString.match(/password: '(.+)'/); - if (!adapterSetup && serial.adapter) { - delete loadedConfig.serial.adapter; - configChanged = true; - } else if (adapterSetup && serial.adapter !== adapterKey) { - loadedConfig.serial.adapter = adapterKey; + let configChanged = false; + if (username !== config.nodeRedUsername || password !== encodedPassword) { + fileContentString = fileContentString.replace(/username: '(.+)'/, `username: '${config.nodeRedUsername}'`); + fileContentString = fileContentString.replace(/password: '(.+)'/, `password: '${encodedPassword}'`); configChanged = true; } if (configChanged) { - logger.info('NodeRed: Writting MQTT and USB adapter information to configuration...'); - await fs.writeFile(configFilepath, yaml.stringify(loadedConfig)); + logger.info('NodeRed: Writting configuration...'); + await fs.writeFile(configFilepath, fileContentString); } return configCreated || configChanged; - */ - return false; } module.exports = { diff --git a/server/services/node-red/lib/constants.js b/server/services/node-red/lib/constants.js index 361a61aaf8..7cdbd08b7d 100644 --- a/server/services/node-red/lib/constants.js +++ b/server/services/node-red/lib/constants.js @@ -1,16 +1,16 @@ const CONFIGURATION = { - Z2M_DRIVER_PATH: 'ZIGBEE2MQTT_DRIVER_PATH', - Z2M_BACKUP: 'Z2M_BACKUP', - ZIGBEE_DONGLE_NAME: 'ZIGBEE_DONGLE_NAME', - MQTT_URL_KEY: 'Z2M_MQTT_URL', - MQTT_URL_VALUE: 'mqtt://localhost:1884', + NODE_RED_USERNAME_VALUE: 'admin', + NODE_RED_URL_VALUE: 'http://localhost:1880', + + NODE_RED_USERNAME: 'NODE_RED_USERNAME', + NODE_RED_PASSWORD: 'NODE_RED_PASSWORD', + NODE_RED_URL: 'NODE_RED_URL', DOCKER_NODE_RED_VERSION: 'DOCKER_NODE_RED_VERSION', // Variable to identify last version of NodeRed docker file is installed }; const DEFAULT = { - DOCKER_NODE_RED_VERSION: '2 ', // Last version of NodeRed docker file, - + DOCKER_NODE_RED_VERSION: '2', // Last version of NodeRed docker file, }; module.exports = { diff --git a/server/services/node-red/lib/disconnect.js b/server/services/node-red/lib/disconnect.js index 09123cd472..a48613fc3b 100644 --- a/server/services/node-red/lib/disconnect.js +++ b/server/services/node-red/lib/disconnect.js @@ -11,29 +11,27 @@ const nodeRedContainerDescriptor = require('../docker/gladys-node-red-container. async function disconnect() { let container; - // Stop backup reccurent job - if (this.backupScheduledJob) { - this.backupScheduledJob.cancel(); - } - this.gladysConnected = false; this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); // Stop NodeRed container - const dockerContainer = await this.gladys.system.getContainers({ - all: true, - filters: { name: [nodeRedContainerDescriptor.name] }, - }); - [container] = dockerContainer; - await this.gladys.system.stopContainer(container.id); + try { + const dockerContainer = await this.gladys.system.getContainers({ + all: true, + filters: { name: [nodeRedContainerDescriptor.name] }, + }); + [container] = dockerContainer; + await this.gladys.system.stopContainer(container.id); + } catch (e) { + logger.warn(`NodeRed: failed to stop container ${container.id}:`, e); + } + this.nodeRedRunning = false; this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); - - } module.exports = { diff --git a/server/services/node-red/lib/getConfiguration.js b/server/services/node-red/lib/getConfiguration.js index 23f521a9f5..451239be1c 100644 --- a/server/services/node-red/lib/getConfiguration.js +++ b/server/services/node-red/lib/getConfiguration.js @@ -11,12 +11,20 @@ const { CONFIGURATION } = require('./constants'); async function getConfiguration() { logger.debug('NodeRed: loading stored configuration...'); + const nodeRedUsername = await this.gladys.variable.getValue(CONFIGURATION.NODE_RED_USERNAME, this.serviceId); + const nodeRedPassword = await this.gladys.variable.getValue(CONFIGURATION.NODE_RED_PASSWORD, this.serviceId); + // Load version parameters - const dockerNodeRedVersion = await this.gladys.variable.getValue(CONFIGURATION.DOCKER_NODE_RED_VERSION, this.serviceId); + const dockerNodeRedVersion = await this.gladys.variable.getValue( + CONFIGURATION.DOCKER_NODE_RED_VERSION, + this.serviceId, + ); // Gladys params const timezone = await this.gladys.variable.getValue(SYSTEM_VARIABLE_NAMES.TIMEZONE); return { + nodeRedUsername, + nodeRedPassword, dockerNodeRedVersion, timezone, }; diff --git a/server/services/node-red/lib/index.js b/server/services/node-red/lib/index.js index 18314c83c0..15dd067481 100644 --- a/server/services/node-red/lib/index.js +++ b/server/services/node-red/lib/index.js @@ -2,18 +2,11 @@ const { init } = require('./init'); const { getConfiguration } = require('./getConfiguration'); const { saveConfiguration } = require('./saveConfiguration'); const { installContainer } = require('./installContainer'); -const { backup } = require('./backup'); const { checkForContainerUpdates } = require('./checkForContainerUpdates'); const { disconnect } = require('./disconnect'); const { isEnabled } = require('./isEnabled'); const { status } = require('./status'); - - const { configureContainer } = require('./configureContainer'); -const { saveZ2mBackup } = require('./saveZ2mBackup'); -const { restoreZ2mBackup } = require('./restoreZ2mBackup'); - -const { JOB_TYPES } = require('../../../utils/constants'); /** * @description Add ability to connect to Node-red. @@ -28,33 +21,22 @@ const NodeRedManager = function NodeRedManager(gladys, serviceId) { this.nodeRedExist = false; this.nodeRedRunning = false; - this.nodeRedConnected = false; this.gladysConnected = false; this.networkModeValid = true; this.dockerBased = true; this.containerRestartWaitTimeInMs = 5 * 1000; - - this.backup = gladys.job.wrapper(JOB_TYPES.SERVICE_NODE_RED_BACKUP, this.backup.bind(this)); - this.backupJob = {}; - this.backupScheduledJob = null; }; - NodeRedManager.prototype.init = init; NodeRedManager.prototype.getConfiguration = getConfiguration; NodeRedManager.prototype.saveConfiguration = saveConfiguration; NodeRedManager.prototype.installContainer = installContainer; -NodeRedManager.prototype.backup = backup; NodeRedManager.prototype.checkForContainerUpdates = checkForContainerUpdates; NodeRedManager.prototype.disconnect = disconnect; NodeRedManager.prototype.isEnabled = isEnabled; NodeRedManager.prototype.status = status; - NodeRedManager.prototype.configureContainer = configureContainer; -NodeRedManager.prototype.saveZ2mBackup = saveZ2mBackup; -NodeRedManager.prototype.restoreZ2mBackup = restoreZ2mBackup; - module.exports = NodeRedManager; diff --git a/server/services/node-red/lib/init.js b/server/services/node-red/lib/init.js index 417145feb0..9828c6ef10 100644 --- a/server/services/node-red/lib/init.js +++ b/server/services/node-red/lib/init.js @@ -29,13 +29,16 @@ async function init() { await this.checkForContainerUpdates(configuration); await this.installContainer(configuration); - if (this.isEnabled()) { - // Schedule reccurent job if not already scheduled - if (!this.backupScheduledJob) { - this.backupScheduledJob = this.gladys.scheduler.scheduleJob('0 0 23 * * *', () => this.backup()); - } + if (!configuration.nodeRedPassword) { + configuration.nodeRedUsername = CONFIGURATION.NODE_RED_USERNAME_VALUE; + configuration.nodeRedPassword = generate(20, { + number: true, + lowercase: true, + uppercase: true, + }); } + await this.saveConfiguration(configuration); return null; } diff --git a/server/services/node-red/lib/installContainer.js b/server/services/node-red/lib/installContainer.js index fdfee77315..a1bf5a03e1 100644 --- a/server/services/node-red/lib/installContainer.js +++ b/server/services/node-red/lib/installContainer.js @@ -5,7 +5,7 @@ const logger = require('../../../utils/logger'); const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); const containerDescriptor = require('../docker/gladys-node-red-container.json'); -const {PlatformNotCompatible} = require('../../../utils/coreErrors'); +const { PlatformNotCompatible } = require('../../../utils/coreErrors'); const sleep = promisify(setTimeout); @@ -28,30 +28,24 @@ async function installContainer(config) { throw new PlatformNotCompatible('DOCKER_BAD_NETWORK'); } - let dockerContainers = await this.gladys.system.getContainers({ all: true, filters: { name: [containerDescriptor.name] }, }); let [container] = dockerContainers; - // const { basePathOnContainer, basePathOnHost } = await this.gladys.system.getGladysBasePath(); - // const containerPath = `${basePathOnHost}/node-red`; - const basePathOnContainer = ``;// TODO CHange if (dockerContainers.length === 0) { - // Restore backup only in case of new installation - // await this.restoreZ2mBackup(containerPath); - // TODO add restore Backup - try { logger.info('Nodered: is being installed as Docker container...'); logger.info(`Pulling ${containerDescriptor.Image} image...`); await this.gladys.system.pull(containerDescriptor.Image); - const containerDescriptorToMutate = cloneDeep(containerDescriptor); - console.log(containerDescriptorToMutate); + // Prepare broker env + logger.info(`Nodered: Preparing environment...`); + await this.configureContainer(config); logger.info(`Creation of container...`); + const containerDescriptorToMutate = cloneDeep(containerDescriptor); const containerLog = await this.gladys.system.createContainer(containerDescriptorToMutate); logger.trace(containerLog); logger.info('NodeRed: successfully installed and configured as Docker container'); @@ -66,7 +60,7 @@ async function installContainer(config) { } } - const configChanged = await this.configureContainer(basePathOnContainer, config); + const configChanged = await this.configureContainer(config); try { dockerContainers = await this.gladys.system.getContainers({ diff --git a/server/services/node-red/lib/restoreZ2mBackup.js b/server/services/node-red/lib/restoreZ2mBackup.js deleted file mode 100644 index a8c8de5417..0000000000 --- a/server/services/node-red/lib/restoreZ2mBackup.js +++ /dev/null @@ -1,48 +0,0 @@ -const fsPromises = require('fs/promises'); -const JSZip = require('jszip'); -const path = require('path'); - -const logger = require('../../../utils/logger'); -const { CONFIGURATION } = require('./constants'); - -/** - * @description Restore z2m backup from database. - * @param {string} containerPath - Zigbee2MQTT configuration directory. - * @returns {Promise} Empty promise. - * @example - * await z2m.restoreZ2mBackup(configPath); - */ -async function restoreZ2mBackup(containerPath) { - await fsPromises.mkdir(containerPath, { recursive: true }); - // Check if configuration is already available - const z2mFiles = await fsPromises.readdir(containerPath); - if (z2mFiles.includes('configuration.yaml')) { - // Configuration is present, do not restore backup - logger.debug('zigbee2mqtt configuration already here, skip restore backup'); - return; - } - - // Check if backup is stored - logger.info('Zigbee2mqtt: loading z2m backup...'); - const z2mBackup = await this.gladys.variable.getValue(CONFIGURATION.Z2M_BACKUP, this.serviceId); - if (z2mBackup) { - logger.info('Restoring zigbee2mqtt configuration...'); - // Stored z2m backup is a base64 zip file - // 1. Decoding base64 content - const zip = new JSZip(); - const { files } = await zip.loadAsync(z2mBackup, { base64: true }); - - await Promise.all( - Object.values(files).map(async (file) => { - const content = await file.async('arraybuffer'); - return fsPromises.writeFile(path.join(containerPath, file.name), Buffer.from(content)); - }), - ); - } else { - logger.info('No zigbee2mqtt backup avaiable'); - } -} - -module.exports = { - restoreZ2mBackup, -}; diff --git a/server/services/node-red/lib/saveConfiguration.js b/server/services/node-red/lib/saveConfiguration.js index 1b5ed8a952..fa1bea7f08 100644 --- a/server/services/node-red/lib/saveConfiguration.js +++ b/server/services/node-red/lib/saveConfiguration.js @@ -20,7 +20,10 @@ async function saveConfiguration(config) { logger.debug('NodeRed: storing configuration...'); const keyValueMap = { + [CONFIGURATION.NODE_RED_USERNAME]: config.nodeRedUsername, + [CONFIGURATION.NODE_RED_PASSWORD]: config.nodeRedPassword, [CONFIGURATION.DOCKER_NODE_RED_VERSION]: config.dockerNodeRedVersion, + [CONFIGURATION.NODE_RED_URL]: CONFIGURATION.NODE_RED_URL_VALUE, }; const variableKeys = Object.keys(keyValueMap); diff --git a/server/services/node-red/lib/saveZ2mBackup.js b/server/services/node-red/lib/saveZ2mBackup.js deleted file mode 100644 index 48d78db63c..0000000000 --- a/server/services/node-red/lib/saveZ2mBackup.js +++ /dev/null @@ -1,45 +0,0 @@ -const logger = require('../../../utils/logger'); -const { CONFIGURATION } = require('./constants'); - -/** - * @description Save Z2M backup. - * @param {object} payload - Z2M MQTT backup payload. - * @returns {Promise} The status of backup JOB, or null. - * @example - * await z2m.saveZ2mBackup({ status: 'ok', data: { zip: 'base64_backup' }}); - */ -async function saveZ2mBackup(payload) { - logger.info('Zigbee2mqtt: storing backup...'); - - const { jobId, resolve: jobResolver, reject: jobRejecter } = this.backupJob; - if (jobId) { - await this.gladys.job.updateProgress(jobId, 60); - } - - const { status, data } = payload; - const backupValid = status === 'ok'; - - if (backupValid) { - await this.gladys.variable.setValue(CONFIGURATION.Z2M_BACKUP, data.zip, this.serviceId); - logger.info('Zigbee2mqtt: backup stored'); - - if (jobId) { - await this.gladys.job.updateProgress(jobId, 100); - } - } else { - logger.error('zigbee2mqtt backup is not ok'); - } - - if (backupValid && jobResolver) { - return jobResolver(); - } - if (!backupValid && jobRejecter) { - return jobRejecter(); - } - - return null; -} - -module.exports = { - saveZ2mBackup, -}; diff --git a/server/services/node-red/lib/status.js b/server/services/node-red/lib/status.js index 633826e2b3..24280c2b4f 100644 --- a/server/services/node-red/lib/status.js +++ b/server/services/node-red/lib/status.js @@ -7,7 +7,6 @@ function status() { const nodeRedEnabled = this.isEnabled(); const zigbee2mqttStatus = { - nodeRedExist: this.nodeRedExist, nodeRedRunning: this.nodeRedRunning, nodeRedEnabled, diff --git a/server/services/node-red/package-lock.json b/server/services/node-red/package-lock.json index 12f0cd3421..4a39f9f238 100644 --- a/server/services/node-red/package-lock.json +++ b/server/services/node-red/package-lock.json @@ -1,11 +1,11 @@ { - "name": "gladys-zigbee2mqtt", + "name": "gladys-node-red", "version": "1.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "gladys-zigbee2mqtt", + "name": "gladys-node-red", "version": "1.0.0", "cpu": [ "x64", @@ -18,11 +18,11 @@ "win32" ], "dependencies": { + "bcryptjs": "^2.4.3", "fs-extra": "^11.1.1", "jszip": "^3.10.1", "lodash.clonedeep": "^4.5.0", - "mqtt": "^4.2.6", - "yaml": "^2.2.2" + "mqtt": "^4.2.6" } }, "node_modules/balanced-match": { @@ -49,6 +49,11 @@ } ] }, + "node_modules/bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -873,14 +878,6 @@ "engines": { "node": ">=0.4" } - }, - "node_modules/yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", - "engines": { - "node": ">= 14" - } } }, "dependencies": { @@ -894,6 +891,11 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, + "bcryptjs": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", + "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -1598,11 +1600,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - }, - "yaml": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", - "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==" } } } diff --git a/server/services/node-red/package.json b/server/services/node-red/package.json index da85deed6a..6a54e05cab 100644 --- a/server/services/node-red/package.json +++ b/server/services/node-red/package.json @@ -13,10 +13,10 @@ "arm64" ], "dependencies": { + "bcryptjs": "^2.4.3", "fs-extra": "^11.1.1", "jszip": "^3.10.1", "lodash.clonedeep": "^4.5.0", - "mqtt": "^4.2.6", - "yaml": "^2.2.2" + "mqtt": "^4.2.6" } } From 12b334b6807adcbe1bfdee4bc0cfc2e6af21ad6d Mon Sep 17 00:00:00 2001 From: callemand Date: Thu, 8 Jun 2023 15:39:02 +0200 Subject: [PATCH 04/32] Fix dependencies --- server/services/node-red/package-lock.json | 746 ++++++++++++++++++++- server/services/node-red/package.json | 2 +- 2 files changed, 738 insertions(+), 10 deletions(-) diff --git a/server/services/node-red/package-lock.json b/server/services/node-red/package-lock.json index 4a39f9f238..8d76fcac7f 100644 --- a/server/services/node-red/package-lock.json +++ b/server/services/node-red/package-lock.json @@ -18,13 +18,73 @@ "win32" ], "dependencies": { - "bcryptjs": "^2.4.3", + "bcrypt": "^5.1.0", "fs-extra": "^11.1.1", "jszip": "^3.10.1", "lodash.clonedeep": "^4.5.0", "mqtt": "^4.2.6" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -49,10 +109,18 @@ } ] }, - "node_modules/bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + "node_modules/bcrypt": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "hasInstallScript": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" + }, + "engines": { + "node": ">= 10.0.0" + } }, "node_modules/bl": { "version": "4.1.0", @@ -137,6 +205,22 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "bin": { + "color-support": "bin.js" + } + }, "node_modules/commist": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", @@ -165,6 +249,11 @@ "typedarray": "^0.0.6" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -186,6 +275,19 @@ } } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -224,6 +326,11 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/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==" + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -250,11 +357,52 @@ "node": ">=14.14" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -335,6 +483,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "node_modules/help-me": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz", @@ -346,6 +499,18 @@ "xtend": "^4.0.0" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -404,6 +569,14 @@ "node": ">=0.10.0" } }, + "node_modules/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==", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", @@ -533,6 +706,39 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/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==", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -549,6 +755,48 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/mqtt": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.2.6.tgz", @@ -592,6 +840,63 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node_modules/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -709,6 +1014,20 @@ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -728,11 +1047,35 @@ } ] }, + "node_modules/semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "node_modules/split2": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", @@ -754,6 +1097,46 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -811,6 +1194,11 @@ "node": ">=0.10.0" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -846,6 +1234,28 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -878,9 +1288,62 @@ "engines": { "node": ">=0.4" } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } }, "dependencies": { + "@mapbox/node-pre-gyp": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz", + "integrity": "sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==", + "requires": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -891,10 +1354,14 @@ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" }, - "bcryptjs": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", - "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" + "bcrypt": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-5.1.0.tgz", + "integrity": "sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==", + "requires": { + "@mapbox/node-pre-gyp": "^1.0.10", + "node-addon-api": "^5.0.0" + } }, "bl": { "version": "4.1.0", @@ -967,6 +1434,16 @@ } } }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, "commist": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", @@ -992,6 +1469,11 @@ "typedarray": "^0.0.6" } }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -1005,6 +1487,16 @@ "ms": "2.1.2" } }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + }, "duplexify": { "version": "3.7.1", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", @@ -1045,6 +1537,11 @@ } } }, + "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==" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -1068,11 +1565,45 @@ "universalify": "^2.0.0" } }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + } + }, "glob": { "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", @@ -1146,6 +1677,11 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, "help-me": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/help-me/-/help-me-1.1.0.tgz", @@ -1157,6 +1693,15 @@ "xtend": "^4.0.0" } }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -1195,6 +1740,11 @@ "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", @@ -1306,6 +1856,29 @@ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=" }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "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" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } + } + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -1319,6 +1892,35 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==" + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + }, "mqtt": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.2.6.tgz", @@ -1354,6 +1956,43 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node-addon-api": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-5.1.0.tgz", + "integrity": "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==" + }, + "node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-4I6pdBY1EthSqDmJkiNk3JIT8cswwR9nfeW/cPdUagJYEQG7R95WRH74wpz7ma8Gh/9dI9FP+OU+0E4FvtA55w==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "requires": { + "abbrev": "1" + } + }, + "npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "requires": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1469,16 +2108,42 @@ "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, + "semver": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", + "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "requires": { + "lru-cache": "^6.0.0" + } + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, "setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, "split2": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", @@ -1500,6 +2165,37 @@ "safe-buffer": "~5.2.0" } }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "tar": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + } + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -1556,6 +2252,11 @@ "is-negated-glob": "^1.0.0" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -1585,6 +2286,28 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1600,6 +2323,11 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" } } } diff --git a/server/services/node-red/package.json b/server/services/node-red/package.json index 6a54e05cab..354508e693 100644 --- a/server/services/node-red/package.json +++ b/server/services/node-red/package.json @@ -13,7 +13,7 @@ "arm64" ], "dependencies": { - "bcryptjs": "^2.4.3", + "bcrypt": "^5.1.0", "fs-extra": "^11.1.1", "jszip": "^3.10.1", "lodash.clonedeep": "^4.5.0", From 638fd5fbf906515d1e5cc72f52cd31d5656c45eb Mon Sep 17 00:00:00 2001 From: callemand Date: Fri, 9 Jun 2023 15:20:21 +0200 Subject: [PATCH 05/32] Add TU --- server/services/node-red/index.js | 13 - .../node-red/lib/configureContainer.js | 26 +- server/services/node-red/lib/constants.js | 1 + server/services/node-red/lib/status.js | 4 +- .../node-red/api/node-red.controller.test.js | 71 +++ .../node-red/expectedDefaultContent.txt | 597 ++++++++++++++++++ .../node-red/expectedNodeRedContent.txt | 597 ++++++++++++++++++ .../node-red/expectedOtherNodeRedContent.txt | 597 ++++++++++++++++++ server/test/services/node-red/index.test.js | 48 ++ .../lib/checkForContainerUpdates.test.js | 93 +++ .../node-red/lib/configureContainer.test.js | 115 ++++ .../services/node-red/lib/disconnect.test.js | 56 ++ .../node-red/lib/getConfiguration.test.js | 44 ++ .../test/services/node-red/lib/init.test.js | 120 ++++ .../node-red/lib/installContainer.test.js | 115 ++++ .../services/node-red/lib/isEnabled.test.js | 27 + .../node-red/lib/saveConfiguration.test.js | 58 ++ .../test/services/node-red/lib/status.test.js | 33 + server/test/services/node-red/mockPassword.js | 11 + 19 files changed, 2600 insertions(+), 26 deletions(-) create mode 100644 server/test/services/node-red/api/node-red.controller.test.js create mode 100644 server/test/services/node-red/expectedDefaultContent.txt create mode 100644 server/test/services/node-red/expectedNodeRedContent.txt create mode 100644 server/test/services/node-red/expectedOtherNodeRedContent.txt create mode 100644 server/test/services/node-red/index.test.js create mode 100644 server/test/services/node-red/lib/checkForContainerUpdates.test.js create mode 100644 server/test/services/node-red/lib/configureContainer.test.js create mode 100644 server/test/services/node-red/lib/disconnect.test.js create mode 100644 server/test/services/node-red/lib/getConfiguration.test.js create mode 100644 server/test/services/node-red/lib/init.test.js create mode 100644 server/test/services/node-red/lib/installContainer.test.js create mode 100644 server/test/services/node-red/lib/isEnabled.test.js create mode 100644 server/test/services/node-red/lib/saveConfiguration.test.js create mode 100644 server/test/services/node-red/lib/status.test.js create mode 100644 server/test/services/node-red/mockPassword.js diff --git a/server/services/node-red/index.js b/server/services/node-red/index.js index c7affab067..bc6027bfc5 100644 --- a/server/services/node-red/index.js +++ b/server/services/node-red/index.js @@ -27,22 +27,9 @@ module.exports = function NodeRedService(gladys, serviceId) { nodeRedManager.disconnect(); } - /** - * @public - * @description Test if Node-red is running. - * @returns {Promise} Returns true if node-red is used. - * @example - * const used = await gladys.services['node-red'].isUsed(); - */ - async function isUsed() { - return nodeRedManager.gladysConnected && nodeRedManager.zigbee2mqttConnected; - // TODO Check if needed - } - return Object.freeze({ start, stop, - isUsed, device: nodeRedManager, controllers: NodeRedController(gladys, nodeRedManager), }); diff --git a/server/services/node-red/lib/configureContainer.js b/server/services/node-red/lib/configureContainer.js index f9fff18a57..64d8d4c7e5 100644 --- a/server/services/node-red/lib/configureContainer.js +++ b/server/services/node-red/lib/configureContainer.js @@ -1,8 +1,9 @@ const fs = require('fs/promises'); const { constants } = require('fs'); const path = require('path'); -const bcrypt = require('bcrypt'); const logger = require('../../../utils/logger'); +const passwordUtils = require('../../../utils/password'); +const { DEFAULT } = require('./constants'); /** * @description Configure Node-red container. @@ -17,7 +18,7 @@ async function configureContainer(config) { const { basePathOnHost } = await this.gladys.system.getGladysBasePath(); // Create configuration path (if not exists) - const configFilepath = path.join(basePathOnHost, 'node-red', 'settings.js'); + const configFilepath = path.join(basePathOnHost, DEFAULT.CONFIGURATION_PATH); await fs.mkdir(path.dirname(configFilepath), { recursive: true }); // Check if config file not already exists let configCreated = false; @@ -31,19 +32,22 @@ async function configureContainer(config) { configCreated = true; } - // Check for changes const fileContent = await fs.readFile(configFilepath); let fileContentString = fileContent.toString(); - const encodedPassword = bcrypt.hashSync(config.nodeRedPassword, 8); - const [, username] = fileContentString.match(/username: '(.+)'/); - const [, password] = fileContentString.match(/password: '(.+)'/); - let configChanged = false; - if (username !== config.nodeRedUsername || password !== encodedPassword) { - fileContentString = fileContentString.replace(/username: '(.+)'/, `username: '${config.nodeRedUsername}'`); - fileContentString = fileContentString.replace(/password: '(.+)'/, `password: '${encodedPassword}'`); - configChanged = true; + if (config.nodeRedPassword && config.nodeRedUsername) { + // Check for changes + + const encodedPassword = await passwordUtils.hash(config.nodeRedPassword); + const [, username] = fileContentString.match(/username: '(.+)'/); + const [, password] = fileContentString.match(/password: '(.+)'/); + + if (username !== config.nodeRedUsername || (await passwordUtils.compare(password, encodedPassword)) === false) { + fileContentString = fileContentString.replace(/username: '(.+)'/, `username: '${config.nodeRedUsername}'`); + fileContentString = fileContentString.replace(/password: '(.+)'/, `password: '${encodedPassword}'`); + configChanged = true; + } } if (configChanged) { diff --git a/server/services/node-red/lib/constants.js b/server/services/node-red/lib/constants.js index 7cdbd08b7d..41c8827056 100644 --- a/server/services/node-red/lib/constants.js +++ b/server/services/node-red/lib/constants.js @@ -11,6 +11,7 @@ const CONFIGURATION = { const DEFAULT = { DOCKER_NODE_RED_VERSION: '2', // Last version of NodeRed docker file, + CONFIGURATION_PATH: 'node-red/settings.js', }; module.exports = { diff --git a/server/services/node-red/lib/status.js b/server/services/node-red/lib/status.js index 24280c2b4f..d44e912d8e 100644 --- a/server/services/node-red/lib/status.js +++ b/server/services/node-red/lib/status.js @@ -6,7 +6,7 @@ */ function status() { const nodeRedEnabled = this.isEnabled(); - const zigbee2mqttStatus = { + const nodeRedStatus = { nodeRedExist: this.nodeRedExist, nodeRedRunning: this.nodeRedRunning, nodeRedEnabled, @@ -14,7 +14,7 @@ function status() { dockerBased: this.dockerBased, networkModeValid: this.networkModeValid, }; - return zigbee2mqttStatus; + return nodeRedStatus; } module.exports = { diff --git a/server/test/services/node-red/api/node-red.controller.test.js b/server/test/services/node-red/api/node-red.controller.test.js new file mode 100644 index 0000000000..3bbf0a655a --- /dev/null +++ b/server/test/services/node-red/api/node-red.controller.test.js @@ -0,0 +1,71 @@ +const sinon = require('sinon'); + +const { assert, fake } = sinon; +const NodeRedController = require('../../../../services/node-red/api/node-red.controller'); + +const event = { + emit: fake.resolves(null), +}; + +const gladys = { + event, +}; +const NodeRedManager = { + status: fake.returns(true), + init: fake.returns(true), + installContainer: fake.returns(true), + disconnect: fake.returns(true), +}; + +describe('NodeRed API', () => { + let controller; + + beforeEach(() => { + controller = NodeRedController(gladys, NodeRedManager, 'de1dd005-092d-456d-93d1-817c9ace4c67'); + sinon.reset(); + }); + + it('get /api/v1/service/node-red/status', async () => { + const req = {}; + const res = { + json: fake.returns(null), + }; + + await controller['get /api/v1/service/node-red/status'].controller(req, res); + assert.calledOnce(NodeRedManager.status); + assert.calledWith(res.json, true); + }); + + it('post /api/v1/service/node-red/connect', async () => { + const req = {}; + const res = { + json: fake.returns(null), + }; + + await controller['post /api/v1/service/node-red/connect'].controller(req, res); + assert.calledOnce(NodeRedManager.init); + assert.calledWith(res.json, { success: true }); + }); + + it('post /api/v1/service/node-red/start', async () => { + const req = {}; + const res = { + json: fake.returns(null), + }; + + await controller['post /api/v1/service/node-red/start'].controller(req, res); + assert.calledOnce(NodeRedManager.installContainer); + assert.calledWith(res.json, { success: true }); + }); + + it('post /api/v1/service/node-red/disconnect', async () => { + const req = {}; + const res = { + json: fake.returns(null), + }; + + await controller['post /api/v1/service/node-red/disconnect'].controller(req, res); + assert.calledOnce(NodeRedManager.disconnect); + assert.calledWith(res.json, { success: true }); + }); +}); diff --git a/server/test/services/node-red/expectedDefaultContent.txt b/server/test/services/node-red/expectedDefaultContent.txt new file mode 100644 index 0000000000..a62dd14475 --- /dev/null +++ b/server/test/services/node-red/expectedDefaultContent.txt @@ -0,0 +1,597 @@ +/** + * This is the default settings file provided by Node-RED. + * + * It can contain any valid JavaScript code that will get run when Node-RED + * is started. + * + * Lines that start with // are commented out. + * Each entry should be separated from the entries above and below by a comma ','. + * + * For more information about individual settings, refer to the documentation: + * https://nodered.org/docs/user-guide/runtime/configuration. + * + * The settings are split into the following sections: + * - Flow File and User Directory Settings + * - Security + * - Server Settings + * - Runtime Settings + * - Editor Settings + * - Node Settings. + * + */ + +module.exports = { + +/** + * *****************************************************************************. + * Flow File and User Directory Settings + * - flowFile + * - credentialSecret + * - flowFilePretty + * - userDir + * - nodesDir + *****************************************************************************. + */ + + /** The file containing the flows. If not set, defaults to flows_.json */ + flowFile: 'flows.json', + + /** + * By default, credentials are encrypted in storage using a generated key. To + * specify your own secret, set the following property. + * If you want to disable encryption of credentials, set this property to false. + * Note: once you set this property, do not change it - doing so will prevent + * node-red from being able to decrypt your existing credentials and they will be + * lost. + */ + // credentialSecret: "a-secret-key", + + /** + * By default, the flow JSON will be formatted over multiple lines making + * it easier to compare changes when using version control. + * To disable pretty-printing of the JSON set the following property to false. + */ + flowFilePretty: true, + + /** + * By default, all user data is stored in a directory called `.node-red` under + * the user's home directory. To use a different location, the following + * property can be used. + */ + // userDir: '/home/nol/.node-red/', + + /** + * Node-RED scans the `nodes` directory in the userDir to find local node files. + * The following property can be used to specify an additional directory to scan. + */ + // nodesDir: '/home/nol/.node-red/nodes', + +/** + * *****************************************************************************. + * Security + * - adminAuth + * - https + * - httpsRefreshInterval + * - requireHttps + * - httpNodeAuth + * - httpStaticAuth + *****************************************************************************. + */ + + /** + * To password protect the Node-RED editor and admin API, the following + * property can be used. See http://nodered.org/docs/security.html for details. + */ + adminAuth: { + type: 'credentials', + users: [{ + username: 'admin', + password: '$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN.', + permissions: '*' + }] + }, + + /** + * The following property can be used to enable HTTPS + * This property can be either an object, containing both a (private) key + * and a (public) certificate, or a function that returns such an object. + * See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener + * for details of its contents. + */ + + /** Option 1: static object */ + // https: { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + // }, + + /** Option 2: function that returns the HTTP configuration object */ + // https: function() { + // // This function should return the options object, or a Promise + // // that resolves to the options object + // return { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + // } + // }, + + /** + * If the `https` setting is a function, the following setting can be used + * to set how often, in hours, the function will be called. That can be used + * to refresh any certificates. + */ + // httpsRefreshInterval : 12, + + /** + * The following property can be used to cause insecure HTTP connections to + * be redirected to HTTPS. + */ + // requireHttps: true, + + /** + * To password protect the node-defined HTTP endpoints (httpNodeRoot), + * including node-red-dashboard, or the static content (httpStatic), the + * following properties can be used. + * The `pass` field is a bcrypt hash of the password. + * See http://nodered.org/docs/security.html#generating-the-password-hash. + */ + // httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + // httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + +/** + * *****************************************************************************. + * Server Settings + * - uiPort + * - uiHost + * - apiMaxLength + * - httpServerOptions + * - httpAdminRoot + * - httpAdminMiddleware + * - httpNodeRoot + * - httpNodeCors + * - httpNodeMiddleware + * - httpStatic + * - httpStaticRoot + *****************************************************************************. + */ + + /** The tcp port that the Node-RED web server is listening on */ + uiPort: process.env.PORT || 1880, + + /** + * By default, the Node-RED UI accepts connections on all IPv4 interfaces. + * To listen on all IPv6 addresses, set uiHost to "::", + * The following property can be used to listen on a specific interface. For + * example, the following would only allow connections from the local machine. + */ + uiHost: "0.0.0.0", + + /** + * The maximum size of HTTP request that will be accepted by the runtime api. + * Default: 5mb. + */ + // apiMaxLength: '5mb', + + /** + * The following property can be used to pass custom options to the Express.js + * server used by Node-RED. For a full list of available options, refer + * to http://expressjs.com/en/api.html#app.settings.table. + */ + // httpServerOptions: { }, + + /** + * By default, the Node-RED UI is available at http://localhost:1880/ + * The following property can be used to specify a different root path. + * If set to false, this is disabled. + */ + // httpAdminRoot: '/admin', + + /** + * The following property can be used to add a custom middleware function + * in front of all admin http routes. For example, to set custom http + * headers. It can be a single function or an array of middleware functions. + */ + // httpAdminMiddleware: function(req,res,next) { + // // Set the X-Frame-Options header to limit where the editor + // // can be embedded + // //res.set('X-Frame-Options', 'sameorigin'); + // next(); + // }, + + + /** + * Some nodes, such as HTTP In, can be used to listen for incoming http requests. + * By default, these are served relative to '/'. The following property + * can be used to specifiy a different root path. If set to false, this is + * disabled. + */ + // httpNodeRoot: '/red-nodes', + + /** + * The following property can be used to configure cross-origin resource sharing + * in the HTTP nodes. + * See https://github.com/troygoode/node-cors#configuration-options for + * details on its contents. The following is a basic permissive set of options: + */ + // httpNodeCors: { + // origin: "*", + // methods: "GET,PUT,POST,DELETE" + // }, + + /** + * If you need to set an http proxy please set an environment variable + * called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system. + * For example - http_proxy=http://myproxy.com:8080 + * (Setting it here will have no effect) + * You may also specify no_proxy (or NO_PROXY) to supply a comma separated + * list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk. + */ + + /** + * The following property can be used to add a custom middleware function + * in front of all http in nodes. This allows custom authentication to be + * applied to all http in nodes, or any other sort of common request processing. + * It can be a single function or an array of middleware functions. + */ + // httpNodeMiddleware: function(req,res,next) { + // // Handle/reject the request, or pass it on to the http in node by calling next(); + // // Optionally skip our rawBodyParser by setting this to true; + // //req.skipRawBodyParser = true; + // next(); + // }, + + /** + * When httpAdminRoot is used to move the UI to a different root path, the + * following property can be used to identify a directory of static content + * that should be served at http://localhost:1880/. + * When httpStaticRoot is set differently to httpAdminRoot, there is no need + * to move httpAdminRoot. + */ + // httpStatic: '/home/nol/node-red-static/', //single static source + /* OR multiple static sources can be created using an array of objects... */ + // httpStatic: [ + // {path: '/home/nol/pics/', root: "/img/"}, + // {path: '/home/nol/reports/', root: "/doc/"}, + // ], + + /** + * All static routes will be appended to httpStaticRoot + * e.g. If httpStatic = "/home/nol/docs" and httpStaticRoot = "/static/" + * then "/home/nol/docs" will be served at "/static/" + * e.g. if httpStatic = [{path: '/home/nol/pics/', root: "/img/"}] + * and httpStaticRoot = "/static/" + * then "/home/nol/pics/" will be served at "/static/img/". + */ + // httpStaticRoot: '/static/', + +/** + * *****************************************************************************. + * Runtime Settings + * - lang + * - runtimeState + * - diagnostics + * - logging + * - contextStorage + * - exportGlobalContextKeys + * - externalModules + *****************************************************************************. + */ + + /** + * Uncomment the following to run node-red in your preferred language. + * Available languages include: en-US (default), ja, de, zh-CN, zh-TW, ru, ko + * Some languages are more complete than others. + */ + // lang: "de", + + /** + * Configure diagnostics options + * - enabled: When `enabled` is `true` (or unset), diagnostics data will + * be available at http://localhost:1880/diagnostics + * - ui: When `ui` is `true` (or unset), the action `show-system-info` will + * be available to logged in users of node-red editor. + */ + diagnostics: { + /** Enable or disable diagnostics endpoint. Must be set to `false` to disable */ + enabled: true, + /** Enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ + ui: true, + }, + /** + * Configure runtimeState options + * - enabled: When `enabled` is `true` flows runtime can be Started/Stoped + * by POSTing to available at http://localhost:1880/flows/state + * - ui: When `ui` is `true`, the action `core:start-flows` and + * `core:stop-flows` will be available to logged in users of node-red editor + * Also, the deploy menu (when set to default) will show a stop or start button. + */ + runtimeState: { + /** Enable or disable flows/state endpoint. Must be set to `false` to disable */ + enabled: false, + /** Show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */ + ui: false, + }, + /** Configure the logging output */ + logging: { + /** Only console logging is currently supported */ + console: { + /** + * Level of logging to be recorded. Options are: + * fatal - only those errors which make the application unusable should be recorded + * error - record errors which are deemed fatal for a particular request + fatal errors + * warn - record problems which are non fatal + errors + fatal errors + * info - record information about the general running of the application + warn + error + fatal errors + * debug - record information which is more verbose than info + info + warn + error + fatal errors + * trace - record very detailed logging + debug + info + warn + error + fatal errors + * off - turn off all logging (doesn't affect metrics or audit). + */ + level: 'info', + /** Whether or not to include metric events in the log output */ + metrics: false, + /** Whether or not to include audit events in the log output */ + audit: false + } + }, + + /** + * Context Storage + * The following property can be used to enable context storage. The configuration + * provided here will enable file-based context that flushes to disk every 30 seconds. + * Refer to the documentation for further options: https://nodered.org/docs/api/context/. + */ + // contextStorage: { + // default: { + // module:"localfilesystem" + // }, + // }, + + /** + * `global.keys()` returns a list of all properties set in global context. + * This allows them to be displayed in the Context Sidebar within the editor. + * In some circumstances it is not desirable to expose them to the editor. The + * following property can be used to hide any property set in `functionGlobalContext` + * from being list by `global.keys()`. + * By default, the property is set to false to avoid accidental exposure of + * their values. Setting this to true will cause the keys to be listed. + */ + exportGlobalContextKeys: false, + + /** + * Configure how the runtime will handle external npm modules. + * This covers: + * - whether the editor will allow new node modules to be installed + * - whether nodes, such as the Function node are allowed to have their + * own dynamically configured dependencies. + * The allow/denyList options can be used to limit what modules the runtime + * will install/load. It can use '*' as a wildcard that matches anything. + */ + externalModules: { + // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ + // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ + // palette: { /** Configuration for the Palette Manager */ + // allowInstall: true, /** Enable the Palette Manager in the editor */ + // allowUpdate: true, /** Allow modules to be updated in the Palette Manager */ + // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ + // allowList: ['*'], + // denyList: [], + // allowUpdateList: ['*'], + // denyUpdateList: [] + // }, + // modules: { /** Configuration for node-specified modules */ + // allowInstall: true, + // allowList: [], + // denyList: [] + // } + }, + + +/** + * *****************************************************************************. + * Editor Settings + * - disableEditor + * - editorTheme + *****************************************************************************. + */ + + /** + * The following property can be used to disable the editor. The admin API + * is not affected by this option. To disable both the editor and the admin + * API, use either the httpRoot or httpAdminRoot properties. + */ + // disableEditor: false, + + /** + * Customising the editor + * See https://nodered.org/docs/user-guide/runtime/configuration#editor-themes + * for all available options. + */ + editorTheme: { + /** + * The following property can be used to set a custom theme for the editor. + * See https://github.com/node-red-contrib-themes/theme-collection for + * a collection of themes to chose from. + */ + // theme: "", + + /** + * To disable the 'Welcome to Node-RED' tour that is displayed the first + * time you access the editor for each release of Node-RED, set this to false. + */ + // tours: false, + + palette: { + /** + * The following property can be used to order the categories in the editor + * palette. If a node's category is not in the list, the category will get + * added to the end of the palette. + * If not set, the following default order is used: + */ + // categories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'], + }, + + projects: { + /** To enable the Projects feature, set this value to true */ + enabled: false, + workflow: { + /** + * Set the default projects workflow mode. + * - manual - you must manually commit changes + * - auto - changes are automatically committed + * This can be overridden per-user from the 'Git config' + * section of 'User Settings' within the editor. + */ + mode: 'manual' + } + }, + + codeEditor: { + /** + * Select the text editor component used by the editor. + * As of Node-RED V3, this defaults to "monaco", but can be set to "ace" if desired. + */ + lib: 'monaco', + options: { + /** + * The follow options only apply if the editor is set to "monaco". + * + * Theme - must match the file name of a theme in + * packages/node_modules/@node-red/editor-client/src/vendor/monaco/dist/theme + * e.g. "tomorrow-night", "upstream-sunburst", "github", "my-theme". + */ + // theme: "vs", + /** + * Other overrides can be set e.g. FontSize, fontFamily, fontLigatures etc. + * For the full list, see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html. + */ + // fontSize: 14, + // fontFamily: "Cascadia Code, Fira Code, Consolas, 'Courier New', monospace", + // fontLigatures: true, + } + } + }, + +/** + * *****************************************************************************. + * Node Settings + * - fileWorkingDirectory + * - functionGlobalContext + * - functionExternalModules + * - nodeMessageBufferMaxLength + * - ui (for use with Node-RED Dashboard) + * - debugUseColors + * - debugMaxLength + * - execMaxBufferSize + * - httpRequestTimeout + * - mqttReconnectTime + * - serialReconnectTime + * - socketReconnectTime + * - socketTimeout + * - tcpMsgQueueSize + * - inboundWebSocketTimeout + * - tlsConfigDisableLocalFiles + * - webSocketNodeVerifyClient + *****************************************************************************. + */ + + /** + * The working directory to handle relative file paths from within the File nodes + * defaults to the working directory of the Node-RED process. + */ + // fileWorkingDirectory: "", + + /** Allow the Function node to load additional npm modules directly */ + functionExternalModules: true, + + /** + * The following property can be used to set predefined values in Global Context. + * This allows extra node modules to be made available with in Function node. + * For example, the following: + * functionGlobalContext: { os:require('os') } + * will allow the `os` module to be accessed in a Function node using: + * global.get("os"). + */ + functionGlobalContext: { + // os:require('os'), + }, + + /** + * The maximum number of messages nodes will buffer internally as part of their + * operation. This applies across a range of nodes that operate on message sequences. + * Defaults to no limit. A value of 0 also means no limit is applied. + */ + // nodeMessageBufferMaxLength: 0, + + /** + * If you installed the optional node-red-dashboard you can set it's path + * relative to httpNodeRoot + * Other optional properties include + * readOnly:{boolean}, + * middleware:{function or array}, (req,res,next) - http middleware + * ioMiddleware:{function or array}, (socket,next) - socket.io middleware. + */ + // ui: { path: "ui" }, + + /** Colourise the console output of the debug node */ + // debugUseColors: true, + + /** The maximum length, in characters, of any message sent to the debug sidebar tab */ + debugMaxLength: 1000, + + /** Maximum buffer size for the exec node. Defaults to 10Mb */ + // execMaxBufferSize: 10000000, + + /** Timeout in milliseconds for HTTP request connections. Defaults to 120s */ + // httpRequestTimeout: 120000, + + /** Retry time in milliseconds for MQTT connections */ + mqttReconnectTime: 15000, + + /** Retry time in milliseconds for Serial port connections */ + serialReconnectTime: 15000, + + /** Retry time in milliseconds for TCP socket connections */ + // socketReconnectTime: 10000, + + /** Timeout in milliseconds for TCP server socket connections. Defaults to no timeout */ + // socketTimeout: 120000, + + /** + * Maximum number of messages to wait in queue while attempting to connect to TCP socket + * defaults to 1000. + */ + // tcpMsgQueueSize: 2000, + + /** + * Timeout in milliseconds for inbound WebSocket connections that do not + * match any configured node. Defaults to 5000. + */ + // inboundWebSocketTimeout: 5000, + + /** + * To disable the option for using local files for storing keys and + * certificates in the TLS configuration node, set this to true. + */ + // tlsConfigDisableLocalFiles: true, + + /** + * The following property can be used to verify websocket connection attempts. + * This allows, for example, the HTTP request headers to be checked to ensure + * they include valid authentication information. + */ + // webSocketNodeVerifyClient: function(info) { + // /** 'info' has three properties: + // * - origin : the value in the Origin header + // * - req : the HTTP request + // * - secure : true if req.connection.authorized or req.connection.encrypted is set + // * + // * The function should return true if the connection should be accepted, false otherwise. + // * + // * Alternatively, if this function is defined to accept a second argument, callback, + // * it can be used to verify the client asynchronously. + // * The callback takes three arguments: + // * - result : boolean, whether to accept the connection or not + // * - code : if result is false, the HTTP error status to return + // * - reason: if result is false, the HTTP reason string to return + // */ + // }, +}; diff --git a/server/test/services/node-red/expectedNodeRedContent.txt b/server/test/services/node-red/expectedNodeRedContent.txt new file mode 100644 index 0000000000..d32fb7fa8c --- /dev/null +++ b/server/test/services/node-red/expectedNodeRedContent.txt @@ -0,0 +1,597 @@ +/** + * This is the default settings file provided by Node-RED. + * + * It can contain any valid JavaScript code that will get run when Node-RED + * is started. + * + * Lines that start with // are commented out. + * Each entry should be separated from the entries above and below by a comma ','. + * + * For more information about individual settings, refer to the documentation: + * https://nodered.org/docs/user-guide/runtime/configuration. + * + * The settings are split into the following sections: + * - Flow File and User Directory Settings + * - Security + * - Server Settings + * - Runtime Settings + * - Editor Settings + * - Node Settings. + * + */ + +module.exports = { + +/** + * *****************************************************************************. + * Flow File and User Directory Settings + * - flowFile + * - credentialSecret + * - flowFilePretty + * - userDir + * - nodesDir + *****************************************************************************. + */ + + /** The file containing the flows. If not set, defaults to flows_.json */ + flowFile: 'flows.json', + + /** + * By default, credentials are encrypted in storage using a generated key. To + * specify your own secret, set the following property. + * If you want to disable encryption of credentials, set this property to false. + * Note: once you set this property, do not change it - doing so will prevent + * node-red from being able to decrypt your existing credentials and they will be + * lost. + */ + // credentialSecret: "a-secret-key", + + /** + * By default, the flow JSON will be formatted over multiple lines making + * it easier to compare changes when using version control. + * To disable pretty-printing of the JSON set the following property to false. + */ + flowFilePretty: true, + + /** + * By default, all user data is stored in a directory called `.node-red` under + * the user's home directory. To use a different location, the following + * property can be used. + */ + // userDir: '/home/nol/.node-red/', + + /** + * Node-RED scans the `nodes` directory in the userDir to find local node files. + * The following property can be used to specify an additional directory to scan. + */ + // nodesDir: '/home/nol/.node-red/nodes', + +/** + * *****************************************************************************. + * Security + * - adminAuth + * - https + * - httpsRefreshInterval + * - requireHttps + * - httpNodeAuth + * - httpStaticAuth + *****************************************************************************. + */ + + /** + * To password protect the Node-RED editor and admin API, the following + * property can be used. See http://nodered.org/docs/security.html for details. + */ + adminAuth: { + type: 'credentials', + users: [{ + username: 'username', + password: 'password', + permissions: '*' + }] + }, + + /** + * The following property can be used to enable HTTPS + * This property can be either an object, containing both a (private) key + * and a (public) certificate, or a function that returns such an object. + * See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener + * for details of its contents. + */ + + /** Option 1: static object */ + // https: { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + // }, + + /** Option 2: function that returns the HTTP configuration object */ + // https: function() { + // // This function should return the options object, or a Promise + // // that resolves to the options object + // return { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + // } + // }, + + /** + * If the `https` setting is a function, the following setting can be used + * to set how often, in hours, the function will be called. That can be used + * to refresh any certificates. + */ + // httpsRefreshInterval : 12, + + /** + * The following property can be used to cause insecure HTTP connections to + * be redirected to HTTPS. + */ + // requireHttps: true, + + /** + * To password protect the node-defined HTTP endpoints (httpNodeRoot), + * including node-red-dashboard, or the static content (httpStatic), the + * following properties can be used. + * The `pass` field is a bcrypt hash of the password. + * See http://nodered.org/docs/security.html#generating-the-password-hash. + */ + // httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + // httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + +/** + * *****************************************************************************. + * Server Settings + * - uiPort + * - uiHost + * - apiMaxLength + * - httpServerOptions + * - httpAdminRoot + * - httpAdminMiddleware + * - httpNodeRoot + * - httpNodeCors + * - httpNodeMiddleware + * - httpStatic + * - httpStaticRoot + *****************************************************************************. + */ + + /** The tcp port that the Node-RED web server is listening on */ + uiPort: process.env.PORT || 1880, + + /** + * By default, the Node-RED UI accepts connections on all IPv4 interfaces. + * To listen on all IPv6 addresses, set uiHost to "::", + * The following property can be used to listen on a specific interface. For + * example, the following would only allow connections from the local machine. + */ + uiHost: "0.0.0.0", + + /** + * The maximum size of HTTP request that will be accepted by the runtime api. + * Default: 5mb. + */ + // apiMaxLength: '5mb', + + /** + * The following property can be used to pass custom options to the Express.js + * server used by Node-RED. For a full list of available options, refer + * to http://expressjs.com/en/api.html#app.settings.table. + */ + // httpServerOptions: { }, + + /** + * By default, the Node-RED UI is available at http://localhost:1880/ + * The following property can be used to specify a different root path. + * If set to false, this is disabled. + */ + // httpAdminRoot: '/admin', + + /** + * The following property can be used to add a custom middleware function + * in front of all admin http routes. For example, to set custom http + * headers. It can be a single function or an array of middleware functions. + */ + // httpAdminMiddleware: function(req,res,next) { + // // Set the X-Frame-Options header to limit where the editor + // // can be embedded + // //res.set('X-Frame-Options', 'sameorigin'); + // next(); + // }, + + + /** + * Some nodes, such as HTTP In, can be used to listen for incoming http requests. + * By default, these are served relative to '/'. The following property + * can be used to specifiy a different root path. If set to false, this is + * disabled. + */ + // httpNodeRoot: '/red-nodes', + + /** + * The following property can be used to configure cross-origin resource sharing + * in the HTTP nodes. + * See https://github.com/troygoode/node-cors#configuration-options for + * details on its contents. The following is a basic permissive set of options: + */ + // httpNodeCors: { + // origin: "*", + // methods: "GET,PUT,POST,DELETE" + // }, + + /** + * If you need to set an http proxy please set an environment variable + * called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system. + * For example - http_proxy=http://myproxy.com:8080 + * (Setting it here will have no effect) + * You may also specify no_proxy (or NO_PROXY) to supply a comma separated + * list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk. + */ + + /** + * The following property can be used to add a custom middleware function + * in front of all http in nodes. This allows custom authentication to be + * applied to all http in nodes, or any other sort of common request processing. + * It can be a single function or an array of middleware functions. + */ + // httpNodeMiddleware: function(req,res,next) { + // // Handle/reject the request, or pass it on to the http in node by calling next(); + // // Optionally skip our rawBodyParser by setting this to true; + // //req.skipRawBodyParser = true; + // next(); + // }, + + /** + * When httpAdminRoot is used to move the UI to a different root path, the + * following property can be used to identify a directory of static content + * that should be served at http://localhost:1880/. + * When httpStaticRoot is set differently to httpAdminRoot, there is no need + * to move httpAdminRoot. + */ + // httpStatic: '/home/nol/node-red-static/', //single static source + /* OR multiple static sources can be created using an array of objects... */ + // httpStatic: [ + // {path: '/home/nol/pics/', root: "/img/"}, + // {path: '/home/nol/reports/', root: "/doc/"}, + // ], + + /** + * All static routes will be appended to httpStaticRoot + * e.g. If httpStatic = "/home/nol/docs" and httpStaticRoot = "/static/" + * then "/home/nol/docs" will be served at "/static/" + * e.g. if httpStatic = [{path: '/home/nol/pics/', root: "/img/"}] + * and httpStaticRoot = "/static/" + * then "/home/nol/pics/" will be served at "/static/img/". + */ + // httpStaticRoot: '/static/', + +/** + * *****************************************************************************. + * Runtime Settings + * - lang + * - runtimeState + * - diagnostics + * - logging + * - contextStorage + * - exportGlobalContextKeys + * - externalModules + *****************************************************************************. + */ + + /** + * Uncomment the following to run node-red in your preferred language. + * Available languages include: en-US (default), ja, de, zh-CN, zh-TW, ru, ko + * Some languages are more complete than others. + */ + // lang: "de", + + /** + * Configure diagnostics options + * - enabled: When `enabled` is `true` (or unset), diagnostics data will + * be available at http://localhost:1880/diagnostics + * - ui: When `ui` is `true` (or unset), the action `show-system-info` will + * be available to logged in users of node-red editor. + */ + diagnostics: { + /** Enable or disable diagnostics endpoint. Must be set to `false` to disable */ + enabled: true, + /** Enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ + ui: true, + }, + /** + * Configure runtimeState options + * - enabled: When `enabled` is `true` flows runtime can be Started/Stoped + * by POSTing to available at http://localhost:1880/flows/state + * - ui: When `ui` is `true`, the action `core:start-flows` and + * `core:stop-flows` will be available to logged in users of node-red editor + * Also, the deploy menu (when set to default) will show a stop or start button. + */ + runtimeState: { + /** Enable or disable flows/state endpoint. Must be set to `false` to disable */ + enabled: false, + /** Show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */ + ui: false, + }, + /** Configure the logging output */ + logging: { + /** Only console logging is currently supported */ + console: { + /** + * Level of logging to be recorded. Options are: + * fatal - only those errors which make the application unusable should be recorded + * error - record errors which are deemed fatal for a particular request + fatal errors + * warn - record problems which are non fatal + errors + fatal errors + * info - record information about the general running of the application + warn + error + fatal errors + * debug - record information which is more verbose than info + info + warn + error + fatal errors + * trace - record very detailed logging + debug + info + warn + error + fatal errors + * off - turn off all logging (doesn't affect metrics or audit). + */ + level: 'info', + /** Whether or not to include metric events in the log output */ + metrics: false, + /** Whether or not to include audit events in the log output */ + audit: false + } + }, + + /** + * Context Storage + * The following property can be used to enable context storage. The configuration + * provided here will enable file-based context that flushes to disk every 30 seconds. + * Refer to the documentation for further options: https://nodered.org/docs/api/context/. + */ + // contextStorage: { + // default: { + // module:"localfilesystem" + // }, + // }, + + /** + * `global.keys()` returns a list of all properties set in global context. + * This allows them to be displayed in the Context Sidebar within the editor. + * In some circumstances it is not desirable to expose them to the editor. The + * following property can be used to hide any property set in `functionGlobalContext` + * from being list by `global.keys()`. + * By default, the property is set to false to avoid accidental exposure of + * their values. Setting this to true will cause the keys to be listed. + */ + exportGlobalContextKeys: false, + + /** + * Configure how the runtime will handle external npm modules. + * This covers: + * - whether the editor will allow new node modules to be installed + * - whether nodes, such as the Function node are allowed to have their + * own dynamically configured dependencies. + * The allow/denyList options can be used to limit what modules the runtime + * will install/load. It can use '*' as a wildcard that matches anything. + */ + externalModules: { + // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ + // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ + // palette: { /** Configuration for the Palette Manager */ + // allowInstall: true, /** Enable the Palette Manager in the editor */ + // allowUpdate: true, /** Allow modules to be updated in the Palette Manager */ + // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ + // allowList: ['*'], + // denyList: [], + // allowUpdateList: ['*'], + // denyUpdateList: [] + // }, + // modules: { /** Configuration for node-specified modules */ + // allowInstall: true, + // allowList: [], + // denyList: [] + // } + }, + + +/** + * *****************************************************************************. + * Editor Settings + * - disableEditor + * - editorTheme + *****************************************************************************. + */ + + /** + * The following property can be used to disable the editor. The admin API + * is not affected by this option. To disable both the editor and the admin + * API, use either the httpRoot or httpAdminRoot properties. + */ + // disableEditor: false, + + /** + * Customising the editor + * See https://nodered.org/docs/user-guide/runtime/configuration#editor-themes + * for all available options. + */ + editorTheme: { + /** + * The following property can be used to set a custom theme for the editor. + * See https://github.com/node-red-contrib-themes/theme-collection for + * a collection of themes to chose from. + */ + // theme: "", + + /** + * To disable the 'Welcome to Node-RED' tour that is displayed the first + * time you access the editor for each release of Node-RED, set this to false. + */ + // tours: false, + + palette: { + /** + * The following property can be used to order the categories in the editor + * palette. If a node's category is not in the list, the category will get + * added to the end of the palette. + * If not set, the following default order is used: + */ + // categories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'], + }, + + projects: { + /** To enable the Projects feature, set this value to true */ + enabled: false, + workflow: { + /** + * Set the default projects workflow mode. + * - manual - you must manually commit changes + * - auto - changes are automatically committed + * This can be overridden per-user from the 'Git config' + * section of 'User Settings' within the editor. + */ + mode: 'manual' + } + }, + + codeEditor: { + /** + * Select the text editor component used by the editor. + * As of Node-RED V3, this defaults to "monaco", but can be set to "ace" if desired. + */ + lib: 'monaco', + options: { + /** + * The follow options only apply if the editor is set to "monaco". + * + * Theme - must match the file name of a theme in + * packages/node_modules/@node-red/editor-client/src/vendor/monaco/dist/theme + * e.g. "tomorrow-night", "upstream-sunburst", "github", "my-theme". + */ + // theme: "vs", + /** + * Other overrides can be set e.g. FontSize, fontFamily, fontLigatures etc. + * For the full list, see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html. + */ + // fontSize: 14, + // fontFamily: "Cascadia Code, Fira Code, Consolas, 'Courier New', monospace", + // fontLigatures: true, + } + } + }, + +/** + * *****************************************************************************. + * Node Settings + * - fileWorkingDirectory + * - functionGlobalContext + * - functionExternalModules + * - nodeMessageBufferMaxLength + * - ui (for use with Node-RED Dashboard) + * - debugUseColors + * - debugMaxLength + * - execMaxBufferSize + * - httpRequestTimeout + * - mqttReconnectTime + * - serialReconnectTime + * - socketReconnectTime + * - socketTimeout + * - tcpMsgQueueSize + * - inboundWebSocketTimeout + * - tlsConfigDisableLocalFiles + * - webSocketNodeVerifyClient + *****************************************************************************. + */ + + /** + * The working directory to handle relative file paths from within the File nodes + * defaults to the working directory of the Node-RED process. + */ + // fileWorkingDirectory: "", + + /** Allow the Function node to load additional npm modules directly */ + functionExternalModules: true, + + /** + * The following property can be used to set predefined values in Global Context. + * This allows extra node modules to be made available with in Function node. + * For example, the following: + * functionGlobalContext: { os:require('os') } + * will allow the `os` module to be accessed in a Function node using: + * global.get("os"). + */ + functionGlobalContext: { + // os:require('os'), + }, + + /** + * The maximum number of messages nodes will buffer internally as part of their + * operation. This applies across a range of nodes that operate on message sequences. + * Defaults to no limit. A value of 0 also means no limit is applied. + */ + // nodeMessageBufferMaxLength: 0, + + /** + * If you installed the optional node-red-dashboard you can set it's path + * relative to httpNodeRoot + * Other optional properties include + * readOnly:{boolean}, + * middleware:{function or array}, (req,res,next) - http middleware + * ioMiddleware:{function or array}, (socket,next) - socket.io middleware. + */ + // ui: { path: "ui" }, + + /** Colourise the console output of the debug node */ + // debugUseColors: true, + + /** The maximum length, in characters, of any message sent to the debug sidebar tab */ + debugMaxLength: 1000, + + /** Maximum buffer size for the exec node. Defaults to 10Mb */ + // execMaxBufferSize: 10000000, + + /** Timeout in milliseconds for HTTP request connections. Defaults to 120s */ + // httpRequestTimeout: 120000, + + /** Retry time in milliseconds for MQTT connections */ + mqttReconnectTime: 15000, + + /** Retry time in milliseconds for Serial port connections */ + serialReconnectTime: 15000, + + /** Retry time in milliseconds for TCP socket connections */ + // socketReconnectTime: 10000, + + /** Timeout in milliseconds for TCP server socket connections. Defaults to no timeout */ + // socketTimeout: 120000, + + /** + * Maximum number of messages to wait in queue while attempting to connect to TCP socket + * defaults to 1000. + */ + // tcpMsgQueueSize: 2000, + + /** + * Timeout in milliseconds for inbound WebSocket connections that do not + * match any configured node. Defaults to 5000. + */ + // inboundWebSocketTimeout: 5000, + + /** + * To disable the option for using local files for storing keys and + * certificates in the TLS configuration node, set this to true. + */ + // tlsConfigDisableLocalFiles: true, + + /** + * The following property can be used to verify websocket connection attempts. + * This allows, for example, the HTTP request headers to be checked to ensure + * they include valid authentication information. + */ + // webSocketNodeVerifyClient: function(info) { + // /** 'info' has three properties: + // * - origin : the value in the Origin header + // * - req : the HTTP request + // * - secure : true if req.connection.authorized or req.connection.encrypted is set + // * + // * The function should return true if the connection should be accepted, false otherwise. + // * + // * Alternatively, if this function is defined to accept a second argument, callback, + // * it can be used to verify the client asynchronously. + // * The callback takes three arguments: + // * - result : boolean, whether to accept the connection or not + // * - code : if result is false, the HTTP error status to return + // * - reason: if result is false, the HTTP reason string to return + // */ + // }, +}; diff --git a/server/test/services/node-red/expectedOtherNodeRedContent.txt b/server/test/services/node-red/expectedOtherNodeRedContent.txt new file mode 100644 index 0000000000..b0bb7facd5 --- /dev/null +++ b/server/test/services/node-red/expectedOtherNodeRedContent.txt @@ -0,0 +1,597 @@ +/** + * This is the default settings file provided by Node-RED. + * + * It can contain any valid JavaScript code that will get run when Node-RED + * is started. + * + * Lines that start with // are commented out. + * Each entry should be separated from the entries above and below by a comma ','. + * + * For more information about individual settings, refer to the documentation: + * https://nodered.org/docs/user-guide/runtime/configuration. + * + * The settings are split into the following sections: + * - Flow File and User Directory Settings + * - Security + * - Server Settings + * - Runtime Settings + * - Editor Settings + * - Node Settings. + * + */ + +module.exports = { + +/** + * *****************************************************************************. + * Flow File and User Directory Settings + * - flowFile + * - credentialSecret + * - flowFilePretty + * - userDir + * - nodesDir + *****************************************************************************. + */ + + /** The file containing the flows. If not set, defaults to flows_.json */ + flowFile: 'flows.json', + + /** + * By default, credentials are encrypted in storage using a generated key. To + * specify your own secret, set the following property. + * If you want to disable encryption of credentials, set this property to false. + * Note: once you set this property, do not change it - doing so will prevent + * node-red from being able to decrypt your existing credentials and they will be + * lost. + */ + // credentialSecret: "a-secret-key", + + /** + * By default, the flow JSON will be formatted over multiple lines making + * it easier to compare changes when using version control. + * To disable pretty-printing of the JSON set the following property to false. + */ + flowFilePretty: true, + + /** + * By default, all user data is stored in a directory called `.node-red` under + * the user's home directory. To use a different location, the following + * property can be used. + */ + // userDir: '/home/nol/.node-red/', + + /** + * Node-RED scans the `nodes` directory in the userDir to find local node files. + * The following property can be used to specify an additional directory to scan. + */ + // nodesDir: '/home/nol/.node-red/nodes', + +/** + * *****************************************************************************. + * Security + * - adminAuth + * - https + * - httpsRefreshInterval + * - requireHttps + * - httpNodeAuth + * - httpStaticAuth + *****************************************************************************. + */ + + /** + * To password protect the Node-RED editor and admin API, the following + * property can be used. See http://nodered.org/docs/security.html for details. + */ + adminAuth: { + type: 'credentials', + users: [{ + username: 'other-username', + password: 'other-password', + permissions: '*' + }] + }, + + /** + * The following property can be used to enable HTTPS + * This property can be either an object, containing both a (private) key + * and a (public) certificate, or a function that returns such an object. + * See http://nodejs.org/api/https.html#https_https_createserver_options_requestlistener + * for details of its contents. + */ + + /** Option 1: static object */ + // https: { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + // }, + + /** Option 2: function that returns the HTTP configuration object */ + // https: function() { + // // This function should return the options object, or a Promise + // // that resolves to the options object + // return { + // key: require("fs").readFileSync('privkey.pem'), + // cert: require("fs").readFileSync('cert.pem') + // } + // }, + + /** + * If the `https` setting is a function, the following setting can be used + * to set how often, in hours, the function will be called. That can be used + * to refresh any certificates. + */ + // httpsRefreshInterval : 12, + + /** + * The following property can be used to cause insecure HTTP connections to + * be redirected to HTTPS. + */ + // requireHttps: true, + + /** + * To password protect the node-defined HTTP endpoints (httpNodeRoot), + * including node-red-dashboard, or the static content (httpStatic), the + * following properties can be used. + * The `pass` field is a bcrypt hash of the password. + * See http://nodered.org/docs/security.html#generating-the-password-hash. + */ + // httpNodeAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + // httpStaticAuth: {user:"user",pass:"$2a$08$zZWtXTja0fB1pzD4sHCMyOCMYz2Z6dNbM6tl8sJogENOMcxWV9DN."}, + +/** + * *****************************************************************************. + * Server Settings + * - uiPort + * - uiHost + * - apiMaxLength + * - httpServerOptions + * - httpAdminRoot + * - httpAdminMiddleware + * - httpNodeRoot + * - httpNodeCors + * - httpNodeMiddleware + * - httpStatic + * - httpStaticRoot + *****************************************************************************. + */ + + /** The tcp port that the Node-RED web server is listening on */ + uiPort: process.env.PORT || 1880, + + /** + * By default, the Node-RED UI accepts connections on all IPv4 interfaces. + * To listen on all IPv6 addresses, set uiHost to "::", + * The following property can be used to listen on a specific interface. For + * example, the following would only allow connections from the local machine. + */ + uiHost: "0.0.0.0", + + /** + * The maximum size of HTTP request that will be accepted by the runtime api. + * Default: 5mb. + */ + // apiMaxLength: '5mb', + + /** + * The following property can be used to pass custom options to the Express.js + * server used by Node-RED. For a full list of available options, refer + * to http://expressjs.com/en/api.html#app.settings.table. + */ + // httpServerOptions: { }, + + /** + * By default, the Node-RED UI is available at http://localhost:1880/ + * The following property can be used to specify a different root path. + * If set to false, this is disabled. + */ + // httpAdminRoot: '/admin', + + /** + * The following property can be used to add a custom middleware function + * in front of all admin http routes. For example, to set custom http + * headers. It can be a single function or an array of middleware functions. + */ + // httpAdminMiddleware: function(req,res,next) { + // // Set the X-Frame-Options header to limit where the editor + // // can be embedded + // //res.set('X-Frame-Options', 'sameorigin'); + // next(); + // }, + + + /** + * Some nodes, such as HTTP In, can be used to listen for incoming http requests. + * By default, these are served relative to '/'. The following property + * can be used to specifiy a different root path. If set to false, this is + * disabled. + */ + // httpNodeRoot: '/red-nodes', + + /** + * The following property can be used to configure cross-origin resource sharing + * in the HTTP nodes. + * See https://github.com/troygoode/node-cors#configuration-options for + * details on its contents. The following is a basic permissive set of options: + */ + // httpNodeCors: { + // origin: "*", + // methods: "GET,PUT,POST,DELETE" + // }, + + /** + * If you need to set an http proxy please set an environment variable + * called http_proxy (or HTTP_PROXY) outside of Node-RED in the operating system. + * For example - http_proxy=http://myproxy.com:8080 + * (Setting it here will have no effect) + * You may also specify no_proxy (or NO_PROXY) to supply a comma separated + * list of domains to not proxy, eg - no_proxy=.acme.co,.acme.co.uk. + */ + + /** + * The following property can be used to add a custom middleware function + * in front of all http in nodes. This allows custom authentication to be + * applied to all http in nodes, or any other sort of common request processing. + * It can be a single function or an array of middleware functions. + */ + // httpNodeMiddleware: function(req,res,next) { + // // Handle/reject the request, or pass it on to the http in node by calling next(); + // // Optionally skip our rawBodyParser by setting this to true; + // //req.skipRawBodyParser = true; + // next(); + // }, + + /** + * When httpAdminRoot is used to move the UI to a different root path, the + * following property can be used to identify a directory of static content + * that should be served at http://localhost:1880/. + * When httpStaticRoot is set differently to httpAdminRoot, there is no need + * to move httpAdminRoot. + */ + // httpStatic: '/home/nol/node-red-static/', //single static source + /* OR multiple static sources can be created using an array of objects... */ + // httpStatic: [ + // {path: '/home/nol/pics/', root: "/img/"}, + // {path: '/home/nol/reports/', root: "/doc/"}, + // ], + + /** + * All static routes will be appended to httpStaticRoot + * e.g. If httpStatic = "/home/nol/docs" and httpStaticRoot = "/static/" + * then "/home/nol/docs" will be served at "/static/" + * e.g. if httpStatic = [{path: '/home/nol/pics/', root: "/img/"}] + * and httpStaticRoot = "/static/" + * then "/home/nol/pics/" will be served at "/static/img/". + */ + // httpStaticRoot: '/static/', + +/** + * *****************************************************************************. + * Runtime Settings + * - lang + * - runtimeState + * - diagnostics + * - logging + * - contextStorage + * - exportGlobalContextKeys + * - externalModules + *****************************************************************************. + */ + + /** + * Uncomment the following to run node-red in your preferred language. + * Available languages include: en-US (default), ja, de, zh-CN, zh-TW, ru, ko + * Some languages are more complete than others. + */ + // lang: "de", + + /** + * Configure diagnostics options + * - enabled: When `enabled` is `true` (or unset), diagnostics data will + * be available at http://localhost:1880/diagnostics + * - ui: When `ui` is `true` (or unset), the action `show-system-info` will + * be available to logged in users of node-red editor. + */ + diagnostics: { + /** Enable or disable diagnostics endpoint. Must be set to `false` to disable */ + enabled: true, + /** Enable or disable diagnostics display in the node-red editor. Must be set to `false` to disable */ + ui: true, + }, + /** + * Configure runtimeState options + * - enabled: When `enabled` is `true` flows runtime can be Started/Stoped + * by POSTing to available at http://localhost:1880/flows/state + * - ui: When `ui` is `true`, the action `core:start-flows` and + * `core:stop-flows` will be available to logged in users of node-red editor + * Also, the deploy menu (when set to default) will show a stop or start button. + */ + runtimeState: { + /** Enable or disable flows/state endpoint. Must be set to `false` to disable */ + enabled: false, + /** Show or hide runtime stop/start options in the node-red editor. Must be set to `false` to hide */ + ui: false, + }, + /** Configure the logging output */ + logging: { + /** Only console logging is currently supported */ + console: { + /** + * Level of logging to be recorded. Options are: + * fatal - only those errors which make the application unusable should be recorded + * error - record errors which are deemed fatal for a particular request + fatal errors + * warn - record problems which are non fatal + errors + fatal errors + * info - record information about the general running of the application + warn + error + fatal errors + * debug - record information which is more verbose than info + info + warn + error + fatal errors + * trace - record very detailed logging + debug + info + warn + error + fatal errors + * off - turn off all logging (doesn't affect metrics or audit). + */ + level: 'info', + /** Whether or not to include metric events in the log output */ + metrics: false, + /** Whether or not to include audit events in the log output */ + audit: false + } + }, + + /** + * Context Storage + * The following property can be used to enable context storage. The configuration + * provided here will enable file-based context that flushes to disk every 30 seconds. + * Refer to the documentation for further options: https://nodered.org/docs/api/context/. + */ + // contextStorage: { + // default: { + // module:"localfilesystem" + // }, + // }, + + /** + * `global.keys()` returns a list of all properties set in global context. + * This allows them to be displayed in the Context Sidebar within the editor. + * In some circumstances it is not desirable to expose them to the editor. The + * following property can be used to hide any property set in `functionGlobalContext` + * from being list by `global.keys()`. + * By default, the property is set to false to avoid accidental exposure of + * their values. Setting this to true will cause the keys to be listed. + */ + exportGlobalContextKeys: false, + + /** + * Configure how the runtime will handle external npm modules. + * This covers: + * - whether the editor will allow new node modules to be installed + * - whether nodes, such as the Function node are allowed to have their + * own dynamically configured dependencies. + * The allow/denyList options can be used to limit what modules the runtime + * will install/load. It can use '*' as a wildcard that matches anything. + */ + externalModules: { + // autoInstall: false, /** Whether the runtime will attempt to automatically install missing modules */ + // autoInstallRetry: 30, /** Interval, in seconds, between reinstall attempts */ + // palette: { /** Configuration for the Palette Manager */ + // allowInstall: true, /** Enable the Palette Manager in the editor */ + // allowUpdate: true, /** Allow modules to be updated in the Palette Manager */ + // allowUpload: true, /** Allow module tgz files to be uploaded and installed */ + // allowList: ['*'], + // denyList: [], + // allowUpdateList: ['*'], + // denyUpdateList: [] + // }, + // modules: { /** Configuration for node-specified modules */ + // allowInstall: true, + // allowList: [], + // denyList: [] + // } + }, + + +/** + * *****************************************************************************. + * Editor Settings + * - disableEditor + * - editorTheme + *****************************************************************************. + */ + + /** + * The following property can be used to disable the editor. The admin API + * is not affected by this option. To disable both the editor and the admin + * API, use either the httpRoot or httpAdminRoot properties. + */ + // disableEditor: false, + + /** + * Customising the editor + * See https://nodered.org/docs/user-guide/runtime/configuration#editor-themes + * for all available options. + */ + editorTheme: { + /** + * The following property can be used to set a custom theme for the editor. + * See https://github.com/node-red-contrib-themes/theme-collection for + * a collection of themes to chose from. + */ + // theme: "", + + /** + * To disable the 'Welcome to Node-RED' tour that is displayed the first + * time you access the editor for each release of Node-RED, set this to false. + */ + // tours: false, + + palette: { + /** + * The following property can be used to order the categories in the editor + * palette. If a node's category is not in the list, the category will get + * added to the end of the palette. + * If not set, the following default order is used: + */ + // categories: ['subflows', 'common', 'function', 'network', 'sequence', 'parser', 'storage'], + }, + + projects: { + /** To enable the Projects feature, set this value to true */ + enabled: false, + workflow: { + /** + * Set the default projects workflow mode. + * - manual - you must manually commit changes + * - auto - changes are automatically committed + * This can be overridden per-user from the 'Git config' + * section of 'User Settings' within the editor. + */ + mode: 'manual' + } + }, + + codeEditor: { + /** + * Select the text editor component used by the editor. + * As of Node-RED V3, this defaults to "monaco", but can be set to "ace" if desired. + */ + lib: 'monaco', + options: { + /** + * The follow options only apply if the editor is set to "monaco". + * + * Theme - must match the file name of a theme in + * packages/node_modules/@node-red/editor-client/src/vendor/monaco/dist/theme + * e.g. "tomorrow-night", "upstream-sunburst", "github", "my-theme". + */ + // theme: "vs", + /** + * Other overrides can be set e.g. FontSize, fontFamily, fontLigatures etc. + * For the full list, see https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IStandaloneEditorConstructionOptions.html. + */ + // fontSize: 14, + // fontFamily: "Cascadia Code, Fira Code, Consolas, 'Courier New', monospace", + // fontLigatures: true, + } + } + }, + +/** + * *****************************************************************************. + * Node Settings + * - fileWorkingDirectory + * - functionGlobalContext + * - functionExternalModules + * - nodeMessageBufferMaxLength + * - ui (for use with Node-RED Dashboard) + * - debugUseColors + * - debugMaxLength + * - execMaxBufferSize + * - httpRequestTimeout + * - mqttReconnectTime + * - serialReconnectTime + * - socketReconnectTime + * - socketTimeout + * - tcpMsgQueueSize + * - inboundWebSocketTimeout + * - tlsConfigDisableLocalFiles + * - webSocketNodeVerifyClient + *****************************************************************************. + */ + + /** + * The working directory to handle relative file paths from within the File nodes + * defaults to the working directory of the Node-RED process. + */ + // fileWorkingDirectory: "", + + /** Allow the Function node to load additional npm modules directly */ + functionExternalModules: true, + + /** + * The following property can be used to set predefined values in Global Context. + * This allows extra node modules to be made available with in Function node. + * For example, the following: + * functionGlobalContext: { os:require('os') } + * will allow the `os` module to be accessed in a Function node using: + * global.get("os"). + */ + functionGlobalContext: { + // os:require('os'), + }, + + /** + * The maximum number of messages nodes will buffer internally as part of their + * operation. This applies across a range of nodes that operate on message sequences. + * Defaults to no limit. A value of 0 also means no limit is applied. + */ + // nodeMessageBufferMaxLength: 0, + + /** + * If you installed the optional node-red-dashboard you can set it's path + * relative to httpNodeRoot + * Other optional properties include + * readOnly:{boolean}, + * middleware:{function or array}, (req,res,next) - http middleware + * ioMiddleware:{function or array}, (socket,next) - socket.io middleware. + */ + // ui: { path: "ui" }, + + /** Colourise the console output of the debug node */ + // debugUseColors: true, + + /** The maximum length, in characters, of any message sent to the debug sidebar tab */ + debugMaxLength: 1000, + + /** Maximum buffer size for the exec node. Defaults to 10Mb */ + // execMaxBufferSize: 10000000, + + /** Timeout in milliseconds for HTTP request connections. Defaults to 120s */ + // httpRequestTimeout: 120000, + + /** Retry time in milliseconds for MQTT connections */ + mqttReconnectTime: 15000, + + /** Retry time in milliseconds for Serial port connections */ + serialReconnectTime: 15000, + + /** Retry time in milliseconds for TCP socket connections */ + // socketReconnectTime: 10000, + + /** Timeout in milliseconds for TCP server socket connections. Defaults to no timeout */ + // socketTimeout: 120000, + + /** + * Maximum number of messages to wait in queue while attempting to connect to TCP socket + * defaults to 1000. + */ + // tcpMsgQueueSize: 2000, + + /** + * Timeout in milliseconds for inbound WebSocket connections that do not + * match any configured node. Defaults to 5000. + */ + // inboundWebSocketTimeout: 5000, + + /** + * To disable the option for using local files for storing keys and + * certificates in the TLS configuration node, set this to true. + */ + // tlsConfigDisableLocalFiles: true, + + /** + * The following property can be used to verify websocket connection attempts. + * This allows, for example, the HTTP request headers to be checked to ensure + * they include valid authentication information. + */ + // webSocketNodeVerifyClient: function(info) { + // /** 'info' has three properties: + // * - origin : the value in the Origin header + // * - req : the HTTP request + // * - secure : true if req.connection.authorized or req.connection.encrypted is set + // * + // * The function should return true if the connection should be accepted, false otherwise. + // * + // * Alternatively, if this function is defined to accept a second argument, callback, + // * it can be used to verify the client asynchronously. + // * The callback takes three arguments: + // * - result : boolean, whether to accept the connection or not + // * - code : if result is false, the HTTP error status to return + // * - reason: if result is false, the HTTP reason string to return + // */ + // }, +}; diff --git a/server/test/services/node-red/index.test.js b/server/test/services/node-red/index.test.js new file mode 100644 index 0000000000..7f5998b84f --- /dev/null +++ b/server/test/services/node-red/index.test.js @@ -0,0 +1,48 @@ +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const NodeRedService = require('../../../services/node-red'); + +const gladys = { + event: { + emit: fake.returns, + }, + job: { + wrapper: (type, func) => { + return async () => { + return func(); + }; + }, + }, +}; + +describe('node-red service', () => { + // PREPARE + let nodeRedService; + + beforeEach(() => { + nodeRedService = NodeRedService(gladys, 'f87b7af2-ca8e-44fc-b754-444354b42fee'); + nodeRedService.device.init = fake.resolves(null); + nodeRedService.device.disconnect = fake.resolves(null); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should start service', async () => { + // EXECUTE + await nodeRedService.start(); + // ASSERT + assert.calledOnce(nodeRedService.device.init); + assert.notCalled(nodeRedService.device.disconnect); + }); + it('should stop service', async () => { + // EXECUTE + await nodeRedService.stop(); + // ASSERT + assert.calledOnce(nodeRedService.device.disconnect); + assert.notCalled(nodeRedService.device.init); + }); +}); diff --git a/server/test/services/node-red/lib/checkForContainerUpdates.test.js b/server/test/services/node-red/lib/checkForContainerUpdates.test.js new file mode 100644 index 0000000000..ae529c16a9 --- /dev/null +++ b/server/test/services/node-red/lib/checkForContainerUpdates.test.js @@ -0,0 +1,93 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const NodeRedManager = require('../../../../services/node-red/lib'); +const { DEFAULT } = require('../../../../services/node-red/lib/constants'); + +const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; + +describe('NodeRed checkForContainerUpdates', () => { + let nodeRedManager; + let gladys; + + beforeEach(() => { + gladys = { + job: { + wrapper: (type, func) => { + return async () => { + return func(); + }; + }, + }, + system: { + getContainers: fake.resolves([]), + removeContainer: fake.resolves(true), + }, + }; + + nodeRedManager = new NodeRedManager(gladys, null, serviceId); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('not updated, but no containers runnning -> it should only update config', async () => { + // PREPARE + const config = { + dockerNodeRedVersion: 'BAD_REVISION', + }; + // EXECUTE + await nodeRedManager.checkForContainerUpdates(config); + // ASSERT + assert.calledWithExactly(gladys.system.getContainers, { + all: true, + filters: { name: ['gladys-node-red'] }, + }); + assert.notCalled(gladys.system.removeContainer); + + expect(config).deep.equal({ + dockerNodeRedVersion: DEFAULT.DOCKER_NODE_RED_VERSION, + }); + }); + + it('not updated, found both containers -> it should remove containers and update config', async () => { + // PREPARE + gladys.system.getContainers = fake.resolves([{ id: 'container-id' }]); + const config = { + dockerNodeRedVersion: 'BAD_REVISION', + }; + // EXECUTE + await nodeRedManager.checkForContainerUpdates(config); + // ASSERT + assert.calledWithExactly(gladys.system.getContainers, { + all: true, + filters: { name: ['gladys-node-red'] }, + }); + + assert.calledWithExactly(gladys.system.removeContainer, 'container-id', { force: true }); + + expect(config).deep.equal({ + dockerNodeRedVersion: DEFAULT.DOCKER_NODE_RED_VERSION, + }); + }); + + it('already updated -> it should do nothing', async () => { + // PREPARE + gladys.system.getContainers = fake.resolves([{ id: 'container-id' }]); + const config = { + dockerNodeRedVersion: DEFAULT.DOCKER_NODE_RED_VERSION, + }; + // EXECUTE + await nodeRedManager.checkForContainerUpdates(config); + // ASSERT + assert.notCalled(gladys.system.getContainers); + assert.notCalled(gladys.system.removeContainer); + + expect(config).deep.equal({ + dockerNodeRedVersion: DEFAULT.DOCKER_NODE_RED_VERSION, + }); + }); +}); diff --git a/server/test/services/node-red/lib/configureContainer.test.js b/server/test/services/node-red/lib/configureContainer.test.js new file mode 100644 index 0000000000..1bf855e507 --- /dev/null +++ b/server/test/services/node-red/lib/configureContainer.test.js @@ -0,0 +1,115 @@ +const { expect } = require('chai'); +const path = require('path'); +const fs = require('fs'); + +const proxyquire = require('proxyquire').noCallThru(); + +const { fake } = require('sinon'); + +const mockPassword = require('../mockPassword'); + +const configureContainerNodeRed = proxyquire('../../../../services/node-red/lib/configureContainer', { + '../../../utils/password': mockPassword, +}); + +const NodeRedManager = proxyquire('../../../../services/node-red/lib', { + './configureContainer': configureContainerNodeRed, +}); + +const { DEFAULT } = require('../../../../services/node-red/lib/constants'); + +const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; +const basePathOnContainer = path.join(__dirname, 'container'); + +describe('NodeRed configureContainer', () => { + let nodeRedManager; + let gladys; + let mockHashSync; + + beforeEach(() => { + gladys = { + job: { + wrapper: (type, func) => { + return async () => { + return func(); + }; + }, + }, + system: { + getGladysBasePath: fake.resolves({ basePathOnHost: basePathOnContainer }), + }, + }; + + nodeRedManager = new NodeRedManager(gladys, null, serviceId); + }); + + afterEach(() => { + mockHashSync.restore(); + fs.rmSync(basePathOnContainer, { force: true, recursive: true }); + }); + + it('it should write default file', async () => { + const config = { + key: 'value', + }; + await nodeRedManager.configureContainer(config); + // Check that file has been created with defaults + const content = fs.readFileSync(path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH), 'utf8'); + expect(content.toString()).to.equal( + fs.readFileSync(path.join(__dirname, '../expectedDefaultContent.txt'), 'utf8').toString(), + ); + }); + + it('it should not override existing configuration file', async () => { + // Create directory + const configFilepath = path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH); + fs.mkdirSync(path.dirname(configFilepath), { recursive: true }); + // Create custom config file + const customConfigContent = 'content: custom'; + fs.writeFileSync(path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH), customConfigContent); + const config = { + key: 'value', + }; + + await nodeRedManager.configureContainer(config); + + const content = fs.readFileSync(path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH), 'utf8'); + expect(content.toString()).to.equal(customConfigContent); + }); + + it('it should only add credentials', async () => { + // Create directory + const configFilepath = path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH); + fs.mkdirSync(path.dirname(configFilepath), { recursive: true }); + // Create custom config file + const config = { + nodeRedUsername: 'username', + nodeRedPassword: 'password', + }; + + await nodeRedManager.configureContainer(config); + + const content = fs.readFileSync(path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH), 'utf8'); + expect(content.toString()).to.equal( + fs.readFileSync(path.join(__dirname, '../expectedNodeRedContent.txt'), 'utf8').toString(), + ); + }); + + it('it should override credentials', async () => { + // Create directory + const configFilepath = path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH); + fs.mkdirSync(path.dirname(configFilepath), { recursive: true }); + // Create custom config file + const config = { + nodeRedUsername: 'other-username', + nodeRedPassword: 'other-password', + }; + + await nodeRedManager.configureContainer(config); + + const content = fs.readFileSync(path.join(basePathOnContainer, DEFAULT.CONFIGURATION_PATH), 'utf8'); + expect(content.toString()).to.equal( + fs.readFileSync(path.join(__dirname, '../expectedOtherNodeRedContent.txt'), 'utf8').toString(), + ); + }); +}); diff --git a/server/test/services/node-red/lib/disconnect.test.js b/server/test/services/node-red/lib/disconnect.test.js new file mode 100644 index 0000000000..ab53e1abe3 --- /dev/null +++ b/server/test/services/node-red/lib/disconnect.test.js @@ -0,0 +1,56 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); + +const NodeRedManager = require('../../../../services/node-red/lib'); + +const container = { + id: 'docker-test', +}; + +const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; + +const mqtt = { + end: fake.resolves(true), + removeAllListeners: fake.resolves(true), +}; + +describe('NodeRed disconnect', () => { + let nodeRedManager; + let gladys; + + beforeEach(() => { + gladys = { + event: { + emit: fake.resolves(null), + }, + system: { + getContainers: fake.resolves([container]), + stopContainer: fake.resolves(true), + }, + }; + + nodeRedManager = new NodeRedManager(gladys, mqtt, serviceId); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('stop container', async () => { + await nodeRedManager.disconnect(); + + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, + }); + assert.calledTwice(gladys.event.emit); + assert.called(gladys.system.stopContainer); + + expect(nodeRedManager.gladysConnected).to.equal(false); + expect(nodeRedManager.nodeRedRunning).to.equal(false); + expect(nodeRedManager.nodeRedExist).to.equal(false); + }); +}); diff --git a/server/test/services/node-red/lib/getConfiguration.test.js b/server/test/services/node-red/lib/getConfiguration.test.js new file mode 100644 index 0000000000..95d3a0fee4 --- /dev/null +++ b/server/test/services/node-red/lib/getConfiguration.test.js @@ -0,0 +1,44 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); +const { fake, assert } = require('sinon'); + +const NodeRedManager = require('../../../../services/node-red/lib'); + +const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; + +describe('NodeRed getConfiguration', () => { + // PREPARE + let nodeRedManager; + let gladys; + + beforeEach(() => { + gladys = { + variable: { + getValue: fake.resolves('fake'), + }, + }; + + nodeRedManager = new NodeRedManager(gladys, serviceId); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should load stored configuration', async () => { + const result = await nodeRedManager.getConfiguration(); + + assert.callCount(gladys.variable.getValue, 4); + assert.calledWithExactly(gladys.variable.getValue, 'NODE_RED_USERNAME', serviceId); + assert.calledWithExactly(gladys.variable.getValue, 'NODE_RED_PASSWORD', serviceId); + assert.calledWithExactly(gladys.variable.getValue, 'DOCKER_NODE_RED_VERSION', serviceId); + assert.calledWithExactly(gladys.variable.getValue, 'TIMEZONE'); + + expect(result).to.deep.equal({ + dockerNodeRedVersion: 'fake', + nodeRedPassword: 'fake', + nodeRedUsername: 'fake', + timezone: 'fake', + }); + }); +}); diff --git a/server/test/services/node-red/lib/init.test.js b/server/test/services/node-red/lib/init.test.js new file mode 100644 index 0000000000..896374e7ef --- /dev/null +++ b/server/test/services/node-red/lib/init.test.js @@ -0,0 +1,120 @@ +const sinon = require('sinon'); +const { expect } = require('chai'); + +const { assert, fake } = sinon; +const proxyquire = require('proxyquire').noCallThru(); + +const mockPassword = require('../mockPassword'); + +const initNodeRed = proxyquire('../../../../services/node-red/lib/init', { + '../../../utils/password': mockPassword, +}); + +const NodeRedManager = proxyquire('../../../../services/node-red/lib', { + './init': initNodeRed, +}); + +const container = { + id: 'docker-test', +}; + +const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; + +describe('NodeRed init', () => { + let nodeRedManager; + let gladys; + + beforeEach(() => { + gladys = { + system: { + getContainers: fake.resolves([container]), + stopContainer: fake.resolves(true), + isDocker: fake.resolves(true), + getNetworkMode: fake.resolves('host'), + restartContainer: fake.resolves(true), + }, + }; + + nodeRedManager = new NodeRedManager(gladys, {}, serviceId); + + nodeRedManager.getConfiguration = sinon.stub(); + nodeRedManager.saveConfiguration = sinon.stub(); + nodeRedManager.checkForContainerUpdates = sinon.stub(); + nodeRedManager.installContainer = sinon.stub(); + + nodeRedManager.dockerBased = undefined; + nodeRedManager.networkModeValid = undefined; + }); + + afterEach(() => { + sinon.reset(); + }); + + it('it should fail because not a Docker System', async () => { + gladys.system.isDocker = fake.resolves(false); + + try { + await nodeRedManager.init(); + assert.fail(); + } catch (e) { + expect(e.message).to.equal('SYSTEM_NOT_RUNNING_DOCKER'); + } + + expect(nodeRedManager.dockerBased).to.equal(false); + assert.notCalled(nodeRedManager.getConfiguration); + assert.notCalled(nodeRedManager.saveConfiguration); + assert.notCalled(nodeRedManager.checkForContainerUpdates); + assert.notCalled(nodeRedManager.installContainer); + }); + + it('it should fail because not a host network', async () => { + gladys.system.getNetworkMode = fake.resolves('container'); + + try { + await nodeRedManager.init(); + assert.fail(); + } catch (e) { + expect(e.message).to.equal('DOCKER_BAD_NETWORK'); + } + + expect(nodeRedManager.networkModeValid).to.equal(false); + assert.notCalled(nodeRedManager.getConfiguration); + assert.notCalled(nodeRedManager.saveConfiguration); + assert.notCalled(nodeRedManager.checkForContainerUpdates); + assert.notCalled(nodeRedManager.installContainer); + }); + + it('it should install containers', async () => { + const config = { + nodeRedPassword: 'nodeRedPassword', + }; + nodeRedManager.getConfiguration.resolves({ ...config }); + + await nodeRedManager.init(); + + assert.calledOnceWithExactly(nodeRedManager.getConfiguration); + assert.calledOnceWithExactly(nodeRedManager.saveConfiguration, config); + assert.calledOnceWithExactly(nodeRedManager.checkForContainerUpdates, config); + assert.calledOnceWithExactly(nodeRedManager.installContainer, config); + }); + + it('it should save node-red params', async () => { + const config = {}; + nodeRedManager.getConfiguration.resolves({ ...config }); + + await nodeRedManager.init(); + + const expectedNewConfig = { + nodeRedPassword: 'password', + nodeRedUsername: 'admin', + }; + + assert.calledOnceWithExactly(nodeRedManager.getConfiguration); + assert.calledOnce(nodeRedManager.saveConfiguration); + assert.calledWithMatch(nodeRedManager.saveConfiguration, sinon.match(expectedNewConfig)); + assert.calledOnce(nodeRedManager.checkForContainerUpdates); + assert.calledWithMatch(nodeRedManager.checkForContainerUpdates, sinon.match(expectedNewConfig)); + assert.calledOnce(nodeRedManager.installContainer); + assert.calledWithMatch(nodeRedManager.installContainer, sinon.match(expectedNewConfig)); + }); +}); diff --git a/server/test/services/node-red/lib/installContainer.test.js b/server/test/services/node-red/lib/installContainer.test.js new file mode 100644 index 0000000000..ee720e77a6 --- /dev/null +++ b/server/test/services/node-red/lib/installContainer.test.js @@ -0,0 +1,115 @@ +const { expect } = require('chai'); +const sinon = require('sinon'); + +const { assert, fake } = sinon; + +const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); +const NodeRedManager = require('../../../../services/node-red/lib'); + +const container = { + id: 'docker-test', + state: 'running', +}; + +const config = {}; + +const containerStopped = { + id: 'docker-test', + state: 'stopped', +}; + +const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; + +describe('NodeRed installContainer', () => { + const TEMP_GLADYS_FOLDER = process.env.TEMP_FOLDER || '../.tmp'; + // PREPARE + let nodeRedManager; + let gladys; + + beforeEach(() => { + gladys = { + event: { + emit: fake.resolves(null), + }, + system: { + isDocker: fake.resolves(true), + getNetworkMode: fake.resolves('host'), + getContainers: fake.resolves([containerStopped]), + stopContainer: fake.resolves(true), + pull: fake.resolves(true), + restartContainer: fake.resolves(true), + createContainer: fake.resolves(true), + exec: fake.resolves(true), + getGladysBasePath: fake.resolves({ + basePathOnHost: '/var/lib/gladysassistant', + basePathOnContainer: TEMP_GLADYS_FOLDER, + }), + }, + }; + + nodeRedManager = new NodeRedManager(gladys, serviceId); + nodeRedManager.nodeRedRunning = false; + nodeRedManager.nodeRedExist = false; + nodeRedManager.containerRestartWaitTimeInMs = 0; + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should restart container', async function Test() { + this.timeout(6000); + + await nodeRedManager.installContainer(config); + + assert.calledWith(gladys.system.restartContainer, container.id); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, + }); + expect(nodeRedManager.nodeRedRunning).to.equal(true); + expect(nodeRedManager.nodeRedExist).to.equal(true); + }); + + it('should do nothing', async () => { + gladys.system.getContainers = fake.resolves([container]); + + await nodeRedManager.installContainer(config); + + assert.notCalled(gladys.system.restartContainer); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, + }); + expect(nodeRedManager.nodeRedRunning).to.equal(true); + expect(nodeRedManager.nodeRedExist).to.equal(true); + }); + + it('it should fail because not a Docker System', async () => { + gladys.system.isDocker = fake.resolves(false); + + try { + await nodeRedManager.installContainer(); + assert.fail(); + } catch (e) { + expect(e.message).to.equal('SYSTEM_NOT_RUNNING_DOCKER'); + } + + expect(nodeRedManager.dockerBased).to.equal(false); + expect(nodeRedManager.nodeRedRunning).to.equal(false); + expect(nodeRedManager.nodeRedExist).to.equal(false); + }); + + it('it should fail because not a host network', async () => { + gladys.system.getNetworkMode = fake.resolves('container'); + + try { + await nodeRedManager.installContainer(); + assert.fail(); + } catch (e) { + expect(e.message).to.equal('DOCKER_BAD_NETWORK'); + } + + expect(nodeRedManager.networkModeValid).to.equal(false); + expect(nodeRedManager.nodeRedRunning).to.equal(false); + expect(nodeRedManager.nodeRedExist).to.equal(false); + }); +}); diff --git a/server/test/services/node-red/lib/isEnabled.test.js b/server/test/services/node-red/lib/isEnabled.test.js new file mode 100644 index 0000000000..d1bd9bd4fc --- /dev/null +++ b/server/test/services/node-red/lib/isEnabled.test.js @@ -0,0 +1,27 @@ +const { expect } = require('chai'); + +const NodeRedManager = require('../../../../services/node-red/lib'); + +const gladys = {}; +const serviceId = '625a8a9a-aa9d-474f-8cec-0718dd4ade04'; + +describe('NodeRed isEnabled', () => { + let nodeRedService; + beforeEach(() => { + nodeRedService = new NodeRedManager(gladys, serviceId); + }); + + it('should return false', () => { + nodeRedService.nodeRedRunning = false; + + const result = nodeRedService.isEnabled(); + expect(result).to.equal(false); + }); + + it('should return true', () => { + nodeRedService.nodeRedRunning = true; + + const result = nodeRedService.isEnabled(); + expect(result).to.equal(true); + }); +}); diff --git a/server/test/services/node-red/lib/saveConfiguration.test.js b/server/test/services/node-red/lib/saveConfiguration.test.js new file mode 100644 index 0000000000..a670f08ac6 --- /dev/null +++ b/server/test/services/node-red/lib/saveConfiguration.test.js @@ -0,0 +1,58 @@ +const sinon = require('sinon'); +const { fake, assert } = require('sinon'); + +const NodeRedManager = require('../../../../services/node-red/lib'); + +const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; + +describe('NodeRed saveConfiguration', () => { + let nodeRedManager; + let gladys; + + beforeEach(() => { + gladys = { + variable: { + setValue: fake.resolves('setValue'), + destroy: fake.resolves('destroy'), + }, + }; + + nodeRedManager = new NodeRedManager(gladys, serviceId); + }); + + afterEach(() => { + sinon.reset(); + }); + + it('should store all variables', async () => { + const config = { + nodeRedUsername: 'nodeRedUsername', + nodeRedPassword: 'nodeRedPassword', + dockerNodeRedVersion: 'dockerNodeRedVersion', + }; + + await nodeRedManager.saveConfiguration(config); + + assert.callCount(gladys.variable.setValue, 4); + assert.calledWithExactly(gladys.variable.setValue, 'NODE_RED_USERNAME', config.nodeRedUsername, serviceId); + assert.calledWithExactly(gladys.variable.setValue, 'NODE_RED_PASSWORD', config.nodeRedPassword, serviceId); + assert.calledWithExactly( + gladys.variable.setValue, + 'DOCKER_NODE_RED_VERSION', + config.dockerNodeRedVersion, + serviceId, + ); + assert.calledWithExactly(gladys.variable.setValue, 'NODE_RED_URL', 'http://localhost:1880', serviceId); + }); + + it('should destroy all variables', async () => { + const config = {}; + + await nodeRedManager.saveConfiguration(config); + + assert.callCount(gladys.variable.destroy, 3); + assert.calledWithExactly(gladys.variable.destroy, 'NODE_RED_USERNAME', serviceId); + assert.calledWithExactly(gladys.variable.destroy, 'NODE_RED_PASSWORD', serviceId); + assert.calledWithExactly(gladys.variable.destroy, 'DOCKER_NODE_RED_VERSION', serviceId); + }); +}); diff --git a/server/test/services/node-red/lib/status.test.js b/server/test/services/node-red/lib/status.test.js new file mode 100644 index 0000000000..4a1d541b09 --- /dev/null +++ b/server/test/services/node-red/lib/status.test.js @@ -0,0 +1,33 @@ +const { expect } = require('chai'); + +const NodeRedManager = require('../../../../services/node-red/lib'); + +const gladys = {}; +const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; + +describe('NodeRed status', () => { + // PREPARE + let nodeRedManager; + + beforeEach(() => { + nodeRedManager = new NodeRedManager(gladys, serviceId); + nodeRedManager.nodeRedExist = true; + nodeRedManager.nodeRedRunning = true; + nodeRedManager.mqttRunning = true; + nodeRedManager.gladysConnected = true; + nodeRedManager.dockerBased = true; + nodeRedManager.networkModeValid = false; + }); + + it('get status', async () => { + // EXECUTE + const result = await nodeRedManager.status(); + // ASSERT + expect(result.nodeRedExist).that.equal(true); + expect(result.nodeRedRunning).that.equal(true); + expect(result.nodeRedEnabled).that.equal(true); + expect(result.gladysConnected).that.equal(true); + expect(result.dockerBased).that.equal(true); + expect(result.networkModeValid).that.equal(false); + }); +}); diff --git a/server/test/services/node-red/mockPassword.js b/server/test/services/node-red/mockPassword.js new file mode 100644 index 0000000000..d181ef8d30 --- /dev/null +++ b/server/test/services/node-red/mockPassword.js @@ -0,0 +1,11 @@ +module.exports = { + hash: (password) => + new Promise((resolve, reject) => { + resolve(password); + }), + compare: (password, hash) => + new Promise((resolve, reject) => { + resolve(true); + }), + generate: () => 'password', +}; From 57801f803dd9e7aa3fbb5b4ee684d85c6904a92f Mon Sep 17 00:00:00 2001 From: callemand Date: Fri, 9 Jun 2023 15:37:23 +0200 Subject: [PATCH 06/32] Fix TU --- server/test/services/node-red/lib/configureContainer.test.js | 2 -- server/test/services/node-red/lib/installContainer.test.js | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/server/test/services/node-red/lib/configureContainer.test.js b/server/test/services/node-red/lib/configureContainer.test.js index 1bf855e507..801520c4d5 100644 --- a/server/test/services/node-red/lib/configureContainer.test.js +++ b/server/test/services/node-red/lib/configureContainer.test.js @@ -24,7 +24,6 @@ const basePathOnContainer = path.join(__dirname, 'container'); describe('NodeRed configureContainer', () => { let nodeRedManager; let gladys; - let mockHashSync; beforeEach(() => { gladys = { @@ -44,7 +43,6 @@ describe('NodeRed configureContainer', () => { }); afterEach(() => { - mockHashSync.restore(); fs.rmSync(basePathOnContainer, { force: true, recursive: true }); }); diff --git a/server/test/services/node-red/lib/installContainer.test.js b/server/test/services/node-red/lib/installContainer.test.js index ee720e77a6..8b720dd284 100644 --- a/server/test/services/node-red/lib/installContainer.test.js +++ b/server/test/services/node-red/lib/installContainer.test.js @@ -41,7 +41,7 @@ describe('NodeRed installContainer', () => { createContainer: fake.resolves(true), exec: fake.resolves(true), getGladysBasePath: fake.resolves({ - basePathOnHost: '/var/lib/gladysassistant', + basePathOnHost: TEMP_GLADYS_FOLDER, basePathOnContainer: TEMP_GLADYS_FOLDER, }), }, From d3004e408f280dcd9ac7d89c96dfbf66c56278fb Mon Sep 17 00:00:00 2001 From: callemand Date: Fri, 9 Jun 2023 15:50:23 +0200 Subject: [PATCH 07/32] Add Password "eye" and hide config when service is not started --- .../all/node-red/setup-page/CheckStatus.js | 36 +++--- .../all/node-red/setup-page/SetupTab.jsx | 113 ++++++++++++------ 2 files changed, 89 insertions(+), 60 deletions(-) diff --git a/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js b/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js index 181c341053..cee4062f99 100644 --- a/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js +++ b/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js @@ -8,23 +8,15 @@ let cx = classNames.bind(style); class CheckStatus extends Component { render({ nodeRedEnabled, nodeRedExist, nodeRedRunning, toggle, dockerBased, networkModeValid, nodeRedStatus }, {}) { - /* - {props.nodeRedStatus === RequestStatus.Error && ( -

- -

- )} - */ - let buttonLabel = ''; - let textLabel = ''; + + let buttonLabel = null; + let textLabel = null; if (nodeRedStatus === RequestStatus.Getting) { buttonLabel = 'integration.nodeRed.setup.activationNodeRed'; textLabel = 'integration.nodeRed.setup.activationNodeRed'; } else if (!dockerBased) { - buttonLabel = 'integration.nodeRed.setup.nonDockerEnv'; textLabel = 'integration.nodeRed.status.nonDockerEnv'; } else if (!networkModeValid) { - buttonLabel = 'integration.nodeRed.setup.invalidDockerNetwork'; textLabel = 'integration.nodeRed.status.invalidDockerNetwork'; } else if (nodeRedEnabled) { buttonLabel = 'integration.nodeRed.setup.disableNodeRed'; @@ -46,7 +38,7 @@ class CheckStatus extends Component { class={cx('row', 'mr-0', 'ml-0', 'alert', { 'alert-success': nodeRedEnabled && nodeRedExist && nodeRedRunning, 'alert-warning': nodeRedEnabled && nodeRedExist && !nodeRedRunning, - 'alert-danger': nodeRedEnabled && !nodeRedExist, + 'alert-danger': (nodeRedEnabled && !nodeRedExist) || !dockerBased || !networkModeValid, 'alert-info': !nodeRedEnabled })} > @@ -56,15 +48,17 @@ class CheckStatus extends Component {
-
- -
+ {buttonLabel && ( +
+ +
+ )}
); diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx index b679818b84..54c94ac25c 100644 --- a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -40,6 +40,10 @@ class SetupTab extends Component { componentWillUnmount = () => { this.props.session.dispatcher.removeListener(WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, this.checkStatus); + if (this.showPasswordTimer) { + clearTimeout(this.showPasswordTimer); + this.showPasswordTimer = null; + } }; toggle = () => { @@ -125,6 +129,21 @@ class SetupTab extends Component { } }; + togglePassword = () => { + const { showPassword } = this.state; + + if (this.showPasswordTimer) { + clearTimeout(this.showPasswordTimer); + this.showPasswordTimer = null; + } + + this.setState({ showPassword: !showPassword }); + + if (!showPassword) { + this.showPasswordTimer = setTimeout(() => this.setState({ showPassword: false }), 5000); + } + }; + render( props, { @@ -136,7 +155,8 @@ class SetupTab extends Component { nodeRedUsername, nodeRedPassword, nodeRedUrl, - nodeRedStatus + nodeRedStatus, + showPassword } ) { return ( @@ -161,46 +181,61 @@ class SetupTab extends Component { toggle={this.toggle} /> -
- - - - -
+ {nodeRedRunning && ( +
+
+ + + + +
-
- - - - -
+
+ +
+ + + + + + +
+
-
- -
+
+ +
+
+ )}

From 27a4b8666c4cb1c0134dd85f4efe17606aafb538 Mon Sep 17 00:00:00 2001 From: callemand Date: Fri, 9 Jun 2023 15:58:44 +0200 Subject: [PATCH 08/32] prettier --- .../all/node-red/setup-page/CheckStatus.js | 1 - .../all/node-red/setup-page/SetupTab.jsx | 14 +++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js b/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js index cee4062f99..21bebc7216 100644 --- a/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js +++ b/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js @@ -8,7 +8,6 @@ let cx = classNames.bind(style); class CheckStatus extends Component { render({ nodeRedEnabled, nodeRedExist, nodeRedRunning, toggle, dockerBased, networkModeValid, nodeRedStatus }, {}) { - let buttonLabel = null; let textLabel = null; if (nodeRedStatus === RequestStatus.Getting) { diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx index 54c94ac25c..71398c6b63 100644 --- a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -214,13 +214,13 @@ class SetupTab extends Component { /> - - + +

From c88416f30c535bc978cfb06d94df9466227cca87 Mon Sep 17 00:00:00 2001 From: callemand Date: Tue, 27 Jun 2023 14:47:14 +0200 Subject: [PATCH 09/32] Fix test --- .../docker/gladys-node-red-container.json | 2 +- .../node-red/lib/configureContainer.js | 10 ++- server/services/node-red/lib/constants.js | 2 +- server/services/node-red/lib/disconnect.js | 9 +- .../system/system.getGladysBasePath.test.js | 8 ++ .../services/node-red/lib/disconnect.test.js | 23 ++++- .../node-red/lib/installContainer.test.js | 83 +++++++++++++++++++ server/utils/password.js | 2 +- 8 files changed, 124 insertions(+), 15 deletions(-) diff --git a/server/services/node-red/docker/gladys-node-red-container.json b/server/services/node-red/docker/gladys-node-red-container.json index b0ba02f2f0..0114f458d1 100644 --- a/server/services/node-red/docker/gladys-node-red-container.json +++ b/server/services/node-red/docker/gladys-node-red-container.json @@ -15,7 +15,7 @@ "PortBindings": { "1880/tcp": [ { - "HostPort": "1880" + "HostPort": "1881" } ] }, diff --git a/server/services/node-red/lib/configureContainer.js b/server/services/node-red/lib/configureContainer.js index 64d8d4c7e5..aea2b5ca8e 100644 --- a/server/services/node-red/lib/configureContainer.js +++ b/server/services/node-red/lib/configureContainer.js @@ -27,7 +27,7 @@ async function configureContainer(config) { await fs.access(configFilepath, constants.R_OK | constants.W_OK); logger.info('NodeRed: configuration file already exists.'); } catch (e) { - logger.info('NodeRed: Writting default configuration...'); + logger.info('NodeRed: Writing default configuration...'); await fs.copyFile(path.join(__dirname, '../docker/settings.txt'), configFilepath); configCreated = true; } @@ -38,12 +38,14 @@ async function configureContainer(config) { let configChanged = false; if (config.nodeRedPassword && config.nodeRedUsername) { // Check for changes - - const encodedPassword = await passwordUtils.hash(config.nodeRedPassword); const [, username] = fileContentString.match(/username: '(.+)'/); const [, password] = fileContentString.match(/password: '(.+)'/); - if (username !== config.nodeRedUsername || (await passwordUtils.compare(password, encodedPassword)) === false) { + if ( + username !== config.nodeRedUsername || + (await passwordUtils.compare(config.nodeRedPassword, password)) === false + ) { + const encodedPassword = await passwordUtils.hash(config.nodeRedPassword, 8); fileContentString = fileContentString.replace(/username: '(.+)'/, `username: '${config.nodeRedUsername}'`); fileContentString = fileContentString.replace(/password: '(.+)'/, `password: '${encodedPassword}'`); configChanged = true; diff --git a/server/services/node-red/lib/constants.js b/server/services/node-red/lib/constants.js index 41c8827056..36557d2931 100644 --- a/server/services/node-red/lib/constants.js +++ b/server/services/node-red/lib/constants.js @@ -1,6 +1,6 @@ const CONFIGURATION = { NODE_RED_USERNAME_VALUE: 'admin', - NODE_RED_URL_VALUE: 'http://localhost:1880', + NODE_RED_URL_VALUE: 'http://localhost:1881', NODE_RED_USERNAME: 'NODE_RED_USERNAME', NODE_RED_PASSWORD: 'NODE_RED_PASSWORD', diff --git a/server/services/node-red/lib/disconnect.js b/server/services/node-red/lib/disconnect.js index a48613fc3b..809fac0b60 100644 --- a/server/services/node-red/lib/disconnect.js +++ b/server/services/node-red/lib/disconnect.js @@ -11,11 +11,6 @@ const nodeRedContainerDescriptor = require('../docker/gladys-node-red-container. async function disconnect() { let container; - this.gladysConnected = false; - this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { - type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, - }); - // Stop NodeRed container try { const dockerContainer = await this.gladys.system.getContainers({ @@ -24,11 +19,13 @@ async function disconnect() { }); [container] = dockerContainer; await this.gladys.system.stopContainer(container.id); + + this.nodeRedRunning = false; + this.gladysConnected = false; } catch (e) { logger.warn(`NodeRed: failed to stop container ${container.id}:`, e); } - this.nodeRedRunning = false; this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); diff --git a/server/test/lib/system/system.getGladysBasePath.test.js b/server/test/lib/system/system.getGladysBasePath.test.js index bbe4a145c0..f2ed66682a 100644 --- a/server/test/lib/system/system.getGladysBasePath.test.js +++ b/server/test/lib/system/system.getGladysBasePath.test.js @@ -49,6 +49,14 @@ describe('system.getGladysBasePath', () => { basePathOnContainer: '/var/lib/gladysassistant', }); }); + it('should return default basePath because failed to get mount', async () => { + system.getContainerMounts = fake.rejects('Failed to get mounts'); + const result = await system.getGladysBasePath(); + expect(result).to.deep.equal({ + basePathOnHost: '/var/lib/gladysassistant', + basePathOnContainer: '/var/lib/gladysassistant', + }); + }); it('should return basePath from mount without SQLITE_FILE_PATH env variable', async () => { system.getContainerMounts = fake.resolves([ { diff --git a/server/test/services/node-red/lib/disconnect.test.js b/server/test/services/node-red/lib/disconnect.test.js index ab53e1abe3..a7946a0d7a 100644 --- a/server/test/services/node-red/lib/disconnect.test.js +++ b/server/test/services/node-red/lib/disconnect.test.js @@ -34,6 +34,9 @@ describe('NodeRed disconnect', () => { }; nodeRedManager = new NodeRedManager(gladys, mqtt, serviceId); + nodeRedManager.gladysConnected = true; + nodeRedManager.nodeRedRunning = true; + nodeRedManager.nodeRedExist = true; }); afterEach(() => { @@ -46,11 +49,27 @@ describe('NodeRed disconnect', () => { assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); - assert.calledTwice(gladys.event.emit); + assert.called(gladys.event.emit); assert.called(gladys.system.stopContainer); expect(nodeRedManager.gladysConnected).to.equal(false); expect(nodeRedManager.nodeRedRunning).to.equal(false); - expect(nodeRedManager.nodeRedExist).to.equal(false); + expect(nodeRedManager.nodeRedExist).to.equal(true); + }); + + it('stop container failed', async () => { + gladys.system.stopContainer = fake.rejects('Error'); + + await nodeRedManager.disconnect(); + + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, + }); + assert.called(gladys.event.emit); + assert.called(gladys.system.stopContainer); + + expect(nodeRedManager.gladysConnected).to.equal(true); + expect(nodeRedManager.nodeRedRunning).to.equal(true); + expect(nodeRedManager.nodeRedExist).to.equal(true); }); }); diff --git a/server/test/services/node-red/lib/installContainer.test.js b/server/test/services/node-red/lib/installContainer.test.js index 8b720dd284..4529edfa51 100644 --- a/server/test/services/node-red/lib/installContainer.test.js +++ b/server/test/services/node-red/lib/installContainer.test.js @@ -51,12 +51,77 @@ describe('NodeRed installContainer', () => { nodeRedManager.nodeRedRunning = false; nodeRedManager.nodeRedExist = false; nodeRedManager.containerRestartWaitTimeInMs = 0; + nodeRedManager.configureContainer = fake.resolves(true); }); afterEach(() => { sinon.reset(); }); + it('should create container', async function Test() { + const getContainers = sinon.stub(); + getContainers.onCall(0).resolves([]); + getContainers.onCall(1).resolves([container]); + + gladys.system.getContainers = getContainers; + this.timeout(6000); + + await nodeRedManager.installContainer(config); + + assert.calledWith(gladys.system.pull, 'nodered/node-red:latest'); + assert.calledWith(gladys.system.createContainer, { + AttachStderr: false, + AttachStdin: false, + AttachStdout: false, + ExposedPorts: { '1880/tcp': {} }, + HostConfig: { + Binds: ['/var/lib/gladysassistant/node-red:/data'], + BlkioWeightDevice: [], + Devices: [], + Dns: [], + DnsOptions: [], + DnsSearch: [], + LogConfig: { Config: { 'max-size': '10m' }, Type: 'json-file' }, + PortBindings: { '1880/tcp': [{ HostPort: '1881' }] }, + RestartPolicy: { Name: 'always' }, + }, + Image: 'nodered/node-red:latest', + NetworkDisabled: false, + Tty: false, + name: 'gladys-node-red', + }); + + assert.calledWith(gladys.system.restartContainer, container.id); + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, + }); + expect(nodeRedManager.nodeRedRunning).to.equal(true); + expect(nodeRedManager.nodeRedExist).to.equal(true); + }); + + it('should failed when create container failed', async function Test() { + const getContainers = sinon.stub(); + getContainers.onCall(0).resolves([]); + getContainers.onCall(1).resolves([container]); + gladys.system.pull = fake.rejects('Error'); + + gladys.system.getContainers = getContainers; + this.timeout(6000); + + try { + await nodeRedManager.installContainer(); + assert.fail(); + } catch (e) { + expect(e.message).to.equal('Error'); + } + + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, + }); + expect(nodeRedManager.nodeRedRunning).to.equal(false); + expect(nodeRedManager.nodeRedExist).to.equal(false); + }); + it('should restart container', async function Test() { this.timeout(6000); @@ -70,6 +135,24 @@ describe('NodeRed installContainer', () => { expect(nodeRedManager.nodeRedExist).to.equal(true); }); + it('should failed when restart container failed', async function Test() { + this.timeout(6000); + gladys.system.restartContainer = fake.rejects('Error'); + + try { + await nodeRedManager.installContainer(); + assert.fail(); + } catch (e) { + expect(e.message).to.equal('Error'); + } + + assert.calledWith(gladys.event.emit, EVENTS.WEBSOCKET.SEND_ALL, { + type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, + }); + expect(nodeRedManager.nodeRedExist).to.equal(false); + expect(nodeRedManager.nodeRedRunning).to.equal(false); + }); + it('should do nothing', async () => { gladys.system.getContainers = fake.resolves([container]); diff --git a/server/utils/password.js b/server/utils/password.js index 292481bf2d..30ab78827c 100644 --- a/server/utils/password.js +++ b/server/utils/password.js @@ -29,7 +29,7 @@ const generate = (length = 20, options = undefined) => { }; module.exports = { - hash: (password) => bcrypt.hash(password, SALT_ROUNDS), + hash: (password, hashRound = SALT_ROUNDS) => bcrypt.hash(password, hashRound), compare: (password, hash) => bcrypt.compare(password, hash), generate, }; From 231efc61b966c265cf67cbbfd28eb4e970eb491c Mon Sep 17 00:00:00 2001 From: callemand Date: Tue, 27 Jun 2023 15:02:41 +0200 Subject: [PATCH 10/32] Fix test --- server/test/services/node-red/lib/installContainer.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/test/services/node-red/lib/installContainer.test.js b/server/test/services/node-red/lib/installContainer.test.js index 4529edfa51..c9ddecaa1a 100644 --- a/server/test/services/node-red/lib/installContainer.test.js +++ b/server/test/services/node-red/lib/installContainer.test.js @@ -51,7 +51,7 @@ describe('NodeRed installContainer', () => { nodeRedManager.nodeRedRunning = false; nodeRedManager.nodeRedExist = false; nodeRedManager.containerRestartWaitTimeInMs = 0; - nodeRedManager.configureContainer = fake.resolves(true); + nodeRedManager.configureContainer = fake.resolves(false); }); afterEach(() => { @@ -59,6 +59,7 @@ describe('NodeRed installContainer', () => { }); it('should create container', async function Test() { + nodeRedManager.configureContainer = fake.resolves(true); const getContainers = sinon.stub(); getContainers.onCall(0).resolves([]); getContainers.onCall(1).resolves([container]); From 55911422fed33f0f53e2f4f2b0524ec474abdc58 Mon Sep 17 00:00:00 2001 From: callemand Date: Tue, 27 Jun 2023 15:21:54 +0200 Subject: [PATCH 11/32] Update saveConfiguration.test.js --- server/test/services/node-red/lib/saveConfiguration.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/test/services/node-red/lib/saveConfiguration.test.js b/server/test/services/node-red/lib/saveConfiguration.test.js index a670f08ac6..64f27a789f 100644 --- a/server/test/services/node-red/lib/saveConfiguration.test.js +++ b/server/test/services/node-red/lib/saveConfiguration.test.js @@ -42,7 +42,7 @@ describe('NodeRed saveConfiguration', () => { config.dockerNodeRedVersion, serviceId, ); - assert.calledWithExactly(gladys.variable.setValue, 'NODE_RED_URL', 'http://localhost:1880', serviceId); + assert.calledWithExactly(gladys.variable.setValue, 'NODE_RED_URL', 'http://localhost:1881', serviceId); }); it('should destroy all variables', async () => { From 55b79ac5b07c590e55d3158c88e99b4f2fc2add2 Mon Sep 17 00:00:00 2001 From: callemand Date: Tue, 11 Jul 2023 12:04:39 +0200 Subject: [PATCH 12/32] Fix url and enable nodered --- .../all/node-red/setup-page/SetupTab.jsx | 37 ++++++++++++++----- .../node-red/lib/configureContainer.js | 1 + server/services/node-red/lib/constants.js | 4 +- server/services/node-red/lib/init.js | 11 ++++-- .../services/node-red/lib/installContainer.js | 10 +++++ server/services/node-red/lib/isEnabled.js | 10 +++-- .../node-red/lib/saveConfiguration.js | 2 +- server/services/node-red/lib/status.js | 4 +- .../node-red/lib/saveConfiguration.test.js | 2 +- 9 files changed, 59 insertions(+), 22 deletions(-) diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx index 71398c6b63..3a0e713a05 100644 --- a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -6,6 +6,7 @@ import classNames from 'classnames/bind'; import style from './style.css'; import get from 'get-value'; import { WEBSOCKET_MESSAGE_TYPES } from '../../../../../../../server/utils/constants'; +import config from '../../../../../config'; let cx = classNames.bind(style); @@ -23,11 +24,21 @@ class SetupTab extends Component { const nodeRedPasswordVariable = await this.props.httpClient.get( '/api/v1/service/node-red/variable/NODE_RED_PASSWORD' ); - const nodeRedUrlVariable = await this.props.httpClient.get('/api/v1/service/node-red/variable/NODE_RED_URL'); + + const isGladysPlus = this.props.session.gatewayClient !== undefined; + let nodeRedUrl = null; + + if (isGladysPlus === false) { + const nodeRedPortVariable = await this.props.httpClient.get('/api/v1/service/node-red/variable/NODE_RED_PORT'); + + const url = new URL(config.localApiUrl); + nodeRedUrl = `${url.protocol}//${url.hostname}:${nodeRedPortVariable.value}`; + } + this.setState({ nodeRedUsername: nodeRedUsernameVariable.value, nodeRedPassword: nodeRedPasswordVariable.value, - nodeRedUrl: nodeRedUrlVariable.value + nodeRedUrl }); } catch (e) { // Variable is not set yet @@ -88,6 +99,10 @@ class SetupTab extends Component { }; stopContainer = async () => { + await this.props.httpClient.post('/api/v1/service/node-red/variable/NODERED_ENABLED', { + value: false + }); + let error = false; try { await this.props.httpClient.post('/api/v1/service/node-red/disconnect'); @@ -225,14 +240,16 @@ class SetupTab extends Component {
- + {nodeRedUrl && ( + + )}
)} diff --git a/server/services/node-red/lib/configureContainer.js b/server/services/node-red/lib/configureContainer.js index aea2b5ca8e..26fab95b7a 100644 --- a/server/services/node-red/lib/configureContainer.js +++ b/server/services/node-red/lib/configureContainer.js @@ -20,6 +20,7 @@ async function configureContainer(config) { // Create configuration path (if not exists) const configFilepath = path.join(basePathOnHost, DEFAULT.CONFIGURATION_PATH); await fs.mkdir(path.dirname(configFilepath), { recursive: true }); + // Check if config file not already exists let configCreated = false; try { diff --git a/server/services/node-red/lib/constants.js b/server/services/node-red/lib/constants.js index 36557d2931..f1a5f61202 100644 --- a/server/services/node-red/lib/constants.js +++ b/server/services/node-red/lib/constants.js @@ -1,10 +1,10 @@ const CONFIGURATION = { NODE_RED_USERNAME_VALUE: 'admin', - NODE_RED_URL_VALUE: 'http://localhost:1881', + NODE_RED_PORT_VALUE: '1881', NODE_RED_USERNAME: 'NODE_RED_USERNAME', NODE_RED_PASSWORD: 'NODE_RED_PASSWORD', - NODE_RED_URL: 'NODE_RED_URL', + NODE_RED_PORT: 'NODE_RED_PORT', DOCKER_NODE_RED_VERSION: 'DOCKER_NODE_RED_VERSION', // Variable to identify last version of NodeRed docker file is installed }; diff --git a/server/services/node-red/lib/init.js b/server/services/node-red/lib/init.js index 9828c6ef10..de820e8b98 100644 --- a/server/services/node-red/lib/init.js +++ b/server/services/node-red/lib/init.js @@ -7,16 +7,21 @@ const { PlatformNotCompatible } = require('../../../utils/coreErrors'); * @description Prepares service and starts connection with broker if needed. * @returns {Promise} Resolve when init finished. * @example - * await z2m.init(); + * await nodeRed.init(); */ async function init() { - const dockerBased = await this.gladys.system.isDocker(); + if (!(await this.isEnabled())) { + logger.info('Nodered: is not enabled, skipping...'); + return; + } + + const dockerBased = (await this.gladys.system.isDocker()) || true; if (!dockerBased) { this.dockerBased = false; throw new PlatformNotCompatible('SYSTEM_NOT_RUNNING_DOCKER'); } - const networkMode = await this.gladys.system.getNetworkMode(); + const networkMode = (await this.gladys.system.getNetworkMode()) || 'host'; if (networkMode !== 'host') { this.networkModeValid = false; throw new PlatformNotCompatible('DOCKER_BAD_NETWORK'); diff --git a/server/services/node-red/lib/installContainer.js b/server/services/node-red/lib/installContainer.js index a1bf5a03e1..b45aa080b6 100644 --- a/server/services/node-red/lib/installContainer.js +++ b/server/services/node-red/lib/installContainer.js @@ -16,6 +16,11 @@ const sleep = promisify(setTimeout); * await nodeRed.installContainer(config); */ async function installContainer(config) { + if (!(await this.isEnabled())) { + logger.info('Nodered: is not enabled, skipping...'); + return; + } + const dockerBased = await this.gladys.system.isDocker(); if (!dockerBased) { this.dockerBased = false; @@ -48,6 +53,11 @@ async function installContainer(config) { const containerDescriptorToMutate = cloneDeep(containerDescriptor); const containerLog = await this.gladys.system.createContainer(containerDescriptorToMutate); logger.trace(containerLog); + + const plop = this.gladys.system.getContainers(); + logger.info('COUCOUCOUCOU'); + logger.info(plop); + logger.info('NodeRed: successfully installed and configured as Docker container'); this.nodeRedExist = true; } catch (e) { diff --git a/server/services/node-red/lib/isEnabled.js b/server/services/node-red/lib/isEnabled.js index 4007ad91b9..f73784df0b 100644 --- a/server/services/node-red/lib/isEnabled.js +++ b/server/services/node-red/lib/isEnabled.js @@ -2,10 +2,14 @@ * @description Checks if Node-red is ready to use. * @returns {boolean} Is the Node-red environment ready to use? * @example - * nodeRed.isEnabled(); + * await nodeRed.isEnabled(); */ -function isEnabled() { - return this.nodeRedRunning; +async function isEnabled() { + const nodeRedEnabled = await this.gladys.variable.getValue('NODERED_ENABLED', this.serviceId); + if (nodeRedEnabled === '1') { + return true; + } + return false; } module.exports = { diff --git a/server/services/node-red/lib/saveConfiguration.js b/server/services/node-red/lib/saveConfiguration.js index fa1bea7f08..f5d71cc886 100644 --- a/server/services/node-red/lib/saveConfiguration.js +++ b/server/services/node-red/lib/saveConfiguration.js @@ -23,7 +23,7 @@ async function saveConfiguration(config) { [CONFIGURATION.NODE_RED_USERNAME]: config.nodeRedUsername, [CONFIGURATION.NODE_RED_PASSWORD]: config.nodeRedPassword, [CONFIGURATION.DOCKER_NODE_RED_VERSION]: config.dockerNodeRedVersion, - [CONFIGURATION.NODE_RED_URL]: CONFIGURATION.NODE_RED_URL_VALUE, + [CONFIGURATION.NODE_RED_PORT]: CONFIGURATION.NODE_RED_PORT_VALUE, }; const variableKeys = Object.keys(keyValueMap); diff --git a/server/services/node-red/lib/status.js b/server/services/node-red/lib/status.js index d44e912d8e..47976aa7be 100644 --- a/server/services/node-red/lib/status.js +++ b/server/services/node-red/lib/status.js @@ -4,8 +4,8 @@ * @example * status(); */ -function status() { - const nodeRedEnabled = this.isEnabled(); +async function status() { + const nodeRedEnabled = await this.isEnabled(); const nodeRedStatus = { nodeRedExist: this.nodeRedExist, nodeRedRunning: this.nodeRedRunning, diff --git a/server/test/services/node-red/lib/saveConfiguration.test.js b/server/test/services/node-red/lib/saveConfiguration.test.js index 64f27a789f..b7e5b5f964 100644 --- a/server/test/services/node-red/lib/saveConfiguration.test.js +++ b/server/test/services/node-red/lib/saveConfiguration.test.js @@ -42,7 +42,7 @@ describe('NodeRed saveConfiguration', () => { config.dockerNodeRedVersion, serviceId, ); - assert.calledWithExactly(gladys.variable.setValue, 'NODE_RED_URL', 'http://localhost:1881', serviceId); + assert.calledWithExactly(gladys.variable.setValue, 'NODE_RED_PORT', '1881', serviceId); }); it('should destroy all variables', async () => { From b9d2478751c5dccd4d8c598e5c288ff695e5136e Mon Sep 17 00:00:00 2001 From: callemand Date: Tue, 11 Jul 2023 15:26:10 +0200 Subject: [PATCH 13/32] fix eslint and prettier --- front/src/components/app.jsx | 2 +- server/services/node-red/lib/init.js | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/front/src/components/app.jsx b/front/src/components/app.jsx index d25823ef37..7bbaa89da8 100644 --- a/front/src/components/app.jsx +++ b/front/src/components/app.jsx @@ -145,7 +145,7 @@ import MELCloudSetupPage from '../routes/integration/all/melcloud/setup-page'; import MELCloudDiscoverPage from '../routes/integration/all/melcloud/discover-page'; // NodeRed integration -import NodeRedPage from "../routes/integration/all/node-red/setup-page"; +import NodeRedPage from '../routes/integration/all/node-red/setup-page'; const defaultState = getDefaultState(); const store = createStore(defaultState); diff --git a/server/services/node-red/lib/init.js b/server/services/node-red/lib/init.js index de820e8b98..86462a7d66 100644 --- a/server/services/node-red/lib/init.js +++ b/server/services/node-red/lib/init.js @@ -44,8 +44,6 @@ async function init() { } await this.saveConfiguration(configuration); - - return null; } module.exports = { From f1e95d4a97956eb21f433f9ad6bf4cc9910463ca Mon Sep 17 00:00:00 2001 From: callemand Date: Tue, 11 Jul 2023 15:54:06 +0200 Subject: [PATCH 14/32] Fix test --- server/services/node-red/lib/init.js | 4 ++-- .../services/node-red/lib/installContainer.js | 4 ---- .../test/services/node-red/lib/init.test.js | 14 +++++++++++ .../node-red/lib/installContainer.test.js | 16 +++++++++++++ .../services/node-red/lib/isEnabled.test.js | 24 +++++++++++++------ .../test/services/node-red/lib/status.test.js | 7 +++++- 6 files changed, 55 insertions(+), 14 deletions(-) diff --git a/server/services/node-red/lib/init.js b/server/services/node-red/lib/init.js index 86462a7d66..ea3b4aadd5 100644 --- a/server/services/node-red/lib/init.js +++ b/server/services/node-red/lib/init.js @@ -15,13 +15,13 @@ async function init() { return; } - const dockerBased = (await this.gladys.system.isDocker()) || true; + const dockerBased = await this.gladys.system.isDocker(); if (!dockerBased) { this.dockerBased = false; throw new PlatformNotCompatible('SYSTEM_NOT_RUNNING_DOCKER'); } - const networkMode = (await this.gladys.system.getNetworkMode()) || 'host'; + const networkMode = await this.gladys.system.getNetworkMode(); if (networkMode !== 'host') { this.networkModeValid = false; throw new PlatformNotCompatible('DOCKER_BAD_NETWORK'); diff --git a/server/services/node-red/lib/installContainer.js b/server/services/node-red/lib/installContainer.js index b45aa080b6..0326957dd8 100644 --- a/server/services/node-red/lib/installContainer.js +++ b/server/services/node-red/lib/installContainer.js @@ -54,10 +54,6 @@ async function installContainer(config) { const containerLog = await this.gladys.system.createContainer(containerDescriptorToMutate); logger.trace(containerLog); - const plop = this.gladys.system.getContainers(); - logger.info('COUCOUCOUCOU'); - logger.info(plop); - logger.info('NodeRed: successfully installed and configured as Docker container'); this.nodeRedExist = true; } catch (e) { diff --git a/server/test/services/node-red/lib/init.test.js b/server/test/services/node-red/lib/init.test.js index 896374e7ef..1a31473689 100644 --- a/server/test/services/node-red/lib/init.test.js +++ b/server/test/services/node-red/lib/init.test.js @@ -33,6 +33,9 @@ describe('NodeRed init', () => { getNetworkMode: fake.resolves('host'), restartContainer: fake.resolves(true), }, + variable: { + getValue: fake.resolves('1'), + }, }; nodeRedManager = new NodeRedManager(gladys, {}, serviceId); @@ -117,4 +120,15 @@ describe('NodeRed init', () => { assert.calledOnce(nodeRedManager.installContainer); assert.calledWithMatch(nodeRedManager.installContainer, sinon.match(expectedNewConfig)); }); + + it('should not init if the service is not enabled', async () => { + gladys.variable.getValue = fake.resolves('0'); + + await nodeRedManager.init(); + + assert.notCalled(nodeRedManager.getConfiguration); + assert.notCalled(nodeRedManager.saveConfiguration); + assert.notCalled(nodeRedManager.checkForContainerUpdates); + assert.notCalled(nodeRedManager.installContainer); + }); }); diff --git a/server/test/services/node-red/lib/installContainer.test.js b/server/test/services/node-red/lib/installContainer.test.js index c9ddecaa1a..c5125a4a11 100644 --- a/server/test/services/node-red/lib/installContainer.test.js +++ b/server/test/services/node-red/lib/installContainer.test.js @@ -45,6 +45,9 @@ describe('NodeRed installContainer', () => { basePathOnContainer: TEMP_GLADYS_FOLDER, }), }, + variable: { + getValue: fake.resolves('1'), + }, }; nodeRedManager = new NodeRedManager(gladys, serviceId); @@ -196,4 +199,17 @@ describe('NodeRed installContainer', () => { expect(nodeRedManager.nodeRedRunning).to.equal(false); expect(nodeRedManager.nodeRedExist).to.equal(false); }); + + it('should not create container if the service is not enabled', async () => { + gladys.variable.getValue = fake.resolves('0'); + + await nodeRedManager.installContainer(config); + + assert.notCalled(gladys.system.pull); + assert.notCalled(gladys.system.createContainer); + assert.notCalled(gladys.system.restartContainer); + + expect(nodeRedManager.nodeRedRunning).to.equal(false); + expect(nodeRedManager.nodeRedExist).to.equal(false); + }); }); diff --git a/server/test/services/node-red/lib/isEnabled.test.js b/server/test/services/node-red/lib/isEnabled.test.js index d1bd9bd4fc..4709d96396 100644 --- a/server/test/services/node-red/lib/isEnabled.test.js +++ b/server/test/services/node-red/lib/isEnabled.test.js @@ -1,8 +1,13 @@ const { expect } = require('chai'); +const { fake } = require('sinon'); const NodeRedManager = require('../../../../services/node-red/lib'); -const gladys = {}; +const gladys = { + variable: { + getValue: fake.resolves(null), + }, +}; const serviceId = '625a8a9a-aa9d-474f-8cec-0718dd4ade04'; describe('NodeRed isEnabled', () => { @@ -11,17 +16,22 @@ describe('NodeRed isEnabled', () => { nodeRedService = new NodeRedManager(gladys, serviceId); }); - it('should return false', () => { - nodeRedService.nodeRedRunning = false; + it('should return false when value not exist', async () => { + const result = await nodeRedService.isEnabled(); + expect(result).to.equal(false); + }); + + it('should return false', async () => { + gladys.variable.getValue = fake.resolves('0'); - const result = nodeRedService.isEnabled(); + const result = await nodeRedService.isEnabled(); expect(result).to.equal(false); }); - it('should return true', () => { - nodeRedService.nodeRedRunning = true; + it('should return true', async () => { + gladys.variable.getValue = fake.resolves('1'); - const result = nodeRedService.isEnabled(); + const result = await nodeRedService.isEnabled(); expect(result).to.equal(true); }); }); diff --git a/server/test/services/node-red/lib/status.test.js b/server/test/services/node-red/lib/status.test.js index 4a1d541b09..fd42fc8837 100644 --- a/server/test/services/node-red/lib/status.test.js +++ b/server/test/services/node-red/lib/status.test.js @@ -1,8 +1,13 @@ const { expect } = require('chai'); +const { fake } = require('sinon'); const NodeRedManager = require('../../../../services/node-red/lib'); -const gladys = {}; +const gladys = { + variable: { + getValue: fake.resolves('1'), + }, +}; const serviceId = 'f87b7af2-ca8e-44fc-b754-444354b42fee'; describe('NodeRed status', () => { From 18069a976a5d65f3217db165219212ad86bff2d4 Mon Sep 17 00:00:00 2001 From: callemand Date: Mon, 4 Sep 2023 10:09:45 +0200 Subject: [PATCH 15/32] Fixing review --- front/src/config/i18n/fr.json | 4 ++-- .../routes/integration/all/node-red/setup-page/SetupTab.jsx | 4 ++-- server/services/node-red/lib/configureContainer.js | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 1c8f6bea42..f445296bc2 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -748,12 +748,12 @@ }, "setup": { "title": "Configuration du service Node-red", - "description": "Ce service utilise un container Docker. Activer Node-red pour déployer ce container.\nPour en savoir plus, rendez-vous sur la page de documentation Node-red", + "description": "Ce service utilise un container Docker. Activez Node-red pour déployer ce container.\nPour en savoir plus, rendez-vous sur la page de documentation Node-red.", "error": "Une erreur s'est produite au démarrage du service Node-red.", "enableLabel": "Activation du service Node-red", "enableNodeRed": "Activer", - "disableNodeRed": "Desactiver", + "disableNodeRed": "Désactiver", "activationNodeRed": "Activation...", "serviceStatus": "Etat du service Node-red", "containersStatus": "Conteneurs liés à Node-red", diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx index 3a0e713a05..f2444fa8d3 100644 --- a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -94,7 +94,7 @@ class SetupTab extends Component { nodeRedStatus: RequestStatus.Success }); } - + await this.getConfiguration(); await this.checkStatus(); }; @@ -119,7 +119,7 @@ class SetupTab extends Component { nodeRedStatus: RequestStatus.Success }); } - + await this.getConfiguration(); await this.checkStatus(); }; diff --git a/server/services/node-red/lib/configureContainer.js b/server/services/node-red/lib/configureContainer.js index 26fab95b7a..2216a1a4a5 100644 --- a/server/services/node-red/lib/configureContainer.js +++ b/server/services/node-red/lib/configureContainer.js @@ -20,6 +20,7 @@ async function configureContainer(config) { // Create configuration path (if not exists) const configFilepath = path.join(basePathOnHost, DEFAULT.CONFIGURATION_PATH); await fs.mkdir(path.dirname(configFilepath), { recursive: true }); + await fs.chown(path.dirname(configFilepath), 1000, 1000); // Check if config file not already exists let configCreated = false; From 71e739f7e6937edb58a17093159fee5e1738d835 Mon Sep 17 00:00:00 2001 From: callemand Date: Mon, 4 Sep 2023 10:20:32 +0200 Subject: [PATCH 16/32] Fix test --- server/services/node-red/lib/configureContainer.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/services/node-red/lib/configureContainer.js b/server/services/node-red/lib/configureContainer.js index 2216a1a4a5..da84f49e05 100644 --- a/server/services/node-red/lib/configureContainer.js +++ b/server/services/node-red/lib/configureContainer.js @@ -20,7 +20,11 @@ async function configureContainer(config) { // Create configuration path (if not exists) const configFilepath = path.join(basePathOnHost, DEFAULT.CONFIGURATION_PATH); await fs.mkdir(path.dirname(configFilepath), { recursive: true }); - await fs.chown(path.dirname(configFilepath), 1000, 1000); + try { + await fs.chown(path.dirname(configFilepath), 1000, 1000); + } catch (e) { + logger.error('NodeRed: Unable to change write of the configuration'); + } // Check if config file not already exists let configCreated = false; From db32d83d9e154c8b5ab000b23562eeb48c24a4db Mon Sep 17 00:00:00 2001 From: callemand Date: Tue, 5 Sep 2023 07:52:56 +0200 Subject: [PATCH 17/32] Fix review --- .../all/node-red/setup-page/CheckStatus.js | 6 +++--- server/services/node-red/lib/disconnect.js | 1 + .../services/node-red/lib/disconnect.test.js | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js b/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js index 21bebc7216..d380e865d0 100644 --- a/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js +++ b/front/src/routes/integration/all/node-red/setup-page/CheckStatus.js @@ -34,21 +34,21 @@ class CheckStatus extends Component { return (
-
+
{buttonLabel && ( -
+
- - {buttonLabel && ( -
- -
- )}
); diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx index f2444fa8d3..9aed7e8805 100644 --- a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -174,6 +174,21 @@ class SetupTab extends Component { showPassword } ) { + let buttonLabel = null; + let buttonClassColor = null; + if (dockerBased && networkModeValid) { + if (nodeRedStatus === RequestStatus.Getting) { + buttonClassColor = 'btn-primary'; + buttonLabel = 'integration.nodeRed.setup.activationNodeRed'; + } else if (nodeRedEnabled) { + buttonClassColor = 'btn-danger'; + buttonLabel = 'integration.nodeRed.setup.disableNodeRed'; + } else { + buttonClassColor = 'btn-primary'; + buttonLabel = 'integration.nodeRed.setup.enableNodeRed'; + } + } + return (
@@ -193,7 +208,6 @@ class SetupTab extends Component { dockerBased={dockerBased} networkModeValid={networkModeValid} nodeRedStatus={nodeRedStatus} - toggle={this.toggle} /> {nodeRedRunning && ( @@ -254,6 +268,18 @@ class SetupTab extends Component {
)} + {buttonLabel && ( +
+ +
+ )} +

From 7e43cc8bd2a06c9e07a9b3f7042986600c076182 Mon Sep 17 00:00:00 2001 From: callemand Date: Fri, 8 Sep 2023 08:12:31 +0200 Subject: [PATCH 19/32] Change UX --- front/src/config/i18n/en.json | 4 +- front/src/config/i18n/fr.json | 4 +- .../all/node-red/setup-page/SetupTab.jsx | 314 ++++++++++-------- 3 files changed, 178 insertions(+), 144 deletions(-) diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index 0cc44fc978..cc37bb7973 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -628,6 +628,8 @@ "enableLabel": "Node-red activation", "enableNodeRed": "Enable", "disableNodeRed": "Disable", + "confirmDisableLabel": "Are you sur you to disable NodeRed ?", + "confirmDisableCancelButton": "Cancel", "activationNodeRed": "Activating...", "serviceStatus": "Node-red Service Status", "containersStatus": "Containers related to Node-red", @@ -636,7 +638,7 @@ "gladys": "Gladys", "usernameLabel": "Username", "passwordLabel": "Password", - "urlLabel": "Node-red interface url: {{nodeRedUrl}}" + "urlLabel": "Node-red interface url: {{nodeRedUrl}} (Not accessible from Gladys Plus)" } }, diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index f445296bc2..1eba9eaf19 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -754,6 +754,8 @@ "enableNodeRed": "Activer", "disableNodeRed": "Désactiver", + "confirmDisableLabel": "Etes-vous sûr de vouloir désactiver NodeRed ?", + "confirmDisableCancelButton": "Annuler", "activationNodeRed": "Activation...", "serviceStatus": "Etat du service Node-red", "containersStatus": "Conteneurs liés à Node-red", @@ -762,7 +764,7 @@ "gladys": "Gladys", "usernameLabel": "Nom d'utilisateur", "passwordLabel": "Mot de passe", - "urlLabel": "Url de l'interface Node-red : {{nodeRedUrl}}" + "urlLabel": "Url de l'interface Node-red : {{nodeRedUrl}} (Pas accessible depuis Gladys Plus)" } }, diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx index 9aed7e8805..8755094bf7 100644 --- a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -159,6 +159,14 @@ class SetupTab extends Component { } }; + showConfirmDelete = () => { + this.setState({ showConfirmDelete: true }); + }; + + cancelDisable = () => { + this.setState({ showConfirmDelete: false }); + }; + render( props, { @@ -171,23 +179,11 @@ class SetupTab extends Component { nodeRedPassword, nodeRedUrl, nodeRedStatus, - showPassword + showPassword, + showConfirmDelete } ) { - let buttonLabel = null; - let buttonClassColor = null; - if (dockerBased && networkModeValid) { - if (nodeRedStatus === RequestStatus.Getting) { - buttonClassColor = 'btn-primary'; - buttonLabel = 'integration.nodeRed.setup.activationNodeRed'; - } else if (nodeRedEnabled) { - buttonClassColor = 'btn-danger'; - buttonLabel = 'integration.nodeRed.setup.disableNodeRed'; - } else { - buttonClassColor = 'btn-primary'; - buttonLabel = 'integration.nodeRed.setup.enableNodeRed'; - } - } + const confirmationDisableMode = true; return (
@@ -268,136 +264,170 @@ class SetupTab extends Component {
)} - {buttonLabel && ( -
- -
+ {dockerBased && networkModeValid && nodeRedEnabled && !showConfirmDelete && ( + )} - -
-

- -

-
-
-
- - - - - - - - - - - {nodeRedEnabled && ( - - )} - - - - - - - -
- - - {nodeRedEnabled && 'Node-red'}
- {`Gladys`} - -
- -
-
- {nodeRedEnabled && ( - {`Node-red`} - )} -
-
- -
-
- - {nodeRedRunning && ( - - - - )} -
+ {dockerBased && networkModeValid && !nodeRedEnabled && !showConfirmDelete && ( + + )} + {dockerBased && networkModeValid && nodeRedEnabled && showConfirmDelete && ( +
+ +
+ + +
-
-
-

- -

-
-
-
- - - - - - - - - - - - - - - - - -
- - - -
- - - - - -
- - - {nodeRedRunning && ( - - - - )} -
+ )} + {nodeRedRunning && ( +
+
+

+ +

+
+
+
+ + + + + + + + + + + {nodeRedEnabled && ( + + )} + + + + + + + +
+ + + {nodeRedEnabled && 'Node-red'}
+ {`Gladys`} + +
+ +
+
+ {nodeRedEnabled && ( + {`Node-red`} + )} +
+
+ +
+
+ + {nodeRedRunning && ( + + + + )} +
+
+
+
+

+ +

+
+
+
+ + + + + + + + + + + + + + + + + +
+ + + +
+ + + + + +
+ + + {nodeRedRunning && ( + + + + )} +
+
+
-
+ )}
); From 55174d1c81f1d17a2f341ea8eccfd4620d2dcc8e Mon Sep 17 00:00:00 2001 From: callemand Date: Fri, 8 Sep 2023 08:17:34 +0200 Subject: [PATCH 20/32] Fix eslint --- .../src/routes/integration/all/node-red/setup-page/SetupTab.jsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx index 8755094bf7..ffab0160de 100644 --- a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -183,8 +183,6 @@ class SetupTab extends Component { showConfirmDelete } ) { - const confirmationDisableMode = true; - return (
From c1c285bad88fc2c8bf06bb88ba0dc71273287234 Mon Sep 17 00:00:00 2001 From: callemand Date: Mon, 11 Sep 2023 10:40:25 +0200 Subject: [PATCH 21/32] Fix after review Bad format name Node-RED Delete node-red folder when disable integration Fix typo --- front/src/config/i18n/en.json | 34 +++++++++--------- front/src/config/i18n/fr.json | 36 +++++++++---------- .../integration/all/node-red/NodeRedPage.js | 2 +- .../all/node-red/setup-page/SetupTab.jsx | 6 ++-- server/lib/system/system.getGladysBasePath.js | 2 +- .../node-red/api/node-red.controller.js | 12 +++---- .../docker/gladys-node-red-container.json | 2 +- server/services/node-red/index.js | 4 +-- .../node-red/lib/checkForContainerUpdates.js | 8 ++--- .../node-red/lib/configureContainer.js | 14 ++++---- server/services/node-red/lib/disconnect.js | 11 +++++- .../services/node-red/lib/getConfiguration.js | 6 ++-- server/services/node-red/lib/index.js | 2 +- server/services/node-red/lib/init.js | 2 +- .../services/node-red/lib/installContainer.js | 12 +++---- server/services/node-red/lib/isEnabled.js | 4 +-- .../node-red/lib/saveConfiguration.js | 10 +++--- server/services/node-red/lib/status.js | 4 +-- .../services/node-red/lib/disconnect.test.js | 6 ++++ 19 files changed, 96 insertions(+), 81 deletions(-) diff --git a/front/src/config/i18n/en.json b/front/src/config/i18n/en.json index cc37bb7973..c0dd631b66 100644 --- a/front/src/config/i18n/en.json +++ b/front/src/config/i18n/en.json @@ -608,37 +608,37 @@ "documentation": "Zigbee2mqtt documentation" }, "nodeRed": { - "title": "Node-red", - "description": "\"Control your devices with Node-red.", + "title": "Node-RED", + "description": "\"Control your devices with Node-RED.", "setupTab": "Setup", - "documentation": "Node-red documentation", + "documentation": "Node-RED documentation", "status": { - "notInstalled": "Node-red server failed to install.", - "notRunning": "Node-red server failed to start.", - "running": "Node-red successfully started.", - "notEnabled": "Node-red is not activated.", - "nonDockerEnv": "Gladys is not running on Docker, you cannot install a Node-red server from here.", + "notInstalled": "Node-RED server failed to install.", + "notRunning": "Node-RED server failed to start.", + "running": "Node-RED successfully started.", + "notEnabled": "Node-RED is not activated.", + "nonDockerEnv": "Gladys is not running on Docker, you cannot install a Node-RED server from here.", "invalidDockerNetwork": "Gladys is under custom installation, to install server from here, Gladys container should be configured with \"host\" network mode." }, "setup": { - "title": "Node-red configuration", - "description": "This service uses docker container. Enable Node-red for deploying this container.\nLearn more on the node-red documentation page", - "error": "An error occured while starting Node-red.", - "enableLabel": "Node-red activation", + "title": "Node-RED configuration", + "description": "This service uses docker container. Enable Node-RED for deploying this container.\nLearn more on the node-red documentation page", + "error": "An error occured while starting Node-RED.", + "enableLabel": "Node-RED activation", "enableNodeRed": "Enable", "disableNodeRed": "Disable", - "confirmDisableLabel": "Are you sur you to disable NodeRed ?", + "confirmDisableLabel": "Are you sur you to disable Node-RED ?", "confirmDisableCancelButton": "Cancel", "activationNodeRed": "Activating...", - "serviceStatus": "Node-red Service Status", - "containersStatus": "Containers related to Node-red", + "serviceStatus": "Node-RED Service Status", + "containersStatus": "Containers related to Node-RED", "status": "Status", - "node-red": "Node-red", + "node-red": "Node-RED", "gladys": "Gladys", "usernameLabel": "Username", "passwordLabel": "Password", - "urlLabel": "Node-red interface url: {{nodeRedUrl}} (Not accessible from Gladys Plus)" + "urlLabel": "Node-RED interface url: {{nodeRedUrl}} (Not accessible from Gladys Plus)" } }, diff --git a/front/src/config/i18n/fr.json b/front/src/config/i18n/fr.json index 1eba9eaf19..ca9aad022b 100644 --- a/front/src/config/i18n/fr.json +++ b/front/src/config/i18n/fr.json @@ -734,37 +734,37 @@ "documentation": "Documentation Zigbee2mqtt" }, "nodeRed": { - "title": "Node-red", - "description": "Contrôlez vos appareils via Node-red.", + "title": "Node-RED", + "description": "Contrôlez vos appareils via Node-RED.", "setupTab": "Configuration", - "documentation": "Documentation Node-red", + "documentation": "Documentation Node-RED", "status": { - "notInstalled": "Le server Node-red n'a pas pu être installé.", - "notRunning": "Le server Node-red n'a pas démarré.", - "running": "Node-red démarré avec succès.", - "notEnabled": "Node-red n'est pas activé.", - "nonDockerEnv": "Gladys ne s'exécute actuellement pas dans Docker, il est impossible d'activer Node-red depuis Gladys.", - "invalidDockerNetwork": "Gladys est basée sur une installation personalisée, pour installer Node-red depuis cette page, le conteneur Docker de Gladys doit être démarré avec l'option \"network=host\"." + "notInstalled": "Le serveur Node-RED n'a pas pu être installé.", + "notRunning": "Le serveur Node-RED n'a pas démarré.", + "running": "Node-RED démarré avec succès.", + "notEnabled": "Node-RED n'est pas activé.", + "nonDockerEnv": "Gladys ne s'exécute actuellement pas dans Docker, il est impossible d'activer Node-RED depuis Gladys.", + "invalidDockerNetwork": "Gladys est basée sur une installation personalisée, pour installer Node-RED depuis cette page, le conteneur Docker de Gladys doit être démarré avec l'option \"network=host\"." }, "setup": { - "title": "Configuration du service Node-red", - "description": "Ce service utilise un container Docker. Activez Node-red pour déployer ce container.\nPour en savoir plus, rendez-vous sur la page de documentation Node-red.", - "error": "Une erreur s'est produite au démarrage du service Node-red.", - "enableLabel": "Activation du service Node-red", + "title": "Configuration du service Node-RED", + "description": "Ce service utilise un container Docker. Activez Node-RED pour déployer ce container.\nPour en savoir plus, rendez-vous sur la page de documentation Node-RED.", + "error": "Une erreur s'est produite au démarrage du service Node-RED.", + "enableLabel": "Activation du service Node-RED", "enableNodeRed": "Activer", "disableNodeRed": "Désactiver", - "confirmDisableLabel": "Etes-vous sûr de vouloir désactiver NodeRed ?", + "confirmDisableLabel": "Etes-vous sûr de vouloir désactiver Node-RED ?", "confirmDisableCancelButton": "Annuler", "activationNodeRed": "Activation...", - "serviceStatus": "Etat du service Node-red", - "containersStatus": "Conteneurs liés à Node-red", + "serviceStatus": "Etat du service Node-RED", + "containersStatus": "Conteneurs liés à Node-RED", "status": "Status", - "node-red": "Node-red", + "node-red": "Node-RED", "gladys": "Gladys", "usernameLabel": "Nom d'utilisateur", "passwordLabel": "Mot de passe", - "urlLabel": "Url de l'interface Node-red : {{nodeRedUrl}} (Pas accessible depuis Gladys Plus)" + "urlLabel": "Url de l'interface Node-RED : {{nodeRedUrl}} (Pas accessible depuis Gladys Plus)" } }, diff --git a/front/src/routes/integration/all/node-red/NodeRedPage.js b/front/src/routes/integration/all/node-red/NodeRedPage.js index 6ec3ef10e7..56839bba67 100644 --- a/front/src/routes/integration/all/node-red/NodeRedPage.js +++ b/front/src/routes/integration/all/node-red/NodeRedPage.js @@ -27,7 +27,7 @@ const NodeRedPage = ({ children, user }) => ( diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx index ffab0160de..a447c723fc 100644 --- a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -320,7 +320,7 @@ class SetupTab extends Component { - {nodeRedEnabled && 'Node-red'} + {nodeRedEnabled && 'Node-RED'} @@ -352,8 +352,8 @@ class SetupTab extends Component { {nodeRedEnabled && ( {`Node-red`} diff --git a/server/lib/system/system.getGladysBasePath.js b/server/lib/system/system.getGladysBasePath.js index 77375a61fb..62d6e01228 100644 --- a/server/lib/system/system.getGladysBasePath.js +++ b/server/lib/system/system.getGladysBasePath.js @@ -26,7 +26,7 @@ async function getGladysBasePath() { } } } catch (e) { - logger.warn(`NodeRed: Error while fetching container mounts: ${e.message}`); + logger.warn(`Node-RED: Error while fetching container mounts: ${e.message}`); } return { basePathOnContainer, basePathOnHost: '/var/lib/gladysassistant' }; } diff --git a/server/services/node-red/api/node-red.controller.js b/server/services/node-red/api/node-red.controller.js index b8903c1ad8..e35ae130dd 100644 --- a/server/services/node-red/api/node-red.controller.js +++ b/server/services/node-red/api/node-red.controller.js @@ -5,7 +5,7 @@ module.exports = function NodeRedController(gladys, nodeRedManager) { /** * @api {get} /api/v1/service/node-red/status Get node-red connection status * @apiName status - * @apiGroup Node-red + * @apiGroup Node-RED */ async function status(req, res) { logger.debug('Get status'); @@ -16,7 +16,7 @@ module.exports = function NodeRedController(gladys, nodeRedManager) { /** * @api {post} /api/v1/service/node-red/connect Connect * @apiName connect - * @apiGroup Node-red + * @apiGroup Node-RED */ async function connect(req, res) { logger.debug('Entering connect step'); @@ -27,12 +27,12 @@ module.exports = function NodeRedController(gladys, nodeRedManager) { } /** - * @api {post} /api/v1/service/node-red/start Install & start Node-red container. + * @api {post} /api/v1/service/node-red/start Install & start Node-RED container. * @apiName installNodeRedContainer - * @apiGroup Node-red + * @apiGroup Node-RED */ async function installNodeRedContainer(req, res) { - logger.debug('Install NodeRed container'); + logger.debug('Install Node-RED container'); await nodeRedManager.installContainer(); res.json({ success: true, @@ -42,7 +42,7 @@ module.exports = function NodeRedController(gladys, nodeRedManager) { /** * @api {post} /api/v1/service/node-red/disconnect Disconnect * @apiName disconnect - * @apiGroup Node-red + * @apiGroup Node-RED */ async function disconnect(req, res) { logger.debug('Entering disconnect step'); diff --git a/server/services/node-red/docker/gladys-node-red-container.json b/server/services/node-red/docker/gladys-node-red-container.json index 0114f458d1..efef54130a 100644 --- a/server/services/node-red/docker/gladys-node-red-container.json +++ b/server/services/node-red/docker/gladys-node-red-container.json @@ -1,6 +1,6 @@ { "name": "gladys-node-red", - "Image": "nodered/node-red:latest", + "Image": "nodered/node-red:3.1", "ExposedPorts": { "1880/tcp": {} }, diff --git a/server/services/node-red/index.js b/server/services/node-red/index.js index bc6027bfc5..f0aca1bf88 100644 --- a/server/services/node-red/index.js +++ b/server/services/node-red/index.js @@ -12,7 +12,7 @@ module.exports = function NodeRedService(gladys, serviceId) { * gladys.services['node-red'].start(); */ async function start() { - logger.log('Starting Node-red service'); + logger.log('Starting Node-RED service'); await nodeRedManager.init(); } @@ -23,7 +23,7 @@ module.exports = function NodeRedService(gladys, serviceId) { * gladys.services['node-red'].stop(); */ function stop() { - logger.log('Stopping Node-red service'); + logger.log('Stopping Node-RED service'); nodeRedManager.disconnect(); } diff --git a/server/services/node-red/lib/checkForContainerUpdates.js b/server/services/node-red/lib/checkForContainerUpdates.js index 103f9db75f..76a5b7c3a5 100644 --- a/server/services/node-red/lib/checkForContainerUpdates.js +++ b/server/services/node-red/lib/checkForContainerUpdates.js @@ -10,11 +10,11 @@ const nodeRedContainerDescriptor = require('../docker/gladys-node-red-container. * await nodeRed.checkForContainerUpdates(config); */ async function checkForContainerUpdates(config) { - logger.info('NodeRed: Checking for current installed versions and required updates...'); + logger.info('Node-RED: Checking for current installed versions and required updates...'); // Check for NodeRed container version if (config.dockerNodeRedVersion !== DEFAULT.DOCKER_NODE_RED_VERSION) { - logger.info(`NodeRed: update #${DEFAULT.DOCKER_NODE_RED_VERSION} of the container required...`); + logger.info(`Node-RED: update #${DEFAULT.DOCKER_NODE_RED_VERSION} of the container required...`); const containers = await this.gladys.system.getContainers({ all: true, @@ -22,7 +22,7 @@ async function checkForContainerUpdates(config) { }); if (containers.length !== 0) { - logger.debug('NodeRed: Removing current installed NodeRed container...'); + logger.debug('Node-RED: Removing current installed Node-RED container...'); // If container is present, we remove it // The init process will create it again const [container] = containers; @@ -31,7 +31,7 @@ async function checkForContainerUpdates(config) { // Update to last version config.dockerNodeRedVersion = DEFAULT.DOCKER_NODE_RED_VERSION; - logger.info(`NodeRed: update #${DEFAULT.DOCKER_NODE_RED_VERSION} of the container done`); + logger.info(`Node-RED: update #${DEFAULT.DOCKER_NODE_RED_VERSION} of the container done`); } } diff --git a/server/services/node-red/lib/configureContainer.js b/server/services/node-red/lib/configureContainer.js index da84f49e05..5a3b1a4f31 100644 --- a/server/services/node-red/lib/configureContainer.js +++ b/server/services/node-red/lib/configureContainer.js @@ -6,14 +6,14 @@ const passwordUtils = require('../../../utils/password'); const { DEFAULT } = require('./constants'); /** - * @description Configure Node-red container. - * @param {object} config - Gladys Node-red stored configuration. + * @description Configure Node-RED container. + * @param {object} config - Gladys Node-RED stored configuration. * @returns {Promise} Indicates if the configuration file has been created or modified. * @example * await this.configureContainer({}); */ async function configureContainer(config) { - logger.info('NodeRed: Docker container is being configured...'); + logger.info('Node-RED: Docker container is being configured...'); const { basePathOnHost } = await this.gladys.system.getGladysBasePath(); @@ -23,7 +23,7 @@ async function configureContainer(config) { try { await fs.chown(path.dirname(configFilepath), 1000, 1000); } catch (e) { - logger.error('NodeRed: Unable to change write of the configuration'); + logger.error('Node-RED: Unable to change write of the configuration'); } // Check if config file not already exists @@ -31,9 +31,9 @@ async function configureContainer(config) { try { // eslint-disable-next-line no-bitwise await fs.access(configFilepath, constants.R_OK | constants.W_OK); - logger.info('NodeRed: configuration file already exists.'); + logger.info('Node-RED: configuration file already exists.'); } catch (e) { - logger.info('NodeRed: Writing default configuration...'); + logger.info('Node-RED: Writing default configuration...'); await fs.copyFile(path.join(__dirname, '../docker/settings.txt'), configFilepath); configCreated = true; } @@ -59,7 +59,7 @@ async function configureContainer(config) { } if (configChanged) { - logger.info('NodeRed: Writting configuration...'); + logger.info('Node-RED: Writting configuration...'); await fs.writeFile(configFilepath, fileContentString); } diff --git a/server/services/node-red/lib/disconnect.js b/server/services/node-red/lib/disconnect.js index e73c33dd5e..e048c401de 100644 --- a/server/services/node-red/lib/disconnect.js +++ b/server/services/node-red/lib/disconnect.js @@ -1,7 +1,10 @@ +const path = require('path'); +const fs = require('fs/promises'); const logger = require('../../../utils/logger'); const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../utils/constants'); const nodeRedContainerDescriptor = require('../docker/gladys-node-red-container.json'); +const { DEFAULT } = require('./constants'); /** * @description Disconnect service from dependent containers. @@ -21,10 +24,16 @@ async function disconnect() { await this.gladys.system.stopContainer(container.id); await this.gladys.system.removeContainer(container.id); + const { basePathOnHost } = await this.gladys.system.getGladysBasePath(); + + const configFilepath = path.join(basePathOnHost, DEFAULT.CONFIGURATION_PATH); + + await fs.rmdir(path.dirname(configFilepath), { recursive: true }); + this.nodeRedRunning = false; this.gladysConnected = false; } catch (e) { - logger.warn(`NodeRed: failed to stop container ${container.id}:`, e); + logger.warn(`Node-RED: failed to stop container ${container.id}:`, e); } this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { diff --git a/server/services/node-red/lib/getConfiguration.js b/server/services/node-red/lib/getConfiguration.js index 451239be1c..9dc96b7e2b 100644 --- a/server/services/node-red/lib/getConfiguration.js +++ b/server/services/node-red/lib/getConfiguration.js @@ -3,13 +3,13 @@ const logger = require('../../../utils/logger'); const { CONFIGURATION } = require('./constants'); /** - * @description Get Node-red configuration. - * @returns {Promise} Current Node-red network configuration. + * @description Get Node-RED configuration. + * @returns {Promise} Current Node-RED network configuration. * @example * const config = await nodeRed.getConfiguration(); */ async function getConfiguration() { - logger.debug('NodeRed: loading stored configuration...'); + logger.debug('Node-RED: loading stored configuration...'); const nodeRedUsername = await this.gladys.variable.getValue(CONFIGURATION.NODE_RED_USERNAME, this.serviceId); const nodeRedPassword = await this.gladys.variable.getValue(CONFIGURATION.NODE_RED_PASSWORD, this.serviceId); diff --git a/server/services/node-red/lib/index.js b/server/services/node-red/lib/index.js index 15dd067481..39aa26ab32 100644 --- a/server/services/node-red/lib/index.js +++ b/server/services/node-red/lib/index.js @@ -9,7 +9,7 @@ const { status } = require('./status'); const { configureContainer } = require('./configureContainer'); /** - * @description Add ability to connect to Node-red. + * @description Add ability to connect to Node-RED. * @param {object} gladys - Gladys instance. * @param {string} serviceId - UUID of the service in DB. * @example diff --git a/server/services/node-red/lib/init.js b/server/services/node-red/lib/init.js index ea3b4aadd5..96bb2b06eb 100644 --- a/server/services/node-red/lib/init.js +++ b/server/services/node-red/lib/init.js @@ -30,7 +30,7 @@ async function init() { // Load stored configuration const configuration = await this.getConfiguration(); - logger.debug('NodeRed: installing and starting required docker containers...'); + logger.debug('Node-RED: installing and starting required docker containers...'); await this.checkForContainerUpdates(configuration); await this.installContainer(configuration); diff --git a/server/services/node-red/lib/installContainer.js b/server/services/node-red/lib/installContainer.js index 0326957dd8..cf137a1129 100644 --- a/server/services/node-red/lib/installContainer.js +++ b/server/services/node-red/lib/installContainer.js @@ -10,7 +10,7 @@ const { PlatformNotCompatible } = require('../../../utils/coreErrors'); const sleep = promisify(setTimeout); /** - * @description Install and starts Node-red container. + * @description Install and starts Node-RED container. * @param {object} config - Service configuration properties. * @example * await nodeRed.installContainer(config); @@ -54,11 +54,11 @@ async function installContainer(config) { const containerLog = await this.gladys.system.createContainer(containerDescriptorToMutate); logger.trace(containerLog); - logger.info('NodeRed: successfully installed and configured as Docker container'); + logger.info('Node-RED: successfully installed and configured as Docker container'); this.nodeRedExist = true; } catch (e) { this.nodeRedExist = false; - logger.error('NodeRed: failed to install as Docker container:', e); + logger.error('Node-RED: failed to install as Docker container:', e); this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); @@ -77,20 +77,20 @@ async function installContainer(config) { // Check if we need to restart the container (container is not running / config changed) if (container.state !== 'running' || configChanged) { - logger.info('NodeRed: container is (re)starting...'); + logger.info('Node-RED: container is (re)starting...'); await this.gladys.system.restartContainer(container.id); // wait a few seconds for the container to restart await sleep(this.containerRestartWaitTimeInMs); } - logger.info('NodeRed: container successfully started'); + logger.info('Node-RED: container successfully started'); this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, }); this.nodeRedRunning = true; this.nodeRedExist = true; } catch (e) { - logger.error('NodeRed: container failed to start:', e); + logger.error('Node-RED: container failed to start:', e); this.nodeRedRunning = false; this.gladys.event.emit(EVENTS.WEBSOCKET.SEND_ALL, { type: WEBSOCKET_MESSAGE_TYPES.NODERED.STATUS_CHANGE, diff --git a/server/services/node-red/lib/isEnabled.js b/server/services/node-red/lib/isEnabled.js index f73784df0b..11f2f12975 100644 --- a/server/services/node-red/lib/isEnabled.js +++ b/server/services/node-red/lib/isEnabled.js @@ -1,6 +1,6 @@ /** - * @description Checks if Node-red is ready to use. - * @returns {boolean} Is the Node-red environment ready to use? + * @description Checks if Node-RED is ready to use. + * @returns {boolean} Is the Node-RED environment ready to use? * @example * await nodeRed.isEnabled(); */ diff --git a/server/services/node-red/lib/saveConfiguration.js b/server/services/node-red/lib/saveConfiguration.js index f5d71cc886..98b9f689d3 100644 --- a/server/services/node-red/lib/saveConfiguration.js +++ b/server/services/node-red/lib/saveConfiguration.js @@ -10,14 +10,14 @@ const saveOrDestroy = async (variableManager, key, value, serviceId) => { }; /** - * @description Save Node-red configuration. - * @param {object} config - Node-red service configuration. - * @returns {Promise} Current Node-red configuration. + * @description Save Node-RED configuration. + * @param {object} config - Node-RED service configuration. + * @returns {Promise} Current Node-RED configuration. * @example * await nodeRed.saveConfiguration(config); */ async function saveConfiguration(config) { - logger.debug('NodeRed: storing configuration...'); + logger.debug('Node-RED: storing configuration...'); const keyValueMap = { [CONFIGURATION.NODE_RED_USERNAME]: config.nodeRedUsername, @@ -32,7 +32,7 @@ async function saveConfiguration(config) { variableKeys.map((key) => saveOrDestroy(this.gladys.variable, key, keyValueMap[key], this.serviceId)), ); - logger.debug('NodeRed: configuration stored'); + logger.debug('Node-RED: configuration stored'); } module.exports = { diff --git a/server/services/node-red/lib/status.js b/server/services/node-red/lib/status.js index 47976aa7be..74d6e6741b 100644 --- a/server/services/node-red/lib/status.js +++ b/server/services/node-red/lib/status.js @@ -1,6 +1,6 @@ /** - * @description Get Node-red status. - * @returns {object} Current Node-red containers and configuration status. + * @description Get Node-RED status. + * @returns {object} Current Node-RED containers and configuration status. * @example * status(); */ diff --git a/server/test/services/node-red/lib/disconnect.test.js b/server/test/services/node-red/lib/disconnect.test.js index 32d125fe7f..0ad137118e 100644 --- a/server/test/services/node-red/lib/disconnect.test.js +++ b/server/test/services/node-red/lib/disconnect.test.js @@ -18,6 +18,8 @@ const mqtt = { removeAllListeners: fake.resolves(true), }; +const TEMP_GLADYS_FOLDER = process.env.TEMP_FOLDER || '../.tmp'; + describe('NodeRed disconnect', () => { let nodeRedManager; let gladys; @@ -31,6 +33,10 @@ describe('NodeRed disconnect', () => { getContainers: fake.resolves([container]), stopContainer: fake.resolves(true), removeContainer: fake.resolves(true), + getGladysBasePath: fake.resolves({ + basePathOnHost: TEMP_GLADYS_FOLDER, + basePathOnContainer: TEMP_GLADYS_FOLDER, + }), }, }; From c63f917f741f64c3c4482ae7b26c60ae68884efd Mon Sep 17 00:00:00 2001 From: callemand Date: Mon, 11 Sep 2023 11:12:05 +0200 Subject: [PATCH 22/32] Fix test --- server/services/node-red/lib/disconnect.js | 2 +- .../test/services/node-red/lib/disconnect.test.js | 13 ++++++++++++- .../services/node-red/lib/installContainer.test.js | 4 ++-- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/server/services/node-red/lib/disconnect.js b/server/services/node-red/lib/disconnect.js index e048c401de..c2f8ddbcf4 100644 --- a/server/services/node-red/lib/disconnect.js +++ b/server/services/node-red/lib/disconnect.js @@ -28,7 +28,7 @@ async function disconnect() { const configFilepath = path.join(basePathOnHost, DEFAULT.CONFIGURATION_PATH); - await fs.rmdir(path.dirname(configFilepath), { recursive: true }); + await fs.rm(path.dirname(configFilepath), { recursive: true }); this.nodeRedRunning = false; this.gladysConnected = false; diff --git a/server/test/services/node-red/lib/disconnect.test.js b/server/test/services/node-red/lib/disconnect.test.js index 0ad137118e..b7d94a46cb 100644 --- a/server/test/services/node-red/lib/disconnect.test.js +++ b/server/test/services/node-red/lib/disconnect.test.js @@ -1,11 +1,22 @@ const { expect } = require('chai'); const sinon = require('sinon'); +const proxiquire = require('proxyquire').noCallThru(); const { assert, fake } = sinon; const { EVENTS, WEBSOCKET_MESSAGE_TYPES } = require('../../../../utils/constants'); -const NodeRedManager = require('../../../../services/node-red/lib'); +const fsMock = { + rm: fake.resolves(true), +}; + +const disconnect = proxiquire('../../../../services/node-red/lib/disconnect', { + 'fs/promises': fsMock, +}); + +const NodeRedManager = proxiquire('../../../../services/node-red/lib', { + './disconnect': disconnect, +}); const container = { id: 'docker-test', diff --git a/server/test/services/node-red/lib/installContainer.test.js b/server/test/services/node-red/lib/installContainer.test.js index c5125a4a11..791879ce6f 100644 --- a/server/test/services/node-red/lib/installContainer.test.js +++ b/server/test/services/node-red/lib/installContainer.test.js @@ -72,7 +72,7 @@ describe('NodeRed installContainer', () => { await nodeRedManager.installContainer(config); - assert.calledWith(gladys.system.pull, 'nodered/node-red:latest'); + assert.calledWith(gladys.system.pull, 'nodered/node-red:3.1'); assert.calledWith(gladys.system.createContainer, { AttachStderr: false, AttachStdin: false, @@ -89,7 +89,7 @@ describe('NodeRed installContainer', () => { PortBindings: { '1880/tcp': [{ HostPort: '1881' }] }, RestartPolicy: { Name: 'always' }, }, - Image: 'nodered/node-red:latest', + Image: 'nodered/node-red:3.1', NetworkDisabled: false, Tty: false, name: 'gladys-node-red', From 18d72092bbb93c6644b450f7566620eb0e60a554 Mon Sep 17 00:00:00 2001 From: callemand Date: Mon, 11 Sep 2023 12:51:03 +0200 Subject: [PATCH 23/32] Fix review --- front/src/routes/integration/all/node-red/NodeRedPage.js | 2 +- .../integration/all/node-red/setup-page/SetupTab.jsx | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/front/src/routes/integration/all/node-red/NodeRedPage.js b/front/src/routes/integration/all/node-red/NodeRedPage.js index 56839bba67..3743472d33 100644 --- a/front/src/routes/integration/all/node-red/NodeRedPage.js +++ b/front/src/routes/integration/all/node-red/NodeRedPage.js @@ -27,7 +27,7 @@ const NodeRedPage = ({ children, user }) => ( diff --git a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx index a447c723fc..6872e2ab17 100644 --- a/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx +++ b/front/src/routes/integration/all/node-red/setup-page/SetupTab.jsx @@ -281,10 +281,7 @@ class SetupTab extends Component { )} {dockerBased && networkModeValid && nodeRedEnabled && showConfirmDelete && ( -
+