diff --git a/app.js b/app.js index 7fadbe1bad7..3de479e9c2e 100644 --- a/app.js +++ b/app.js @@ -89,6 +89,7 @@ socketManager.authMiddleware = function (socket, next) { } let gw // the gateway instance +const plugins = [] // flag used to prevent multiple restarts while one is already in progress let restarting = false @@ -249,9 +250,41 @@ function startGateway (settings) { gw.start() + const pluginsConfig = settings.gateway ? settings.gateway.plugins : null + + // load custom plugins + if (pluginsConfig && Array.isArray(pluginsConfig)) { + for (const plugin of pluginsConfig) { + try { + const pluginName = path.basename(plugin) + const instance = require(plugin)({ + zwave, + mqtt, + app, + logger: loggers.module(pluginName) + }) + instance.name = pluginName + plugins.push(instance) + logger.info(`Successfully loaded plugin ${instance.name}`) + } catch (error) { + logger.error(`Error while loading ${plugin} plugin`, error) + } + } + } + restarting = false } +async function destroyPlugins () { + while (plugins.length > 0) { + const instance = plugins.pop() + if (instance && typeof instance.destroy === 'function') { + logger.info('Closing plugin ' + instance.name) + await instance.destroy() + } + } +} + function setupInterceptor () { // intercept logs and redirect them to socket const interceptor = function (write) { @@ -868,6 +901,7 @@ app.post('/api/settings', apisLimiter, isAuthenticated, async function ( restarting = true await jsonStore.put(store.settings, settings) await gw.close() + await destroyPlugins() // reload loggers settings setupLogging(settings) // restart clients and gateway @@ -911,6 +945,7 @@ async function gracefuShutdown () { logger.warn('Shutdown detected: closing clients...') try { await gw.close() + await destroyPlugins() } catch (error) { logger.error('Error while closing clients', error) } diff --git a/docker/Dockerfile b/docker/Dockerfile index 04bcfc59fa0..07f1c1f1bf4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,6 +14,8 @@ COPY package.json package-lock.json ./ RUN npm config set unsafe-perm true RUN npm install --no-optional +# add plugin support, space separated +ARG plugins # when update devices arg is set update config files from zwavejs repo ARG updateDevices ARG zwavejs=https://github.com/zwave-js/node-zwave-js/archive/master.tar.gz @@ -37,11 +39,14 @@ RUN npm run build && \ static \ stylesheets +RUN if [ ! -z "$plugins" ]; \ + then echo "Installing plugins ${plugins}"; npm i ${plugins} ; fi + # STEP: 2 (runtime) FROM node:15.10.0-alpine RUN apk add --no-cache \ - libstdc++ \ + libstdc++ \ openssl \ libgcc \ libusb \ diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 5956a273995..c3e7c556b3c 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -20,6 +20,7 @@ - [Z2M Migration](guide/migrating.md) - [Env Vars](guide/env-vars.md) - [FAQ](guide/faq.md) + - [Plugins](guide/plugins.md) - Development diff --git a/docs/guide/plugins.md b/docs/guide/plugins.md new file mode 100644 index 00000000000..1e6d561c173 --- /dev/null +++ b/docs/guide/plugins.md @@ -0,0 +1,52 @@ +# Plugins + +Plugins are nodejs packages that can be integrated in zwavejsmqtt in order to add new awesome features. They have access to all the clients (zwave and mqtt) and express instance. + +## Usage + +A plugin is imported in zwavejs1mqtt using `require(pluginName)(context)` where context provides access to these elements: + +- `zwave`: Zwave client +- `mqtt`: Mqtt client +- `app`: Express instance +- `logger`: A logger instance to log things in console/file based on logger general settings + +In order to add a plugin you have to specify the absolute/relative path to it or, if it is available as an npm package, you can install it using the command: + +```bash +npm i my-awesome-plugin +``` + +## Plugins with docker + +Building the container is straight forward. Here an example of build command installing plugin `my-awesome-plugin` + +```bash +docker build -f docker/Dockerfile --build-arg plugins='my-awesome-plugin' -t : . +``` + +## Developing custom Plugins + +In order to implement a plugin you need to create a class with a constructor that accepts a single parameter that is the context we spoke in [usage](#usage) section and a `destroy` function that will be called when application is closed or settings updated. + +Here is a minimal example of a custom plugin: + +```js +function MyPlugin (ctx) { + this.zwave = ctx.zwave + this.mqtt = ctx.mqtt + this.logger = ctx.logger + this.express = ctx.app + + // this.express.get('/my-route', function(req, res) {...}) + // this.mqtt.publish(...) + // this.zwave.on('valueChanged', onValueChanged) + // ... add all the stuff you need here +} + +MyPlugin.prototype.destroy = async function () { + // clean up the state +} + +module.export = MyPlugin +``` diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index dbb0feaa70b..79ef1564a1c 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -132,15 +132,6 @@ function ZwaveClient (config, socket) { this.healTimeout = null this.status = ZWAVE_STATUS.closed - - // load custom plugins - if (config.plugin) { - try { - require(config.plugin)(this) - } catch (error) { - logger.error(`Error while loading ${config.plugin} plugin`, error.message) - } - } } inherits(ZwaveClient, EventEmitter) diff --git a/src/components/Settings.vue b/src/components/Settings.vue index e16223dc08b..c555c435d1f 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -25,6 +25,18 @@ v-model="newGateway.authEnabled" > + + +