├── .babelrc ├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question---help.md ├── dependabot.yml └── workflows │ ├── automerge.yml │ ├── ci.yml │ ├── close-stale.yml │ └── dockerbuild.yml ├── .gitignore ├── .markdownlint.json ├── .markdownlintignore ├── .mocharc.yml ├── .postcssrc.js ├── .prettierignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── app.js ├── bin └── www ├── build ├── build.js ├── check-versions.js ├── logo.png ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── app.js ├── dev.env.js ├── index.js ├── prod.env.js ├── store.js └── webConfig.js ├── docker ├── Dockerfile ├── README.md └── docker-compose.yml ├── docs ├── MQTT-Logo.png ├── OZW_Logo.png ├── OZW_Panel_Node.png ├── debug.png ├── groups_associations.png ├── hass_devices.png ├── mesh.png ├── mesh_diagram.png ├── scenes.png ├── settings.png └── subpath.md ├── hass ├── configurations.js └── devices.js ├── kubernetes ├── deployment.yaml ├── ingress.yaml ├── namespace.yaml └── service.yaml ├── kustomization.yaml ├── lib ├── Constants.js ├── Gateway.js ├── MqttClient.js ├── ZwaveClient.js ├── debug.js ├── jsonStore.js ├── renderIndex.js └── utils.js ├── package-lock.json ├── package.json ├── package.sh ├── pkg └── .gitkeep ├── public └── stylesheets │ └── style.css ├── src ├── App.vue ├── apis │ └── ConfigApis.js ├── assets │ ├── css │ │ ├── my-mesh.css │ │ └── my-progress.css │ └── logo.png ├── components │ ├── Confirm.vue │ ├── ControlPanel.vue │ ├── Mesh.vue │ ├── Settings.vue │ ├── ValueId.vue │ ├── custom │ │ └── file-input.vue │ ├── dialogs │ │ ├── DialogGatewayValue.vue │ │ └── DialogSceneValue.vue │ └── nodes-table │ │ ├── filter-options.vue │ │ ├── index.vue │ │ ├── nodes-table.css │ │ └── nodes-table.js ├── main.js ├── modules │ ├── NodeCollection.js │ ├── NodeCollection.test.js │ ├── Settings.js │ └── Settings.test.js ├── plugins │ └── vuetify.js ├── router │ └── index.js └── store │ ├── index.js │ └── mutations.js ├── static ├── .gitkeep ├── favicon.ico ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest └── logo.png ├── store └── .gitkeep ├── test ├── config │ └── webConfig.test.js └── lib │ ├── Constants.test.js │ ├── Gateway.test.js │ ├── debug.test.js │ ├── jsonStore.test.js │ ├── renderIndex.test.js │ └── utils.test.js ├── views ├── error.ejs └── index.ejs └── wallaby.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false, 7 | "targets": { 8 | "browsers": [ 9 | "> 1%", 10 | "last 2 versions", 11 | "not ie <= 8" 12 | ] 13 | } 14 | } 15 | ] 16 | ], 17 | "plugins": [ 18 | "transform-vue-jsx", 19 | "@babel/plugin-transform-runtime", 20 | "@babel/plugin-syntax-dynamic-import", 21 | "@babel/plugin-syntax-import-meta", 22 | "@babel/plugin-proposal-class-properties", 23 | "@babel/plugin-proposal-json-strings", 24 | [ 25 | "@babel/plugin-proposal-decorators", 26 | { 27 | "legacy": true 28 | } 29 | ], 30 | "@babel/plugin-proposal-function-sent", 31 | "@babel/plugin-proposal-export-namespace-from", 32 | "@babel/plugin-proposal-numeric-separator", 33 | "@babel/plugin-proposal-throw-expressions" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | *.md 4 | docs 5 | .vscode 6 | .nyc_output 7 | .coverage 8 | pkg 9 | test 10 | node_modules -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | /config/ 3 | /dist/ 4 | /*.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // https://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | env: { 6 | browser: true, 7 | mocha: true, 8 | node: true 9 | }, 10 | extends: [ 11 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 12 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 13 | 'plugin:vue/essential', 14 | // https://github.com/standard/standard/blob/master/docs/RULES-en.md 15 | 'standard' 16 | ], 17 | // required to lint *.vue files 18 | plugins: ['vue', 'babel'], 19 | // add your custom rules here 20 | rules: { 21 | // allow async-await 22 | 'generator-star-spacing': 'off', 23 | // allow debugger during development 24 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 25 | 'no-unused-vars': ['error', { vars: 'local' }] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: robertsLando 4 | patreon: user?u=16906849 5 | custom: paypal.me/daniellando 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help to improve the project 4 | title: "[bug]" 5 | labels: bug 6 | assignees: robertsLando 7 | 8 | --- 9 | 10 | **Version** 11 | 12 | Build/Run method 13 | - [ ] Docker 14 | - [ ] PKG 15 | - [ ] Manually built (git clone - npm install - npm run build ) 16 | 17 | Zwave2Mqtt version: 1.2.3 18 | Openzwave Version: 1.4.1 19 | 20 | **Describe the bug** 21 | A clear and concise description of what the bug is. 22 | 23 | **To Reproduce** 24 | Steps to reproduce the behavior: 25 | 1. Go to '...' 26 | 2. Click on '....' 27 | 3. Scroll down to '....' 28 | 4. See error 29 | 30 | **Expected behavior** 31 | A clear and concise description of what you expected to happen. 32 | 33 | **Additional context** 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feat]" 5 | labels: enhancement 6 | assignees: robertsLando 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question---help.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question / Help 3 | about: Ask for support 4 | title: "[question] " 5 | labels: question 6 | assignees: robertsLando 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | allow: 8 | - dependency-type: 'production' 9 | labels: 10 | - 'prod-dependencies' 11 | 12 | - package-ecosystem: 'npm' 13 | directory: '/' 14 | schedule: 15 | interval: 'daily' 16 | target-branch: master 17 | allow: 18 | - dependency-type: 'development' 19 | labels: 20 | - 'dev-dependencies' 21 | - 'automerge' 22 | 23 | - package-ecosystem: 'docker' # See documentation for possible values 24 | directory: '/docker' 25 | schedule: 26 | interval: 'daily' 27 | labels: 28 | - 'automerge' 29 | 30 | - package-ecosystem: 'github-actions' 31 | directory: '/' 32 | schedule: 33 | interval: 'daily' 34 | -------------------------------------------------------------------------------- /.github/workflows/automerge.yml: -------------------------------------------------------------------------------- 1 | name: automerge 2 | on: 3 | pull_request: 4 | types: 5 | - labeled 6 | - unlabeled 7 | - synchronize 8 | - opened 9 | - edited 10 | - ready_for_review 11 | - reopened 12 | - unlocked 13 | pull_request_review: 14 | types: 15 | - submitted 16 | check_suite: 17 | types: 18 | - completed 19 | status: {} 20 | jobs: 21 | automerge: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - name: automerge 25 | uses: 'pascalgn/automerge-action@v0.12.0' 26 | env: 27 | GITHUB_TOKEN: '${{ secrets.GITHUB_TOKEN }}' 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [10.x, 12.x, 14.x] 16 | 17 | steps: 18 | - run: sudo apt-get update 19 | - name: Install Deps 20 | run: sudo apt-get install libudev-dev 21 | 22 | - name: Checkout Openzwave 23 | uses: actions/checkout@v2 24 | with: 25 | repository: OpenZWave/open-zwave 26 | path: open-zwave 27 | 28 | - name: Cache openzwave 29 | uses: actions/cache@v2.1.2 30 | with: 31 | path: open-zwave 32 | key: ${{ hashFiles('open-zwave/.git/refs/heads/master') }} 33 | 34 | - name: Install Openzwave 35 | run: | 36 | mv open-zwave /tmp 37 | cd /tmp/open-zwave 38 | sudo make install 39 | sudo ldconfig /usr/local/lib /usr/local/lib64 40 | 41 | - name: Checkout Zwave2Mqtt 42 | uses: actions/checkout@v2 43 | 44 | - name: Use Node.js ${{ matrix.node-version }} 45 | uses: actions/setup-node@v2.1.2 46 | with: 47 | node-version: ${{ matrix.node-version }} 48 | 49 | - name: Cache node modules 50 | id: cache-node-modules 51 | uses: actions/cache@v2.1.2 52 | with: 53 | path: node_modules 54 | key: ${{ matrix['node-version'] }}-npm-${{ hashFiles('package-lock.json') }} 55 | 56 | - name: NPM Install 57 | if: steps.cache-node-modules.outputs.cache-hit != 'true' 58 | run: npm install 59 | 60 | - name: NPM Lint 61 | if: matrix['node-version'] == '12.x' 62 | run: npm run lint 63 | 64 | - name: Cache build 65 | id: cache-build 66 | uses: actions/cache@v2.1.2 67 | with: 68 | path: dist 69 | key: ${{ matrix['node-version'] }}-cache-build-${{ hashFiles('build/*') }}-${{ hashFiles('src/*') }}-${{ hashFiles('static/*') }}-${{ hashFiles('package-lock.json') }} 70 | 71 | - name: Build 72 | if: steps.cache-build.outputs.cache-hit != 'true' 73 | run: npm run build 74 | 75 | - name: Test 76 | run: npm run test 77 | 78 | - name: Generate coverage report 79 | if: matrix['node-version'] == '12.x' 80 | run: | 81 | npm run coverage 82 | npm run record-coverage 83 | 84 | - name: Coveralls 85 | uses: coverallsapp/github-action@master 86 | if: matrix['node-version'] == '12.x' 87 | with: 88 | github-token: ${{ secrets.github_token }} 89 | 90 | - name: move ozw back for caching 91 | run: | 92 | mv /tmp/open-zwave ./ 93 | -------------------------------------------------------------------------------- /.github/workflows/close-stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues' 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v3 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Remove the stale label or comment or this will be closed in 5 days. To ignore this issue entirely you can add the no-stale label' 14 | close-issue-message: 'This issue is now closed due to inactivity, you can of course reopen or reference this issue if you see fit.' 15 | stale-pr-message: 'This pull-request is stale because it has been open 90 days with no activity. Remove the stale label or comment or this will be closed in 5 days. To ignore this pull-request entirely you can add the no-stale label' 16 | close-pr-message: 'This pull-request is now closed due to inactivity, you can of course reopen or reference this pull-request if you see fit.' 17 | days-before-stale: 90 18 | days-before-close: 5 19 | exempt-issue-labels: 'no-stale,enhancement' 20 | exempt-pr-labels: 'no-stale' 21 | -------------------------------------------------------------------------------- /.github/workflows/dockerbuild.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | release: 9 | types: 10 | - created 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | 18 | - uses: docker/setup-buildx-action@v1 19 | - uses: docker/setup-qemu-action@v1 20 | - name: Login to dockerhub 21 | if: ${{ github.event_name != 'pull_request' }} 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKER_USERNAME }} 25 | password: ${{ secrets.DOCKER_PASSWORD }} 26 | 27 | - name: Prepare 28 | id: prepare 29 | run: | 30 | DOCKER_REPO=robertslando/zwave2mqtt 31 | TAGS=${DOCKER_REPO}:sha-${GITHUB_SHA} 32 | 33 | if [ "$GITHUB_REF" == "refs/heads/master" ]; then 34 | TAGS=$TAGS,${DOCKER_REPO}:dev 35 | fi 36 | if [ "$GITHUB_EVENT_NAME" == "release" ]; then 37 | TAGS=$TAGS,${DOCKER_REPO}:latest 38 | TAGS=$TAGS,${DOCKER_REPO}:$(echo ${GITHUB_REF} | sed "s/refs\/tags\/v//") >> ./TAGS 39 | fi 40 | 41 | echo ::set-output name=TAGS::${TAGS} 42 | echo DOCKER_REPO="${DOCKER_REPO}" >> $GITHUB_ENV 43 | 44 | - name: build+push 45 | uses: docker/build-push-action@v2 46 | with: 47 | cache-from: type=registry,ref=${{ env.DOCKER_REPO }}:sha-${{ env.GITHUB_SHA }} 48 | platforms: linux/arm64,linux/amd64,linux/arm/v6,linux/arm/v7 49 | file: docker/Dockerfile 50 | push: ${{ github.event_name != 'pull_request' }} 51 | tags: ${{ steps.prepare.outputs.TAGS }} 52 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | .DS_Store 8 | 9 | # Distribution path 10 | /dist/ 11 | /store/ 12 | /pkg/ 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # Optional npm cache directory 49 | .npm 50 | 51 | # Optional eslint cache 52 | .eslintcache 53 | 54 | # Optional REPL history 55 | .node_repl_history 56 | 57 | # Output of 'npm pack' 58 | *.tgz 59 | 60 | # Yarn Integrity file 61 | .yarn-integrity 62 | 63 | # dotenv environment variables file 64 | .env 65 | 66 | # next.js build output 67 | .next 68 | 69 | # Editor directories and files 70 | .idea 71 | .vscode 72 | *.suo 73 | *.ntvs* 74 | *.njsproj 75 | *.sln 76 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD013": { 4 | "line_length": -1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.markdownlintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | CHANGELOG.md -------------------------------------------------------------------------------- /.mocharc.yml: -------------------------------------------------------------------------------- 1 | recursive: true 2 | watch-files: 3 | - 'lib/**/*.js' 4 | - 'test/**/*.js' 5 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: { 5 | 'postcss-import': {}, 6 | 'postcss-url': {}, 7 | // to edit target browsers: use "browserslist" field in package.json 8 | autoprefixer: {} 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | CHANGELOG.md 2 | .github/ISSUE_TEMPLATE 3 | dist -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "javascript-test-runner.additionalArgs": "node_modules/.bin/mocha", 4 | "javascript-test-runner.envVars": {}, 5 | "eslint.format.enable": true, 6 | "editor.defaultFormatter": "numso.prettier-standard-vscode", 7 | "eslint.lintTask.enable": true, 8 | "wallaby.startAutomatically": true, 9 | "[vue]": { 10 | "editor.defaultFormatter": "dbaeumer.vscode-eslint" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Daniel Lando 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please email daniel.sorridi+zwave2mqtt@gmail.com; chris+zwave2mqtt@cns.me.uk 6 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express') 2 | var reqlib = require('app-root-path').require 3 | var logger = require('morgan') 4 | var cookieParser = require('cookie-parser') 5 | var bodyParser = require('body-parser') 6 | var app = express() 7 | var SerialPort = require('serialport') 8 | var jsonStore = reqlib('/lib/jsonStore.js') 9 | var cors = require('cors') 10 | var ZWaveClient = reqlib('/lib/ZwaveClient') 11 | var MqttClient = reqlib('/lib/MqttClient') 12 | var Gateway = reqlib('/lib/Gateway') 13 | var store = reqlib('config/store.js') 14 | var debug = reqlib('/lib/debug')('App') 15 | var history = require('connect-history-api-fallback') 16 | var utils = reqlib('/lib/utils.js') 17 | const renderIndex = reqlib('/lib/renderIndex') 18 | var gw //the gateway instance 19 | let io 20 | 21 | debug('Zwave2Mqtt version: ' + require('./package.json').version) 22 | debug('Application path:' + utils.getPath(true)) 23 | 24 | // view engine setup 25 | app.set('views', utils.joinPath(false, 'views')) 26 | app.set('view engine', 'ejs') 27 | 28 | app.use(logger('dev')) 29 | app.use(bodyParser.json({ limit: '50mb' })) 30 | app.use( 31 | bodyParser.urlencoded({ 32 | limit: '50mb', 33 | extended: true, 34 | parameterLimit: 50000 35 | }) 36 | ) 37 | app.use(cookieParser()) 38 | 39 | app.get('/', renderIndex) 40 | 41 | app.use('/', express.static(utils.joinPath(false, 'dist'))) 42 | 43 | app.use(cors()) 44 | 45 | app.use(history()) 46 | 47 | function startGateway () { 48 | var settings = jsonStore.get(store.settings) 49 | 50 | var mqtt, zwave 51 | 52 | if (settings.mqtt) { 53 | mqtt = new MqttClient(settings.mqtt) 54 | } 55 | 56 | if (settings.zwave) { 57 | zwave = new ZWaveClient(settings.zwave, io) 58 | } 59 | 60 | gw = new Gateway(settings.gateway, zwave, mqtt) 61 | } 62 | 63 | app.startSocket = function (server) { 64 | io = require('socket.io')(server) 65 | 66 | if (gw.zwave) gw.zwave.socket = io 67 | 68 | io.on('connection', function (socket) { 69 | debug('New connection', socket.id) 70 | 71 | socket.on('INITED', function () { 72 | if (gw.zwave) 73 | socket.emit('INIT', { 74 | nodes: gw.zwave.nodes, 75 | info: gw.zwave.ozwConfig, 76 | error: gw.zwave.error, 77 | cntStatus: gw.zwave.cntStatus 78 | }) 79 | }) 80 | 81 | socket.on('ZWAVE_API', async function (data) { 82 | debug('Zwave api call:', data.api, data.args) 83 | if (gw.zwave) { 84 | var result = await gw.zwave.callApi(data.api, ...data.args) 85 | result.api = data.api 86 | socket.emit('API_RETURN', result) 87 | } 88 | }) 89 | 90 | socket.on('HASS_API', async function (data) { 91 | switch (data.apiName) { 92 | case 'delete': 93 | gw.publishDiscovery(data.device, data.node_id, true, true) 94 | break 95 | case 'discover': 96 | gw.publishDiscovery(data.device, data.node_id, false, true) 97 | break 98 | case 'rediscoverNode': 99 | gw.rediscoverNode(data.node_id) 100 | break 101 | case 'disableDiscovery': 102 | gw.disableDiscovery(data.node_id) 103 | break 104 | case 'update': 105 | gw.zwave.updateDevice(data.device, data.node_id) 106 | break 107 | case 'add': 108 | gw.zwave.addDevice(data.device, data.node_id) 109 | break 110 | case 'store': 111 | await gw.zwave.storeDevices(data.devices, data.node_id, data.remove) 112 | break 113 | } 114 | }) 115 | 116 | socket.on('disconnect', function () { 117 | debug('User disconnected', socket.id) 118 | }) 119 | }) 120 | 121 | const interceptor = function (write) { 122 | return function (...args) { 123 | io.emit('DEBUG', args[0].toString()) 124 | write.apply(process.stdout, args) 125 | } 126 | } 127 | 128 | process.stdout.write = interceptor(process.stdout.write) 129 | process.stderr.write = interceptor(process.stderr.write) 130 | } 131 | 132 | // ----- APIs ------ 133 | 134 | app.get('/health', async function (req, res) { 135 | var mqtt = false 136 | var zwave = false 137 | 138 | if (gw) { 139 | mqtt = gw.mqtt ? gw.mqtt.getStatus().status : false 140 | zwave = gw.zwave ? gw.zwave.getStatus().status : false 141 | } 142 | 143 | var status = mqtt && zwave 144 | 145 | res.status(status ? 200 : 500).send(status ? 'Ok' : 'Error') 146 | }) 147 | 148 | app.get('/health/:client', async function (req, res) { 149 | var client = req.params.client 150 | 151 | if (client !== 'zwave' && client !== 'mqtt') 152 | res.status(500).send("Requested client doesn 't exist") 153 | else { 154 | status = gw && gw[client] ? gw[client].getStatus().status : false 155 | } 156 | 157 | res.status(status ? 200 : 500).send(status ? 'Ok' : 'Error') 158 | }) 159 | 160 | //get settings 161 | app.get('/api/settings', async function (req, res) { 162 | var data = { 163 | success: true, 164 | settings: jsonStore.get(store.settings), 165 | devices: gw.zwave ? gw.zwave.devices : {}, 166 | serial_ports: [] 167 | } 168 | if (process.platform !== 'sunos') { 169 | try { 170 | var ports = await SerialPort.list() 171 | } catch (error) { 172 | debug(error) 173 | } 174 | 175 | data.serial_ports = ports ? ports.map(p => p.path) : [] 176 | res.json(data) 177 | } else res.json(data) 178 | }) 179 | 180 | //get config 181 | app.get('/api/exportConfig', function (req, res) { 182 | return res.json({ 183 | success: true, 184 | data: jsonStore.get(store.nodes), 185 | message: 'Successfully exported nodes JSON configuration' 186 | }) 187 | }) 188 | 189 | //import config 190 | app.post('/api/importConfig', async function (req, res) { 191 | var config = req.body.data 192 | try { 193 | if (!gw.zwave) throw Error('Zwave client not inited') 194 | 195 | if (!Array.isArray(config)) throw Error('Configuration not valid') 196 | else { 197 | for (let i = 0; i < config.length; i++) { 198 | const e = config[i] 199 | if (e && (!e.hasOwnProperty('name') || !e.hasOwnProperty('loc'))) { 200 | throw Error('Configuration not valid') 201 | } else if (e) { 202 | await gw.zwave.callApi('_setNodeName', i, e.name || '') 203 | await gw.zwave.callApi('_setNodeLocation', i, e.loc || '') 204 | if (e.hassDevices) 205 | await gw.zwave.storeDevices(e.hassDevices, i, false) 206 | } 207 | } 208 | } 209 | 210 | res.json({ success: true, message: 'Configuration imported successfully' }) 211 | } catch (error) { 212 | debug(error.message) 213 | return res.json({ success: false, message: error.message }) 214 | } 215 | }) 216 | 217 | //update settings 218 | app.post('/api/settings', function (req, res) { 219 | jsonStore 220 | .put(store.settings, req.body) 221 | .then(data => { 222 | res.json({ success: true, message: 'Configuration updated successfully' }) 223 | return gw.close() 224 | }) 225 | .then(() => startGateway()) 226 | .catch(err => { 227 | debug(err) 228 | res.json({ success: false, message: err.message }) 229 | }) 230 | }) 231 | 232 | // catch 404 and forward to error handler 233 | app.use(function (req, res, next) { 234 | var err = new Error('Not Found') 235 | err.status = 404 236 | next(err) 237 | }) 238 | 239 | // error handler 240 | app.use(function (err, req, res, next) { 241 | // set locals, only providing error in development 242 | res.locals.message = err.message 243 | res.locals.error = req.app.get('env') === 'development' ? err : {} 244 | 245 | console.log( 246 | '%s %s %d - Error: %s', 247 | req.method, 248 | req.url, 249 | err.status, 250 | err.message 251 | ) 252 | 253 | // render the error page 254 | res.status(err.status || 500) 255 | res.redirect('/') 256 | }) 257 | 258 | startGateway() 259 | 260 | process.removeAllListeners('SIGINT') 261 | 262 | process.on('SIGINT', function () { 263 | debug('Closing...') 264 | gw.close() 265 | process.exit() 266 | }) 267 | 268 | module.exports = app 269 | -------------------------------------------------------------------------------- /bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | var reqlib = require('app-root-path').require 7 | var jsonStore = reqlib('/lib/jsonStore.js') 8 | var store = reqlib('/config/store.js') 9 | var debug = reqlib('/lib/debug')('App') 10 | var conf = reqlib('/config/app.js') 11 | var utils = reqlib('/lib/utils.js') 12 | var Promise = require('bluebird') 13 | 14 | // jsonstore is a singleton instance that handles the json configuration files 15 | // used in the application. Init it before anything else than start app. 16 | // if jsonstore fails exit the application 17 | jsonStore.init(store) 18 | .then(() => { 19 | var fs = require('fs') 20 | var getFiles = (dir) => new Promise((resolve, reject) => fs.readdir(dir, (err, files) => err ? reject(err) : resolve(files))) 21 | return getFiles(utils.getPath(true)) 22 | }) 23 | .then((files) => { 24 | var path = require('path') 25 | var fs = require('fs') 26 | var moveIfExists = (path, newPath) => new Promise((resolve, reject) => fs.rename(path, newPath, (err) => (!err || err.code === 'ENOENT') ? resolve() : reject(err))) 27 | files = files.filter(f => (path.basename(f) === 'OZW_Log.txt' || path.basename(f).endsWith('.xml'))) 28 | return Promise.map(files, function (f) { 29 | return moveIfExists(f, utils.joinPath(path.dirname(f), 'store', path.basename(f))) 30 | }) 31 | }) 32 | .then(() => { 33 | var app = reqlib('app.js') 34 | var http = require('http') 35 | 36 | /** 37 | * Normalize a port into a number, string, or false. 38 | */ 39 | 40 | function normalizePort (val) { 41 | var port = parseInt(val, 10) 42 | 43 | if (isNaN(port)) { 44 | // named pipe 45 | return val 46 | } 47 | 48 | if (port >= 0) { 49 | // port number 50 | return port 51 | } 52 | 53 | return false 54 | } 55 | 56 | /** 57 | * Event listener for HTTP server "error" event. 58 | */ 59 | 60 | function onError (error) { 61 | if (error.syscall !== 'listen') { 62 | throw error 63 | } 64 | 65 | var bind = typeof port === 'string' 66 | ? 'Pipe ' + port 67 | : 'Port ' + port 68 | 69 | // handle specific listen errors with friendly messages 70 | switch (error.code) { 71 | case 'EACCES': 72 | console.error(bind + ' requires elevated privileges') 73 | process.exit(1) 74 | case 'EADDRINUSE': 75 | console.error(bind + ' is already in use') 76 | process.exit(1) 77 | default: 78 | throw error 79 | } 80 | } 81 | 82 | /** 83 | * Event listener for HTTP server "listening" event. 84 | */ 85 | 86 | function onListening () { 87 | var addr = server.address() 88 | var bind = typeof addr === 'string' 89 | ? 'pipe ' + addr 90 | : 'port ' + addr.port 91 | debug('Listening on', bind) 92 | } 93 | 94 | /** 95 | * Get port from environment and store in Express. 96 | */ 97 | 98 | var port = normalizePort(process.env.PORT || conf.port) 99 | app.set('port', port) 100 | 101 | /** 102 | * Create HTTP server. 103 | */ 104 | 105 | var server = http.createServer(app) 106 | 107 | /** 108 | * Listen on provided port, on preferred network interfaces. 109 | */ 110 | 111 | var host = process.env.HOST || conf.host || '0.0.0.0' 112 | 113 | server.listen(port, host) 114 | server.on('error', onError) 115 | server.on('listening', onListening) 116 | 117 | app.startSocket(server) 118 | }) 119 | .catch(err => { 120 | console.error(err) 121 | }) 122 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | const ora = require('ora') 7 | const rm = require('rimraf') 8 | const path = require('path') 9 | const chalk = require('chalk') 10 | const webpack = require('webpack') 11 | const config = require('../config') 12 | const webpackConfig = require('./webpack.prod.conf') 13 | 14 | const spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { 18 | if (err) throw err 19 | webpack(webpackConfig, (err, stats) => { 20 | spinner.stop() 21 | if (err) throw err 22 | process.stdout.write( 23 | stats.toString({ 24 | colors: true, 25 | modules: false, 26 | children: false, // If you are using ts-loader, setting this to true will make TypeScript errors show up during build. 27 | chunks: false, 28 | chunkModules: false 29 | }) + '\n\n' 30 | ) 31 | 32 | if (stats.hasErrors()) { 33 | console.log(chalk.red(' Build failed with errors.\n')) 34 | process.exit(1) 35 | } 36 | 37 | console.log(chalk.cyan(' Build complete.\n')) 38 | console.log( 39 | chalk.yellow( 40 | ' Tip: built files are meant to be served over an HTTP server.\n' + 41 | " Opening index.html over file:// won't work.\n" 42 | ) 43 | ) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const chalk = require('chalk') 3 | const semver = require('semver') 4 | const packageConfig = require('../package.json') 5 | const shell = require('shelljs') 6 | 7 | function exec (cmd) { 8 | return require('child_process') 9 | .execSync(cmd) 10 | .toString() 11 | .trim() 12 | } 13 | 14 | const versionRequirements = [ 15 | { 16 | name: 'node', 17 | currentVersion: semver.clean(process.version), 18 | versionRequirement: packageConfig.engines.node 19 | } 20 | ] 21 | 22 | if (shell.which('npm')) { 23 | versionRequirements.push({ 24 | name: 'npm', 25 | currentVersion: exec('npm --version'), 26 | versionRequirement: packageConfig.engines.npm 27 | }) 28 | } 29 | 30 | module.exports = function () { 31 | const warnings = [] 32 | 33 | for (let i = 0; i < versionRequirements.length; i++) { 34 | const mod = versionRequirements[i] 35 | 36 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 37 | warnings.push( 38 | mod.name + 39 | ': ' + 40 | chalk.red(mod.currentVersion) + 41 | ' should be ' + 42 | chalk.green(mod.versionRequirement) 43 | ) 44 | } 45 | } 46 | 47 | if (warnings.length) { 48 | console.log('') 49 | console.log( 50 | chalk.yellow( 51 | 'To use this template, you must update following to modules:' 52 | ) 53 | ) 54 | console.log() 55 | 56 | for (let i = 0; i < warnings.length; i++) { 57 | const warning = warnings[i] 58 | console.log(' ' + warning) 59 | } 60 | 61 | console.log() 62 | process.exit(1) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /build/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/build/logo.png -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const config = require('../config') 4 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 5 | const packageConfig = require('../package.json') 6 | 7 | exports.assetsPath = function (_path) { 8 | const assetsSubDirectory = 9 | process.env.NODE_ENV === 'production' 10 | ? config.build.assetsSubDirectory 11 | : config.dev.assetsSubDirectory 12 | 13 | return path.posix.join(assetsSubDirectory, _path) 14 | } 15 | 16 | exports.cssLoaders = function (options) { 17 | options = options || {} 18 | 19 | const cssLoader = { 20 | loader: 'css-loader', 21 | options: { 22 | sourceMap: options.sourceMap, 23 | esModule: false 24 | } 25 | } 26 | 27 | const postcssLoader = { 28 | loader: 'postcss-loader', 29 | options: { 30 | sourceMap: options.sourceMap 31 | } 32 | } 33 | 34 | // generate loader string to be used with extract text plugin 35 | function generateLoaders (loader, loaderOptions) { 36 | const loaders = options.usePostCSS 37 | ? [cssLoader, postcssLoader] 38 | : [cssLoader] 39 | 40 | if (loader) { 41 | loaders.push({ 42 | loader: loader + '-loader', 43 | options: Object.assign({}, loaderOptions, { 44 | sourceMap: options.sourceMap 45 | }) 46 | }) 47 | } 48 | 49 | // Extract CSS when that option is specified 50 | // (which is the case during production build) 51 | if (options.extract) { 52 | return [ 53 | { 54 | loader: MiniCssExtractPlugin.loader 55 | }, 56 | 'css-loader' 57 | ] 58 | } else { 59 | return ['vue-style-loader'].concat(loaders) 60 | } 61 | } 62 | 63 | // https://vue-loader.vuejs.org/en/configurations/extract-css.html 64 | return { 65 | css: generateLoaders(), 66 | postcss: generateLoaders(), 67 | less: generateLoaders('less'), 68 | sass: generateLoaders('sass', { indentedSyntax: true }), 69 | scss: generateLoaders('sass'), 70 | stylus: generateLoaders('stylus'), 71 | styl: generateLoaders('stylus') 72 | } 73 | } 74 | 75 | // Generate loaders for standalone style files (outside of .vue) 76 | exports.styleLoaders = function (options) { 77 | const output = [] 78 | const loaders = exports.cssLoaders(options) 79 | 80 | for (const extension in loaders) { 81 | const loader = loaders[extension] 82 | output.push({ 83 | test: new RegExp('\\.' + extension + '$'), 84 | use: loader 85 | }) 86 | } 87 | 88 | return output 89 | } 90 | 91 | exports.createNotifierCallback = () => { 92 | const notifier = require('node-notifier') 93 | 94 | return (severity, errors) => { 95 | if (severity !== 'error') return 96 | 97 | const error = errors[0] 98 | const filename = error.file && error.file.split('!').pop() 99 | 100 | notifier.notify({ 101 | title: packageConfig.name, 102 | message: severity + ': ' + error.name, 103 | subtitle: filename || '', 104 | icon: path.join(__dirname, 'logo.png') 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const config = require('../config') 4 | const isProduction = process.env.NODE_ENV === 'production' 5 | const sourceMapEnabled = isProduction 6 | ? config.build.productionSourceMap 7 | : config.dev.cssSourceMap 8 | 9 | module.exports = { 10 | loaders: utils.cssLoaders({ 11 | sourceMap: sourceMapEnabled, 12 | extract: isProduction 13 | }), 14 | cssSourceMap: sourceMapEnabled, 15 | cacheBusting: config.dev.cacheBusting, 16 | transformToRequire: { 17 | video: ['src', 'poster'], 18 | source: 'src', 19 | img: 'src', 20 | image: 'xlink:href' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const config = require('../config') 5 | const vueLoaderConfig = require('./vue-loader.conf') 6 | const VueLoaderPlugin = require('vue-loader/lib/plugin') 7 | 8 | function resolve (dir) { 9 | return path.join(__dirname, '..', dir) 10 | } 11 | 12 | const createLintingRule = () => ({ 13 | test: /\.(js|vue)$/, 14 | loader: 'eslint-loader', 15 | enforce: 'pre', 16 | include: [resolve('src'), resolve('test')], 17 | options: { 18 | formatter: require('eslint-friendly-formatter'), 19 | emitWarning: !config.dev.showEslintErrorsInOverlay 20 | } 21 | }) 22 | 23 | module.exports = { 24 | context: path.resolve(__dirname, '../'), 25 | entry: { 26 | app: './src/main.js' 27 | }, 28 | plugins: [new VueLoaderPlugin()], 29 | output: { 30 | path: config.build.assetsRoot, 31 | filename: '[name].js', 32 | publicPath: 33 | process.env.NODE_ENV === 'production' 34 | ? config.build.assetsPublicPath 35 | : config.dev.assetsPublicPath 36 | }, 37 | resolve: { 38 | extensions: ['.js', '.vue', '.json'], 39 | alias: { 40 | vue$: 'vue/dist/vue.esm.js', 41 | '@': resolve('src') 42 | } 43 | }, 44 | module: { 45 | rules: [ 46 | ...(config.dev.useEslint ? [createLintingRule()] : []), 47 | { 48 | test: /\.vue$/, 49 | loader: 'vue-loader', 50 | options: vueLoaderConfig 51 | }, 52 | { 53 | test: /\.js$/, 54 | loader: 'babel-loader', 55 | include: [ 56 | resolve('src'), 57 | resolve('test'), 58 | resolve('node_modules/webpack-dev-server/client') 59 | ] 60 | }, 61 | { 62 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 63 | loader: 'url-loader', 64 | options: { 65 | limit: 10000, 66 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 67 | } 68 | }, 69 | { 70 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 71 | loader: 'url-loader', 72 | options: { 73 | limit: 10000, 74 | name: utils.assetsPath('media/[name].[hash:7].[ext]') 75 | } 76 | }, 77 | { 78 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 79 | loader: 'url-loader', 80 | options: { 81 | limit: 10000, 82 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]'), 83 | publicPath: '../..' 84 | } 85 | }, 86 | { 87 | test: /\.s(c|a)ss$/, 88 | use: [ 89 | 'vue-style-loader', 90 | 'css-loader', 91 | { 92 | loader: 'sass-loader', 93 | // Requires sass-loader@^8.0.0 94 | options: { 95 | implementation: require('sass'), 96 | sassOptions: { 97 | fiber: require('fibers'), 98 | indentedSyntax: true // optional 99 | } 100 | } 101 | } 102 | ] 103 | } 104 | ] 105 | }, 106 | node: { 107 | // prevent webpack from injecting useless setImmediate polyfill because Vue 108 | // source contains it (although only uses it if it's native). 109 | setImmediate: false, 110 | // prevent webpack from injecting mocks to Node native modules 111 | // that does not make sense for the client 112 | dgram: 'empty', 113 | fs: 'empty', 114 | net: 'empty', 115 | tls: 'empty', 116 | child_process: 'empty' 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const appConfig = require('../config/webConfig') 6 | const { merge } = require('webpack-merge') 7 | const path = require('path') 8 | const baseWebpackConfig = require('./webpack.base.conf') 9 | const CopyWebpackPlugin = require('copy-webpack-plugin') 10 | const HtmlWebpackPlugin = require('html-webpack-plugin') 11 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 12 | const portfinder = require('portfinder') 13 | 14 | const HOST = process.env.HOST 15 | const PORT = process.env.PORT && Number(process.env.PORT) 16 | 17 | const devWebpackConfig = merge(baseWebpackConfig, { 18 | mode: 'development', 19 | module: { 20 | rules: utils.styleLoaders({ 21 | sourceMap: config.dev.cssSourceMap, 22 | usePostCSS: true 23 | }) 24 | }, 25 | // cheap-module-eval-source-map is faster for development 26 | devtool: config.dev.devtool, 27 | 28 | // these devServer options should be customized in /config/index.js 29 | devServer: { 30 | clientLogLevel: 'warning', 31 | historyApiFallback: true, 32 | hot: true, 33 | contentBase: false, // since we use CopyWebpackPlugin. 34 | compress: true, 35 | disableHostCheck: true, 36 | host: HOST || config.dev.host, 37 | port: PORT || config.dev.port, 38 | open: config.dev.autoOpenBrowser, 39 | overlay: config.dev.errorOverlay 40 | ? { warnings: false, errors: true } 41 | : false, 42 | publicPath: config.dev.assetsPublicPath, 43 | proxy: config.dev.proxyTable, 44 | quiet: true, // necessary for FriendlyErrorsPlugin 45 | watchOptions: { 46 | poll: config.dev.poll 47 | } 48 | }, 49 | plugins: [ 50 | new webpack.DefinePlugin({ 51 | 'process.env': require('../config/dev.env') 52 | }), 53 | new webpack.HotModuleReplacementPlugin(), 54 | new webpack.NamedModulesPlugin(), // HMR shows correct file names in console on update. 55 | new webpack.NoEmitOnErrorsPlugin(), 56 | // https://github.com/ampedandwired/html-webpack-plugin 57 | new HtmlWebpackPlugin({ 58 | title: 'ZWave To MQTT', 59 | filename: 'index.html', 60 | template: 'views/index.ejs', 61 | templateParameters: { 62 | config: appConfig 63 | }, 64 | inject: true 65 | }), 66 | // copy custom static assets 67 | new CopyWebpackPlugin({ 68 | patterns: [ 69 | { 70 | from: path.resolve(__dirname, '../static'), 71 | to: config.dev.assetsSubDirectory, 72 | globOptions: { 73 | ignore: ['.*'] 74 | } 75 | } 76 | ] 77 | }) 78 | ] 79 | }) 80 | 81 | module.exports = new Promise((resolve, reject) => { 82 | portfinder.basePort = process.env.PORT || config.dev.port 83 | portfinder.getPort((err, port) => { 84 | if (err) { 85 | reject(err) 86 | } else { 87 | // publish the new Port, necessary for e2e tests 88 | process.env.PORT = port 89 | // add port to devServer config 90 | devWebpackConfig.devServer.port = port 91 | 92 | // Add FriendlyErrorsPlugin 93 | devWebpackConfig.plugins.push( 94 | new FriendlyErrorsPlugin({ 95 | compilationSuccessInfo: { 96 | messages: [ 97 | `Your application is running here: http://${devWebpackConfig.devServer.host}:${port}` 98 | ] 99 | }, 100 | onErrors: config.dev.notifyOnErrors 101 | ? utils.createNotifierCallback() 102 | : undefined 103 | }) 104 | ) 105 | 106 | resolve(devWebpackConfig) 107 | } 108 | }) 109 | }) 110 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const path = require('path') 3 | const utils = require('./utils') 4 | const webpack = require('webpack') 5 | const config = require('../config') 6 | const { merge } = require('webpack-merge') 7 | const baseWebpackConfig = require('./webpack.base.conf') 8 | const CopyWebpackPlugin = require('copy-webpack-plugin') 9 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 10 | const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') 11 | const TerserPlugin = require('terser-webpack-plugin') 12 | 13 | const env = require('../config/prod.env') 14 | 15 | const webpackConfig = merge(baseWebpackConfig, { 16 | mode: 'production', 17 | module: { 18 | rules: utils.styleLoaders({ 19 | sourceMap: config.build.productionSourceMap, 20 | extract: true, 21 | usePostCSS: true 22 | }) 23 | }, 24 | optimization: { 25 | minimize: true, 26 | minimizer: [ 27 | new TerserPlugin({ 28 | test: /\.js(\?.*)?$/i 29 | }) 30 | ] 31 | }, 32 | devtool: config.build.productionSourceMap ? config.build.devtool : false, 33 | output: { 34 | path: config.build.assetsRoot, 35 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 36 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 37 | }, 38 | plugins: [ 39 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 40 | new webpack.DefinePlugin({ 41 | 'process.env': env 42 | }), 43 | // extract css into its own file 44 | new MiniCssExtractPlugin({ 45 | filename: utils.assetsPath('css/[name].[contenthash].css') 46 | }), 47 | // Compress extracted CSS. We are using this plugin so that possible 48 | // duplicated CSS from different components can be deduped. 49 | new OptimizeCSSPlugin({ 50 | cssProcessorOptions: config.build.productionSourceMap 51 | ? { safe: true, map: { inline: false } } 52 | : { safe: true } 53 | }), 54 | // keep module.id stable when vendor modules does not change 55 | new webpack.HashedModuleIdsPlugin(), 56 | // enable scope hoisting 57 | new webpack.optimize.ModuleConcatenationPlugin(), 58 | 59 | // copy custom static assets 60 | new CopyWebpackPlugin({ 61 | patterns: [ 62 | { 63 | from: path.resolve(__dirname, '../static'), 64 | to: config.build.assetsSubDirectory, 65 | globOptions: { 66 | ignore: ['.*'] 67 | } 68 | } 69 | ] 70 | }) 71 | ] 72 | }) 73 | 74 | if (config.build.productionGzip) { 75 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 76 | 77 | webpackConfig.plugins.push( 78 | new CompressionWebpackPlugin({ 79 | asset: '[path].gz[query]', 80 | algorithm: 'gzip', 81 | test: new RegExp( 82 | '\\.(' + config.build.productionGzipExtensions.join('|') + ')$' 83 | ), 84 | threshold: 10240, 85 | minRatio: 0.8 86 | }) 87 | ) 88 | } 89 | 90 | if (config.build.bundleAnalyzerReport) { 91 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer') 92 | .BundleAnalyzerPlugin 93 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 94 | } 95 | 96 | module.exports = webpackConfig 97 | -------------------------------------------------------------------------------- /config/app.js: -------------------------------------------------------------------------------- 1 | // config/app.js 2 | module.exports = { 3 | title: 'ZWave To MQTT', 4 | storeDir: 'store', 5 | base: '/', 6 | port: 8091 7 | } 8 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { merge } = require('webpack-merge') 3 | const prodEnv = require('./prod.env') 4 | const appConfig = require('./app.js') 5 | 6 | module.exports = merge(prodEnv, { 7 | NODE_ENV: '"development"', 8 | PORT: appConfig.port 9 | }) 10 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | // Template version: 1.3.1 3 | // see http://vuejs-templates.github.io/webpack for documentation. 4 | 5 | const path = require('path') 6 | 7 | const proxyScheme = process.env.SERVER_SSL ? 'https' : 'http' 8 | const proxyWebSocketScheme = process.env.SERVER_SSL ? 'wss' : 'ws' 9 | const proxyHostname = process.env.SERVER_HOST 10 | ? process.env.SERVER_HOST 11 | : 'localhost' 12 | const proxyPort = process.env.SERVER_PORT ? process.env.SERVER_PORT : '8091' 13 | 14 | const proxyURL = process.env.SERVER_URL 15 | ? process.env.SERVER_URL 16 | : `${proxyScheme}://${proxyHostname}:${proxyPort}` 17 | 18 | const proxyWSURL = process.env.SERVER_WS_URL 19 | ? process.env.SERVER_WS_URL 20 | : `${proxyWebSocketScheme}://${proxyHostname}:${proxyPort}` 21 | 22 | module.exports = { 23 | dev: { 24 | // Paths 25 | assetsSubDirectory: 'static', 26 | assetsPublicPath: '/', 27 | proxyTable: { 28 | '/socket.io': { 29 | target: proxyWSURL, 30 | ws: true 31 | }, 32 | '/health': proxyURL, 33 | '/api': proxyURL 34 | }, 35 | 36 | // Various Dev Server settings 37 | host: 'localhost', // can be overwritten by process.env.HOST 38 | port: 8092, // can be overwritten by process.env.PORT, if port is in use, a free one will be determined 39 | autoOpenBrowser: false, 40 | errorOverlay: true, 41 | notifyOnErrors: true, 42 | poll: false, // https://webpack.js.org/configuration/dev-server/#devserver-watchoptions- 43 | 44 | // Use Eslint Loader? 45 | // If true, your code will be linted during bundling and 46 | // linting errors and warnings will be shown in the console. 47 | useEslint: false, 48 | // If true, eslint errors and warnings will also be shown in the error overlay 49 | // in the browser. 50 | showEslintErrorsInOverlay: false, 51 | 52 | /** 53 | * Source Maps 54 | */ 55 | 56 | // https://webpack.js.org/configuration/devtool/#development 57 | devtool: 'cheap-module-eval-source-map', 58 | 59 | // If you have problems debugging vue-files in devtools, 60 | // set this to false - it *may* help 61 | // https://vue-loader.vuejs.org/en/options.html#cachebusting 62 | cacheBusting: true, 63 | 64 | cssSourceMap: true 65 | }, 66 | 67 | build: { 68 | // Template for index.html 69 | index: path.resolve(__dirname, '../dist/index.html'), 70 | 71 | // Paths 72 | assetsRoot: path.resolve(__dirname, '../dist'), 73 | assetsSubDirectory: 'static', 74 | assetsPublicPath: '/', 75 | 76 | /** 77 | * Source Maps 78 | */ 79 | 80 | productionSourceMap: true, 81 | // https://webpack.js.org/configuration/devtool/#production 82 | devtool: '#source-map', 83 | 84 | // Gzip off by default as many popular static hosts such as 85 | // Surge or Netlify already gzip all static assets for you. 86 | // Before setting to `true`, make sure to: 87 | // npm install --save-dev compression-webpack-plugin 88 | productionGzip: false, 89 | productionGzipExtensions: ['js', 'css'], 90 | 91 | // Run the build command with an extra argument to 92 | // View the bundle analyzer report after build finishes: 93 | // `npm run build --report` 94 | // Set to `true` or `false` to always turn it on or off 95 | bundleAnalyzerReport: process.env.npm_config_report 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | module.exports = { 3 | NODE_ENV: '"production"', 4 | VERSION: `"${require('../package.json').version}"` 5 | } 6 | -------------------------------------------------------------------------------- /config/store.js: -------------------------------------------------------------------------------- 1 | // config/store.js 2 | module.exports = { 3 | settings: { file: 'settings.json', default: {} }, 4 | scenes: { file: 'scenes.json', default: [] }, 5 | nodes: { file: 'nodes.json', default: [] } 6 | } 7 | -------------------------------------------------------------------------------- /config/webConfig.js: -------------------------------------------------------------------------------- 1 | const appConfig = require('./app') 2 | 3 | appConfig.base = appConfig.base && appConfig.base.replace(/\/?$/, '/') 4 | 5 | const defaultConfig = { 6 | base: '/', 7 | title: 'ZWave To MQTT' 8 | } 9 | 10 | module.exports = { 11 | ...defaultConfig, 12 | ...appConfig 13 | } 14 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # ---------------- 2 | # STEP 1: 3 | FROM chrisns/openzwave:alpine-1.6.1520 as ozw 4 | 5 | # ---------------- 6 | # STEP 2: 7 | FROM node:erbium-alpine AS build-z2m 8 | 9 | # Install required dependencies 10 | RUN apk --no-cache add \ 11 | coreutils \ 12 | linux-headers \ 13 | alpine-sdk \ 14 | python \ 15 | openssl 16 | 17 | # needed to build openzwave-shared 18 | COPY --from=ozw /usr/local/include/openzwave /usr/local/include/openzwave 19 | COPY --from=ozw /openzwave/libopenzwave.so* /lib/ 20 | COPY --from=ozw /openzwave/config /usr/local/etc/openzwave 21 | 22 | ENV LD_LIBRARY_PATH /lib 23 | 24 | WORKDIR /root/Zwave2Mqtt 25 | COPY . . 26 | RUN npm config set unsafe-perm true 27 | RUN npm install 28 | RUN npm run build 29 | RUN npm prune --production 30 | RUN rm -rf \ 31 | build \ 32 | index.html \ 33 | package-lock.json \ 34 | package.sh \ 35 | src \ 36 | static \ 37 | stylesheets 38 | 39 | # ---------------- 40 | # STEP 3: 41 | FROM node:erbium-alpine 42 | 43 | LABEL maintainer="robertsLando" 44 | 45 | RUN apk add --no-cache \ 46 | libstdc++ \ 47 | libgcc \ 48 | libusb \ 49 | tzdata \ 50 | eudev 51 | 52 | # Copy files from previous build stage 53 | COPY --from=ozw /openzwave/libopenzwave.so* /lib/ 54 | COPY --from=ozw /openzwave/config /usr/local/etc/openzwave 55 | COPY --from=build-z2m /root/Zwave2Mqtt /usr/src/app 56 | 57 | # Set enviroment 58 | ENV LD_LIBRARY_PATH /lib 59 | 60 | WORKDIR /usr/src/app 61 | 62 | EXPOSE 8091 63 | 64 | CMD ["node", "bin/www"] 65 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Zwave2Mqtt-docker 2 | 3 | [![dockeri.co](https://dockeri.co/image/robertslando/zwave2mqtt)](https://hub.docker.com/r/robertslando/zwave2mqtt) 4 | 5 | [![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/MVg9wc2HE 'Buy Me A Coffee') 6 | 7 | Docker container for Zwave2Mqtt Gateway and Control Panel app using pkg 8 | 9 | **ATTENTION: STARTING FROM Z2M 2.1.1 OZW 1.4 SUPPORT HAS ENDED AND `latest` TAG WILL ALWAYS HAVE OZW 1.6** 10 | 11 | ## Tags 12 | 13 | Supported architectures are: 14 | 15 | - `x86_64 amd64` 16 | - `armv6` 17 | - `armv7` (Ex. Raspberry PI) 18 | - `arm64` (Ex. OrangePI NanoPI) 19 | 20 | **Available Tags**: 21 | 22 | - `latest`: Always points to the latest stable version published (using OZW 1.6) 23 | - `dev`: Always point to latest OZW and Zwave2Mqtt master branches 24 | - `3.1.0`: OZW 1.6.1115 25 | - `3.0.4`: OZW 1.6.1115 26 | - `3.0.3`: OZW 1.6.1080 27 | - `3.0.2`: OZW 1.6.1061 28 | - `3.0.1`: OZW 1.6.1045 29 | - `3.0.0`: OZW 1.6.1045 30 | - `2.2.0`: OZW 1.6.1038 31 | - `2.1.1`: OZW 1.6.1004 32 | - `2.1.0`: OZW 1.4 33 | - `2.1.0-dev`: OZW 1.6.1004 34 | - `2.0.6`: OZW 1.4 35 | - `2.0.6-dev`: OZW 1.6.962 36 | 37 | **DEPRECATED**: 38 | 39 | - `latest-dev`: Starting from version 2.1.1 OZW 1.4 is no more supported so `latest` tag will always contain OZW 1.6. Last available `latest-dev` manifest is running z2m 2.1.0 with ozw 1.6 40 | 41 | ## Install 42 | 43 | Here there are 3 different way to start the container and provide data persistence. In all of this solutions **remember to**: 44 | 45 | 1. Replace `/dev/ttyACM0` with your serial device 46 | 2. Add `-e TZ=Europe/Stockholm` to the `docker run` command to set the correct timezone in container 47 | 48 | ### Run using volumes 49 | 50 | ```bash 51 | docker run --rm -it -p 8091:8091 --device=/dev/ttyACM0 --mount source=zwave2mqtt,target=/usr/src/app/store robertslando/zwave2mqtt:latest 52 | ``` 53 | 54 | ### Run using local folder 55 | 56 | Here we will store our data in the current path (`$(pwd)`) named `store`. You can choose the path and the directory name you prefer, a valid alternative (with linux) could be `/var/lib/zwave2mqtt` 57 | 58 | ```bash 59 | mkdir store 60 | docker run --rm -it -p 8091:8091 --device=/dev/ttyACM0 -v $(pwd)/store:/usr/src/app/store robertslando/zwave2mqtt:latest 61 | ``` 62 | 63 | ### Run as a service 64 | 65 | To run the app as a service you can use the `docker-compose.yml` file you find [here](./docker-compose.yml). Here is the content: 66 | 67 | ```yml 68 | version: '3.7' 69 | services: 70 | zwave2mqtt: 71 | container_name: zwave2mqtt 72 | image: robertslando/zwave2mqtt:latest 73 | restart: always 74 | tty: true 75 | stop_signal: SIGINT 76 | networks: 77 | - zwave 78 | devices: 79 | - '/dev/ttyACM0:/dev/ttyACM0' 80 | volumes: 81 | - ./store:/usr/src/app/store 82 | ports: 83 | - '8091:8091' 84 | networks: 85 | zwave: 86 | # volumes: 87 | # zwave2mqtt: 88 | # name: zwave2mqtt 89 | ``` 90 | 91 | Like the other solutions, remember to replace device `/dev/ttyACM0` with the path of your USB stick and choose the solution you prefer for data persistence. 92 | 93 | ### Run as a kubernetes deployment 94 | 95 | ```yml 96 | apiVersion: apps/v1 97 | kind: Deployment 98 | metadata: 99 | name: zwave 100 | spec: 101 | replicas: 1 102 | selector: 103 | matchLabels: 104 | name: zwave 105 | template: 106 | metadata: 107 | labels: 108 | name: zwave 109 | spec: 110 | containers: 111 | - name: zwave 112 | image: robertslando/zwave2mqtt:latest 113 | livenessProbe: 114 | failureThreshold: 10 115 | httpGet: 116 | httpHeaders: 117 | - name: Accept 118 | value: text/plain 119 | path: /health 120 | port: http 121 | initialDelaySeconds: 30 122 | periodSeconds: 3 123 | successThreshold: 1 124 | timeoutSeconds: 1 125 | ports: 126 | - containerPort: 8091 127 | name: http 128 | protocol: TCP 129 | resources: 130 | limits: 131 | cpu: "1" 132 | memory: 512Mi 133 | requests: 134 | cpu: "1" 135 | memory: 400Mi 136 | securityContext: 137 | allowPrivilegeEscalation: true 138 | privileged: true 139 | volumeMounts: 140 | - mountPath: /dev/ttyUSB1 141 | name: zwavestick 142 | - mountPath: /usr/src/app/store 143 | name: data 144 | # - mountPath: /usr/src/app/store/settings.json <-- if putting your settings.json in a secret 145 | # name: config 146 | # readOnly: true 147 | # subPath: settings.json 148 | nodeSelector: 149 | kubernetes.io/hostname: stick1 #<--- the name of your cluster node that the zwave usb stick in 150 | volumes: 151 | # - name: config <-- if putting your settings.json in a secret 152 | # secret: 153 | # defaultMode: 420 154 | # secretName: zwave2mqtt 155 | - name: zwavestick 156 | hostPath: 157 | path: /dev/ttyACM0 158 | type: File 159 | - name: data 160 | hostPath: 161 | path: /zwave/data 162 | --- 163 | apiVersion: v1 164 | kind: Service 165 | metadata: 166 | name: zwave 167 | spec: 168 | ports: 169 | - name: http 170 | port: 80 171 | targetPort: http 172 | selector: 173 | name: zwave 174 | --- 175 | apiVersion: extensions/v1beta1 176 | kind: Ingress 177 | metadata: 178 | name: zwave 179 | spec: 180 | rules: 181 | - host: zwave.example.com 182 | http: 183 | paths: 184 | - backend: 185 | serviceName: zwave 186 | servicePort: http 187 | ``` 188 | 189 | Like the other solutions, remember to replace device `/dev/ttyACM0` with the path of your USB stick and choose the solution you prefer for data persistence. 190 | 191 | ### Upgrade from 1.0.0 to 1.1.0 192 | 193 | In 1.0.0 version all application data where stored inside the volume. This could cause many problems expectially when upgrading. To prevent this, starting from version 1.1.0 all persistence data have been moved to application `store` folder. If you have all your data stored inside a volume `zwave2mqtt` this is how to backup them: 194 | 195 | ```bash 196 | APP=$(docker run --rm -it -d --mount source=zwave2mqtt,target=/usr/src/app robertslando/zwave2mqtt:latest) 197 | docker cp $APP:/usr/src/app ./ 198 | docker kill $APP 199 | ``` 200 | 201 | This will create a directory `app` with all app data inside. Move all files like `OZW_log.txt zwscene.xml zwcfg_.xml` in `app/store` folder and use that folder as volume following [this](#run-using-local-folder) section 202 | 203 | ### ATTENTION 204 | 205 | If you get the error `standard_init_linux.go:207: exec user process caused "exec format error"` probably it's because you previously installed a wrong architecture version of the package so in that case you must delete the existing volume that contains the old executable: 206 | 207 | `docker volume rm zwave2mqtt` 208 | 209 | Check files inside volume 210 | 211 | ```bash 212 | docker run --rm -it --mount source=zwave2mqtt,target=/usr/src/app robertslando/zwave2mqtt:latest find /usr/src/app 213 | ``` 214 | 215 | Delete Volume 216 | 217 | ```bash 218 | docker volume rm zwave2mqtt 219 | ``` 220 | 221 | ### Auto Update OZW device database 222 | 223 | If you would like to enable this feature of OZW you need to keep the device database inside a volume or a local folder and map it inside the container. To do this follow this steps: 224 | 225 | ```sh 226 | APP=$(docker run --rm -it -d robertslando/zwave2mqtt:latest) 227 | docker cp $APP:/usr/local/etc/openzwave ./ 228 | docker kill $APP 229 | ``` 230 | 231 | With this command you should have copied all your container device db in a local folder named `openzwave`. Now you should map this folder inside your container: 232 | 233 | By adding an option: 234 | 235 | `-v $(pwd)/openzwave:/usr/local/etc/openzwave` 236 | 237 | Or in docker-compose file: 238 | 239 | ```yml 240 | volumes: 241 | - ./openzwave:/usr/local/etc/openzwave 242 | ``` 243 | 244 | ## Custom builds 245 | 246 | The docker images are the latest stable images of the [zwave2mqtt](https://github.com/OpenZWave/Zwave2Mqtt) repo. If you want to keep your image updated with the latest changes you can build it on your local machine. Just select a commit sha, a branch name, or a tag name, and pass it to docker build using the _--build-arg_ option for the _Z2M_GIT_SHA1_ and _OPENZWAVE_GIT_SHA1_ arguments. For example: 247 | 248 | ```bash 249 | git clone https://github.com/OpenZWave/Zwave2Mqtt.git 250 | cd Zwave2Mqtt/docker 251 | docker build -f docker/Dockerfile --build-arg Z2M_GIT_SHA1=master --build-arg OPENZWAVE_GIT_SHA1=master -t robertslando/zwave2mqtt:latest . 252 | ``` 253 | 254 | Build just the `build` container 255 | 256 | ```bash 257 | docker build -f docker/Dockerfile --target=build -t robertslando/zwave2mqtt_build . 258 | 259 | ``` 260 | 261 | ## SSH inside container 262 | 263 | ```bash 264 | docker run --rm -p 8091:8091 --device=/dev/ttyACM0 -it --mount source=zwave2mqtt,target=/usr/src/app robertslando/zwave2mqtt:latest sh 265 | ``` 266 | 267 | ```bash 268 | docker run --rm -p 8091:8091 --device=/dev/ttyACM0 -it --mount source=zwave2mqtt,target=/dist/pkg robertslando/zwave2mqtt_build sh 269 | ``` 270 | -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | zwave2mqtt: 4 | container_name: zwave2mqtt 5 | image: robertslando/zwave2mqtt:latest 6 | restart: always 7 | tty: true 8 | stop_signal: SIGINT 9 | networks: 10 | - zwave 11 | devices: 12 | - '/dev/ttyACM0:/dev/ttyACM0' 13 | volumes: 14 | - ./store:/usr/src/app/store 15 | ports: 16 | - '8091:8091' 17 | networks: 18 | zwave: 19 | # volumes: 20 | # zwave2mqtt: 21 | # name: zwave2mqtt 22 | 23 | -------------------------------------------------------------------------------- /docs/MQTT-Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/docs/MQTT-Logo.png -------------------------------------------------------------------------------- /docs/OZW_Logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/docs/OZW_Logo.png -------------------------------------------------------------------------------- /docs/OZW_Panel_Node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/docs/OZW_Panel_Node.png -------------------------------------------------------------------------------- /docs/debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/docs/debug.png -------------------------------------------------------------------------------- /docs/groups_associations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/docs/groups_associations.png -------------------------------------------------------------------------------- /docs/hass_devices.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/docs/hass_devices.png -------------------------------------------------------------------------------- /docs/mesh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/docs/mesh.png -------------------------------------------------------------------------------- /docs/mesh_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/docs/mesh_diagram.png -------------------------------------------------------------------------------- /docs/scenes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/docs/scenes.png -------------------------------------------------------------------------------- /docs/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/docs/settings.png -------------------------------------------------------------------------------- /docs/subpath.md: -------------------------------------------------------------------------------- 1 | # ZWave To MQTT Behind a Reverse Proxy 2 | 3 | There are two ways to enable ZWave To MQTT to sit behing a proxy that uses 4 | subpaths to serve the pages and services. 5 | 6 | You can use a header to signal where the external path is or you can configure 7 | the base path. In both cases these are dynamic configurations, so you can deploy 8 | without having to build again the frontend. 9 | 10 | ## Using an HTTP header 11 | 12 | You can pass the external path by setting the `X-External-Path` header, for example 13 | suppose you had the following `nginx` configuration: 14 | 15 | ```nginx 16 | map $http_upgrade $connection_upgrade { 17 | default upgrade; 18 | '' close; 19 | } 20 | 21 | server { 22 | listen 9000 default_server; 23 | listen [::]:9000 default_server; 24 | 25 | location /hassio/ingress/ { 26 | proxy_pass http://localhost:8091/; 27 | proxy_set_header X-External-Path /hassio/ingress; 28 | proxy_http_version 1.1; 29 | proxy_set_header Upgrade $http_upgrade; 30 | proxy_set_header Connection $connection_upgrade; 31 | } 32 | } 33 | ``` 34 | 35 | This will tell the application to serve the application and relevant elements under 36 | `/hassio/ingress/`. 37 | 38 | In case you are using the [ingress of Home Assistant](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/) you will want to 39 | pick up the `X-Ingress-Path;` and map it, something along 40 | these lines: 41 | 42 | ```nginx 43 | proxy_set_header X-External-Path $http_x_ingress_path; 44 | ``` 45 | 46 | ## Using the configuration 47 | 48 | You can simply change the `config/app.js` and set `base` to whatever is 49 | the subpath you will be serving this from. 50 | 51 | As an example, if your proxy is placing the app behind `/zwave/` your configuration 52 | would look like: 53 | 54 | ```javascript 55 | module.exports = { 56 | title: 'ZWave to MQTT', 57 | storeDir: 'store', 58 | base: '/zwave/', 59 | port: 8091 60 | } 61 | ``` 62 | -------------------------------------------------------------------------------- /hass/configurations.js: -------------------------------------------------------------------------------- 1 | // List of Home-Assistant configuration for MQTT Discovery 2 | // https://www.home-assistant.io/docs/mqtt/discovery/ 3 | 4 | module.exports = { 5 | // Binary sensor https://www.home-assistant.io/components/binary_sensor.mqtt 6 | binary_sensor_occupancy: { 7 | type: 'binary_sensor', 8 | object_id: 'occupancy', 9 | discovery_payload: { 10 | payload_on: true, 11 | payload_off: false, 12 | value_template: '{{ value_json.value }}', 13 | device_class: 'motion' 14 | } 15 | }, 16 | binary_sensor_presence: { 17 | type: 'binary_sensor', 18 | object_id: 'presence', 19 | discovery_payload: { 20 | payload_on: true, 21 | payload_off: false, 22 | value_template: '{{ value_json.value }}', 23 | device_class: 'presence' 24 | } 25 | }, 26 | binary_sensor_contact: { 27 | type: 'binary_sensor', 28 | object_id: 'contact', 29 | discovery_payload: { 30 | payload_on: true, 31 | payload_off: false, 32 | value_template: '{{ value_json.value }}', 33 | device_class: 'door' 34 | } 35 | }, 36 | binary_sensor_lock: { 37 | type: 'binary_sensor', 38 | object_id: 'lock', 39 | discovery_payload: { 40 | payload_on: false, 41 | payload_off: true, 42 | value_template: '{{ value_json.value }}', 43 | device_class: 'lock' 44 | } 45 | }, 46 | binary_sensor_water_leak: { 47 | type: 'binary_sensor', 48 | object_id: 'water_leak', 49 | discovery_payload: { 50 | payload_on: true, 51 | payload_off: false, 52 | value_template: '{{ value_json.value }}', 53 | device_class: 'moisture' 54 | } 55 | }, 56 | binary_sensor_smoke: { 57 | type: 'binary_sensor', 58 | object_id: 'smoke', 59 | discovery_payload: { 60 | payload_on: true, 61 | payload_off: false, 62 | value_template: '{{ value_json.value }}', 63 | device_class: 'smoke' 64 | } 65 | }, 66 | binary_sensor_gas: { 67 | type: 'binary_sensor', 68 | object_id: 'gas', 69 | discovery_payload: { 70 | payload_on: true, 71 | payload_off: false, 72 | value_template: '{{ value_json.value }}', 73 | device_class: 'gas' 74 | } 75 | }, 76 | binary_sensor_carbon_monoxide: { 77 | type: 'binary_sensor', 78 | object_id: 'carbon_monoxide', 79 | discovery_payload: { 80 | payload_on: true, 81 | payload_off: false, 82 | value_template: '{{ value_json.value }}', 83 | device_class: 'safety' 84 | } 85 | }, 86 | binary_sensor_tamper: { 87 | type: 'binary_sensor', 88 | object_id: 'tamper', 89 | discovery_payload: { 90 | payload_on: true, 91 | payload_off: false, 92 | value_template: '{{ value_json.value }}', 93 | device_class: 'safety' 94 | } 95 | }, 96 | binary_sensor_alarm: { 97 | type: 'binary_sensor', 98 | object_id: 'alarm', 99 | discovery_payload: { 100 | payload_on: true, 101 | payload_off: false, 102 | value_template: '{{ value_json.value }}', 103 | device_class: 'problem' 104 | } 105 | }, 106 | binary_sensor_router: { 107 | type: 'binary_sensor', 108 | object_id: 'router', 109 | discovery_payload: { 110 | payload_on: true, 111 | payload_off: false, 112 | value_template: '{{ value_json.value }}', 113 | device_class: 'connectivity' 114 | } 115 | }, 116 | binary_sensor_battery_low: { 117 | type: 'binary_sensor', 118 | object_id: 'battery_low', 119 | discovery_payload: { 120 | payload_on: true, 121 | payload_off: false, 122 | value_template: '{{ value_json.value}}', 123 | device_class: 'battery' 124 | } 125 | }, 126 | 127 | // Sensor https://www.home-assistant.io/components/sensor.mqtt 128 | sensor_generic: { 129 | type: 'sensor', 130 | object_id: 'generic', 131 | discovery_payload: { 132 | value_template: '{{ value_json.value }}' 133 | } 134 | }, 135 | central_scene: { 136 | type: 'sensor', 137 | object_id: 'scene_state', 138 | discovery_payload: { 139 | state_topic: true, 140 | value_template: '{{ value_json.value}}' 141 | } 142 | }, 143 | // Light https://www.home-assistant.io/components/light.mqtt 144 | light_rgb_switch: { 145 | type: 'light', 146 | object_id: 'rgb_switch', 147 | discovery_payload: { 148 | state_topic: true, 149 | command_topic: true, 150 | rgb_command_template: "{{ '#%02x%02x%02x' | format(red, green, blue)}}", 151 | rgb_command_topic: true, 152 | rgb_state_topic: true, 153 | rgb_value_template: 154 | '{{ value_json.value[1:3] | int(0, 16) }},{{ value_json.value[3:5] | int(0, 16) }},{{ value_json.value[5:7] | int(0, 16) }}' 155 | } 156 | }, 157 | light_rgb_dimmer: { 158 | type: 'light', 159 | object_id: 'rgb_dimmer', 160 | discovery_payload: { 161 | state_topic: true, 162 | command_topic: true, 163 | brightness_state_topic: true, 164 | brightness_command_topic: true, 165 | on_command_type: 'first', 166 | state_value_template: '{{ "OFF" if value_json.value == 0 else "ON" }}', 167 | brightness_value_template: 168 | '{{ (value_json.value / 99 * 255) | round(0) }}', 169 | rgb_command_template: '{{ "#%02x%02x%02x" | format(red, green, blue)}}', 170 | rgb_command_topic: true, 171 | rgb_state_topic: true, 172 | rgb_value_template: 173 | '{{ value_json.value[1:3] | int(0, 16) }},{{ value_json.value[3:5] | int(0, 16) }},{{ value_json.value[5:7] | int(0, 16) }}' 174 | } 175 | }, 176 | light_dimmer: { 177 | type: 'light', 178 | object_id: 'dimmer', 179 | discovery_payload: { 180 | schema: 'template', 181 | brightness_template: '{{ (value_json.value / 99 * 255) | round(0) }}', 182 | state_topic: true, 183 | state_template: '{{ "off" if value_json.value == 0 else "on" }}', 184 | command_topic: true, 185 | command_on_template: 186 | '{{ ((brightness / 255 * 99) | round(0)) if brightness is defined else 255 }}', 187 | command_off_template: '0' 188 | } 189 | }, 190 | volume_dimmer: { 191 | type: 'light', 192 | object_id: 'volume_dimmer', 193 | discovery_payload: { 194 | command_topic: true, 195 | state_topic: false, 196 | brightness_command_topic: true, 197 | brightness_scale: 100, 198 | brightness_state_topic: true, 199 | brightness_value_template: '{{ value_json.value }}', 200 | on_command_type: 'last', 201 | payload_off: 0, 202 | payload_on: 25 203 | } 204 | }, 205 | 206 | // Switch https://www.home-assistant.io/components/switch.mqtt 207 | switch: { 208 | type: 'switch', 209 | object_id: 'switch', 210 | discovery_payload: { 211 | payload_off: false, 212 | payload_on: true, 213 | value_template: '{{ value_json.value }}', 214 | command_topic: true 215 | } 216 | }, 217 | 218 | // Cover https://www.home-assistant.io/components/cover.mqtt 219 | cover: { 220 | type: 'cover', 221 | object_id: 'cover', 222 | discovery_payload: { 223 | command_topic: true, 224 | optimistic: true 225 | } 226 | }, 227 | cover_position: { 228 | type: 'cover', 229 | object_id: 'position', 230 | discovery_payload: { 231 | state_topic: true, 232 | command_topic: true, 233 | position_topic: true, 234 | set_position_topic: true, 235 | value_template: '{{ value_json.value | round(0) }}', 236 | position_open: 99, 237 | position_closed: 0, 238 | payload_open: '99', 239 | payload_close: '0' 240 | } 241 | }, 242 | 243 | barrier_state: { 244 | type: 'cover', 245 | object_id: 'barrier_state', 246 | discovery_payload: { 247 | command_topic: true, 248 | state_topic: true, 249 | value_template: '{{ value_json.value }}', 250 | device_class: 'garage', 251 | payload_open: 'Opened', 252 | payload_close: 'Closed', 253 | payload_stop: 'Stopped', 254 | state_open: 'Opened', 255 | state_opening: 'Opening', 256 | state_closed: 'Closed', 257 | state_closing: 'Closing' 258 | } 259 | }, 260 | 261 | // Lock https://www.home-assistant.io/components/lock.mqtt 262 | lock: { 263 | type: 'lock', 264 | object_id: 'lock', 265 | discovery_payload: { 266 | command_topic: true, 267 | state_locked: 'true', 268 | state_unlocked: 'false', 269 | value_template: 270 | '{% if value_json.value == false %} false {% elif value_json.value == true %} true {% else %} unknown {% endif %}' 271 | } 272 | }, 273 | 274 | // Thermostat/HVAC https://www.home-assistant.io/components/climate.mqtt 275 | thermostat: { 276 | type: 'climate', 277 | object_id: 'climate', 278 | discovery_payload: { 279 | min_temp: 7, 280 | max_temp: 30, 281 | modes: ['off', 'auto', 'heat'], 282 | mode_state_topic: true, 283 | mode_state_template: '{{ value_json.mode }}', 284 | mode_command_topic: true, 285 | current_temperature_topic: true, 286 | current_temperature_template: '{{ value_json.value }}', 287 | temperature_state_topic: true, 288 | temperature_state_template: '{{ value_json.value }}', 289 | temperature_command_topic: true 290 | } 291 | }, 292 | 293 | // Fan https://www.home-assistant.io/components/fan.mqtt/ 294 | fan: { 295 | type: 'fan', 296 | object_id: 'fan', 297 | discovery_payload: { 298 | state_topic: true, 299 | state_value_template: '{{ value_json.state }}', 300 | command_topic: true, 301 | command_topic_postfix: 'fan_state', 302 | speed_state_topic: true, 303 | speed_command_topic: true, 304 | speed_value_template: '{{ value_json.speed }}', 305 | speeds: ['off', 'low', 'medium', 'high', 'on', 'auto', 'smart'] 306 | } 307 | }, 308 | sound_switch: { 309 | type: 'fan', 310 | object_id: 'sound_switch', 311 | discovery_payload: { 312 | command_topic: true, 313 | speed_command_topic: true, 314 | speed_state_topic: true, 315 | state_topic: true, 316 | speeds: ['off', 'low', 'medium', 'high'], 317 | payload_low_speed: 10, 318 | payload_medium_speed: 25, 319 | payload_high_speed: 50, 320 | payload_off: 0, 321 | payload_on: 25, 322 | state_value_template: '{{ value_json.value | int }}', 323 | speed_value_template: '{{ value_json.value | int }}' 324 | } 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /kubernetes/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: zwave 5 | spec: 6 | replicas: 1 7 | selector: 8 | matchLabels: 9 | name: zwave 10 | template: 11 | metadata: 12 | labels: 13 | name: zwave 14 | spec: 15 | containers: 16 | - name: zwave 17 | image: robertslando/zwave2mqtt:latest 18 | livenessProbe: 19 | failureThreshold: 12 20 | httpGet: 21 | httpHeaders: 22 | - name: Accept 23 | value: text/plain 24 | path: /health 25 | port: http 26 | initialDelaySeconds: 30 27 | periodSeconds: 10 28 | successThreshold: 1 29 | timeoutSeconds: 2 30 | ports: 31 | - containerPort: 8091 32 | name: http 33 | protocol: TCP 34 | resources: 35 | limits: 36 | cpu: '1' 37 | memory: 512Mi 38 | requests: 39 | cpu: '1' 40 | memory: 400Mi 41 | securityContext: 42 | allowPrivilegeEscalation: true 43 | privileged: true 44 | volumeMounts: 45 | - mountPath: /dev/ttyUSB1 46 | name: zwavestick 47 | - mountPath: /usr/src/app/store 48 | name: data 49 | # - mountPath: /usr/local/etc/openzwave 50 | # name: ozwdatabase 51 | # - mountPath: /usr/src/app/store/settings.json <-- if putting your settings.json in a secret 52 | # name: config 53 | # readOnly: true 54 | # subPath: settings.json 55 | # nodeSelector: 56 | # kubernetes.io/hostname: stick1 #<--- the name of your cluster node that the zwave usb stick in 57 | volumes: 58 | # - name: config <-- if putting your settings.json in a secret 59 | # secret: 60 | # defaultMode: 420 61 | # secretName: zwave2mqtt 62 | - name: zwavestick 63 | hostPath: 64 | path: /dev/ttyACM0 65 | type: File 66 | - name: data 67 | hostPath: 68 | path: /zwave/data 69 | # - name: ozwdatabase 70 | # hostPath: 71 | # path: /zwave/database 72 | -------------------------------------------------------------------------------- /kubernetes/ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: extensions/v1beta1 2 | kind: Ingress 3 | metadata: 4 | name: zwave 5 | spec: 6 | rules: 7 | - host: zwave.example.com 8 | http: 9 | paths: 10 | - backend: 11 | serviceName: zwave 12 | servicePort: http 13 | -------------------------------------------------------------------------------- /kubernetes/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: zwave 5 | -------------------------------------------------------------------------------- /kubernetes/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: zwave 5 | spec: 6 | ports: 7 | - name: http 8 | port: 80 9 | targetPort: http 10 | selector: 11 | name: zwave 12 | -------------------------------------------------------------------------------- /kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - kubernetes/namespace.yaml 6 | - kubernetes/deployment.yaml 7 | - kubernetes/service.yaml 8 | - kubernetes/ingress.yaml 9 | 10 | namespace: zwave 11 | -------------------------------------------------------------------------------- /lib/MqttClient.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // eslint-disable-next-line one-var 4 | var reqlib = require('app-root-path').require, 5 | mqtt = require('mqtt'), 6 | utils = reqlib('/lib/utils.js'), 7 | NeDBStore = require('mqtt-nedb-store'), 8 | EventEmitter = require('events'), 9 | storeDir = reqlib('config/app.js').storeDir, 10 | debug = reqlib('/lib/debug')('Mqtt'), 11 | url = require('native-url'), 12 | inherits = require('util').inherits 13 | 14 | debug.color = 5 15 | 16 | const CLIENTS_PREFIX = '_CLIENTS' 17 | const EVENTS_PREFIX = '_EVENTS' 18 | 19 | const DEVICES_PREFIX = '$devices' 20 | 21 | const BROADCAST_PREFIX = '_BROADCAST' 22 | 23 | const NAME_PREFIX = 'ZWAVE_GATEWAY-' 24 | 25 | var ACTIONS = ['broadcast', 'api'] 26 | 27 | var HASS_WILL = 'homeassistant/status' 28 | 29 | /** 30 | * The constructor 31 | */ 32 | function MqttClient (config) { 33 | if (!(this instanceof MqttClient)) { 34 | return new MqttClient(config) 35 | } 36 | EventEmitter.call(this) 37 | init.call(this, config) 38 | } 39 | 40 | inherits(MqttClient, EventEmitter) 41 | 42 | function init (config) { 43 | this.config = config 44 | this.toSubscribe = [] 45 | 46 | if (!config || config.disabled) { 47 | debug('MQTT is disabled') 48 | return 49 | } 50 | 51 | this.clientID = this.cleanName(NAME_PREFIX + config.name) 52 | 53 | var parsed = url.parse(config.host || '') 54 | var protocol = 'mqtt' 55 | 56 | if (parsed.protocol) protocol = parsed.protocol.replace(/:$/, '') 57 | 58 | var options = { 59 | clientId: this.clientID, 60 | reconnectPeriod: config.reconnectPeriod, 61 | clean: config.clean, 62 | rejectUnauthorized: !config.allowSelfsigned, 63 | protocol: protocol, 64 | host: parsed.hostname || config.host, 65 | port: config.port, 66 | will: { 67 | topic: this.getClientTopic(), 68 | payload: JSON.stringify({ value: false }), 69 | qos: this.config.qos, 70 | retain: this.config.retain 71 | } 72 | } 73 | 74 | if (['mqtts', 'wss', 'wxs', 'alis', 'tls'].indexOf(protocol) >= 0) { 75 | if (!config.allowSelfsigned) options.ca = config._ca 76 | options.key = config._key 77 | options.cert = config._cert 78 | } 79 | 80 | if (config.store) { 81 | const COMPACT = { autocompactionInterval: 30000 } 82 | var manager = NeDBStore(utils.joinPath(true, storeDir, 'mqtt'), { 83 | incoming: COMPACT, 84 | outgoing: COMPACT 85 | }) 86 | options.incomingStore = manager.incoming 87 | options.outgoingStore = manager.outgoing 88 | } 89 | 90 | if (config.auth) { 91 | options.username = config.username 92 | options.password = config.password 93 | } 94 | 95 | try { 96 | var client = mqtt.connect(options) 97 | 98 | this.client = client 99 | 100 | client.on('connect', onConnect.bind(this)) 101 | client.on('message', onMessageReceived.bind(this)) 102 | client.on('reconnect', onReconnect.bind(this)) 103 | client.on('close', onClose.bind(this)) 104 | client.on('error', onError.bind(this)) 105 | client.on('offline', onOffline.bind(this)) 106 | } catch (e) { 107 | debug('Error while connecting MQTT', e.message) 108 | this.error = e.message 109 | } 110 | } 111 | 112 | /** 113 | * Function called when MQTT client connects 114 | */ 115 | function onConnect () { 116 | debug('MQTT client connected') 117 | this.emit('connect') 118 | 119 | if (this.toSubscribe) { 120 | for (var i = 0; i < this.toSubscribe.length; i++) { 121 | this.subscribe(this.toSubscribe[i]) 122 | } 123 | } 124 | 125 | this.client.subscribe(HASS_WILL) 126 | 127 | // subscribe to actions 128 | // eslint-disable-next-line no-redeclare 129 | for (var i = 0; i < ACTIONS.length; i++) { 130 | this.client.subscribe( 131 | [this.config.prefix, CLIENTS_PREFIX, this.clientID, ACTIONS[i], '#'].join( 132 | '/' 133 | ) 134 | ) 135 | } 136 | 137 | this.emit('brokerStatus', true) 138 | 139 | // Update client status 140 | this.updateClientStatus(true) 141 | 142 | this.toSubscribe = [] 143 | } 144 | 145 | /** 146 | * Function called when MQTT client reconnects 147 | */ 148 | function onReconnect () { 149 | debug('MQTT client reconnecting') 150 | } 151 | 152 | /** 153 | * Function called when MQTT client reconnects 154 | */ 155 | function onError (error) { 156 | debug(error.message) 157 | this.error = error.message 158 | } 159 | 160 | /** 161 | * Function called when MQTT client go offline 162 | */ 163 | function onOffline () { 164 | debug('MQTT client offline') 165 | this.emit('brokerStatus', false) 166 | } 167 | 168 | /** 169 | * Function called when MQTT client is closed 170 | */ 171 | function onClose () { 172 | debug('MQTT client closed') 173 | } 174 | 175 | /** 176 | * Function called when an MQTT message is received 177 | */ 178 | function onMessageReceived (topic, payload) { 179 | debug('Message received on', topic) 180 | 181 | if (topic === HASS_WILL) { 182 | this.emit('hassStatus', payload.toString().toLowerCase() === 'online') 183 | return 184 | } 185 | 186 | // remove prefix 187 | topic = topic.substring(this.config.prefix.length + 1) 188 | 189 | var parts = topic.split('/') 190 | 191 | // It's not a write request 192 | if (parts.pop() !== 'set') return 193 | 194 | if (this.closed) return 195 | 196 | if (isNaN(payload)) { 197 | try { 198 | payload = JSON.parse(payload) 199 | } catch (e) { 200 | payload = payload.toString() 201 | } 202 | } else payload = Number(payload) 203 | 204 | // It's an action 205 | if (parts[0] === CLIENTS_PREFIX) { 206 | if (parts.length < 3) return 207 | 208 | var action = ACTIONS.indexOf(parts[2]) 209 | 210 | switch (action) { 211 | case 0: // broadcast 212 | this.emit('broadcastRequest', parts.slice(3), payload) 213 | // publish back to give a feedback the action is received 214 | // same topic without /set suffix 215 | this.publish(parts.join('/'), payload) 216 | break 217 | case 1: // api 218 | this.emit('apiCall', parts.join('/'), parts[3], payload) 219 | break 220 | default: 221 | debug('Unknown action received', action, topic) 222 | } 223 | } else { 224 | // It's a write request on zwave network 225 | this.emit('writeRequest', parts, payload) 226 | } 227 | } // end onMessageReceived 228 | 229 | /** 230 | * Returns the topic used to send client and devices status updateStates 231 | * if name is null the client is the gateway itself 232 | */ 233 | MqttClient.prototype.getClientTopic = function (...devices) { 234 | var subTopic = '' 235 | 236 | for (var i = 0; i < devices.length; i++) { 237 | var name = this.cleanName(devices[i]) 238 | subTopic += '/' + DEVICES_PREFIX + '/' + name 239 | } 240 | 241 | return ( 242 | this.config.prefix + 243 | '/' + 244 | CLIENTS_PREFIX + 245 | '/' + 246 | this.clientID + 247 | subTopic + 248 | '/status' 249 | ) 250 | } 251 | 252 | MqttClient.prototype.cleanName = function (name) { 253 | if (!isNaN(name)) return name 254 | 255 | name = name.replace(/\s/g, '_') 256 | return name.replace(/[+*#\\.'`!?^=(),"%[\]:;{}]+/g, '') 257 | } 258 | 259 | /** 260 | * Method used to close clients connection, use this before destroy 261 | */ 262 | MqttClient.prototype.close = function () { 263 | return new Promise(resolve => { 264 | this.closed = true 265 | 266 | if (this.client) { 267 | this.client.end(!this.client.connected, () => resolve()) 268 | } else { 269 | resolve() 270 | } 271 | 272 | this.removeAllListeners() 273 | 274 | if (this.client) { 275 | this.client.removeAllListeners() 276 | } 277 | }) 278 | } 279 | 280 | /** 281 | * Method used to get status 282 | */ 283 | MqttClient.prototype.getStatus = function () { 284 | var status = {} 285 | 286 | status.status = this.client && this.client.connected 287 | status.error = this.error || 'Offline' 288 | status.config = this.config 289 | 290 | return status 291 | } 292 | 293 | /** 294 | * Method used to update client connection status 295 | */ 296 | MqttClient.prototype.updateClientStatus = function (connected, ...devices) { 297 | if (this.client) { 298 | this.client.publish( 299 | this.getClientTopic(...devices), 300 | JSON.stringify({ value: connected, time: Date.now() }), 301 | { retain: this.config.retain, qos: this.config.qos } 302 | ) 303 | } 304 | } 305 | 306 | /** 307 | * Method used to update client 308 | */ 309 | MqttClient.prototype.update = function (config) { 310 | this.close() 311 | 312 | debug('Restarting Mqtt Client after update...') 313 | 314 | init.call(this, config) 315 | } 316 | 317 | /** 318 | * Method used to subscribe tags for write requests 319 | */ 320 | MqttClient.prototype.subscribe = function (topic) { 321 | if (this.client && this.client.connected) { 322 | topic = this.config.prefix + '/' + topic + '/set' 323 | debug('Subscribing to %s', topic) 324 | this.client.subscribe(topic) 325 | } else { 326 | this.toSubscribe.push(topic) 327 | } 328 | } 329 | 330 | /** 331 | * Method used to publish an update 332 | */ 333 | MqttClient.prototype.publish = function (topic, data, options, prefix) { 334 | if (this.client) { 335 | options = options || { 336 | qos: this.config.qos, 337 | retain: this.config.retain 338 | } 339 | 340 | topic = (prefix || this.config.prefix) + '/' + topic 341 | 342 | this.client.publish(topic, JSON.stringify(data), options, function (err) { 343 | if (err) { 344 | debug('Error while publishing a value', err.message) 345 | } 346 | }) 347 | } // end if client 348 | } 349 | 350 | /** 351 | * Method used to get the topic with prefix/suffix 352 | */ 353 | MqttClient.prototype.getTopic = function (topic, set) { 354 | return this.config.prefix + '/' + topic + (set ? '/set' : '') 355 | } 356 | 357 | /** 358 | * Used to get client connection status 359 | */ 360 | Object.defineProperty(MqttClient.prototype, 'connected', { 361 | get: function () { 362 | return this.client && this.client.connected 363 | }, 364 | enumerable: true 365 | }) 366 | 367 | /** 368 | * The prefix to add to broadcast values 369 | */ 370 | Object.defineProperty(MqttClient.prototype, 'broadcastPrefix', { 371 | get: function () { 372 | return BROADCAST_PREFIX 373 | }, 374 | enumerable: true 375 | }) 376 | 377 | /** 378 | * The prefix to add to events 379 | */ 380 | Object.defineProperty(MqttClient.prototype, 'eventsPrefix', { 381 | get: function () { 382 | return EVENTS_PREFIX 383 | }, 384 | enumerable: true 385 | }) 386 | 387 | module.exports = MqttClient 388 | -------------------------------------------------------------------------------- /lib/debug.js: -------------------------------------------------------------------------------- 1 | var log = require('debug') 2 | var debug 3 | 4 | function init () { 5 | if (!process.env.DEBUG) { 6 | log.enable('z2m:*') 7 | } 8 | debug = log('z2m') 9 | 10 | debug.log = console.log.bind(console) 11 | } 12 | init() 13 | module.exports = function (namespace) { 14 | return debug.extend(namespace) 15 | } 16 | -------------------------------------------------------------------------------- /lib/jsonStore.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // eslint-disable-next-line one-var 4 | var jsonfile = require('jsonfile'), 5 | reqlib = require('app-root-path').require, 6 | storeDir = reqlib('config/app.js').storeDir, 7 | Promise = require('bluebird'), 8 | debug = reqlib('/lib/debug')('Store'), 9 | utils = reqlib('lib/utils.js') 10 | 11 | debug.color = 3 12 | 13 | function getFile (config) { 14 | return new Promise((resolve, reject) => { 15 | jsonfile.readFile(utils.joinPath(true, storeDir, config.file), function ( 16 | err, 17 | data 18 | ) { 19 | if (err && err.code !== 'ENOENT') { 20 | reject(err) 21 | } else { 22 | if (err && err.code === 'ENOENT') { 23 | debug(config.file, 'not found') 24 | } 25 | if (!data) { 26 | data = config.default 27 | } 28 | resolve({ file: config.file, data: data }) 29 | } 30 | }) 31 | }) 32 | } 33 | 34 | /** 35 | Constructor 36 | **/ 37 | function StorageHelper () { 38 | this.store = {} 39 | } 40 | 41 | StorageHelper.prototype.init = function (config) { 42 | return new Promise((resolve, reject) => { 43 | storage_helper.config = config 44 | Promise.map(Object.keys(config), function (model) { 45 | return getFile(config[model]) 46 | }) 47 | .then(results => { 48 | for (var i = 0; i < results.length; i++) { 49 | storage_helper.store[results[i].file] = results[i].data 50 | } 51 | resolve(storage_helper.store) 52 | }) 53 | .catch(err => reject(err)) 54 | }) 55 | } 56 | 57 | StorageHelper.prototype.get = function (model) { 58 | if (storage_helper.store[model.file]) { 59 | return storage_helper.store[model.file] 60 | } else { 61 | throw Error('Requested file not present in store: ' + model.file) 62 | } 63 | } 64 | 65 | StorageHelper.prototype.put = function (model, data) { 66 | return new Promise((resolve, reject) => { 67 | jsonfile.writeFile( 68 | utils.joinPath(true, storeDir, model.file), 69 | data, 70 | function (err) { 71 | if (err) { 72 | reject(err) 73 | } else { 74 | storage_helper.store[model.file] = data 75 | resolve(storage_helper.store[model.file]) 76 | } 77 | } 78 | ) 79 | }) 80 | } 81 | 82 | // eslint-disable-next-line camelcase 83 | var storage_helper = (module.exports = new StorageHelper()) 84 | -------------------------------------------------------------------------------- /lib/renderIndex.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const path = require('path') 3 | const reqlib = require('app-root-path').require 4 | 5 | const webConfig = reqlib('/config/webConfig') 6 | 7 | function findFiles (folder, ext) { 8 | const folderPath = path.join(__dirname, '..', 'dist', folder) 9 | const folderFiles = fs.readdirSync(folderPath) 10 | return folderFiles 11 | .filter(function (file) { 12 | return path.extname(file).toLowerCase() === `.${ext.toLowerCase()}` 13 | }) 14 | .map(function (file) { 15 | return path.join(folder, file) 16 | }) 17 | } 18 | 19 | let cssFiles 20 | let jsFiles 21 | 22 | function basePath (config, headers) { 23 | return (headers['x-external-path'] || config.base).replace(/\/?$/, '/') 24 | } 25 | 26 | module.exports = function (req, res) { 27 | cssFiles = cssFiles || findFiles(path.join('static', 'css'), 'css') 28 | jsFiles = jsFiles || findFiles(path.join('static', 'js'), 'js') 29 | res.render('index.ejs', { 30 | config: { 31 | ...webConfig, 32 | base: basePath(webConfig, req.headers) 33 | }, 34 | cssFiles, 35 | jsFiles 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line one-var 2 | var appRoot = require('app-root-path'), 3 | path = require('path') 4 | 5 | module.exports = { 6 | getPath (write) { 7 | if (write && process.pkg) return process.cwd() 8 | else return appRoot.toString() 9 | }, 10 | joinPath (...paths) { 11 | if (paths.length > 0 && typeof paths[0] === 'boolean') { 12 | paths[0] = this.getPath(paths[0]) 13 | } 14 | return path.join(...paths) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zwave2mqtt", 3 | "version": "4.1.1", 4 | "bin": { 5 | "zwave2mqtt": "bin/www" 6 | }, 7 | "description": "Zwave To MQTT Gateway", 8 | "author": "Daniel Lando ", 9 | "private": false, 10 | "license": "MIT", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/OpenZWave/Zwave2Mqtt" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/OpenZWave/Zwave2Mqtt/issues" 17 | }, 18 | "keywords": [ 19 | "mqtt", 20 | "zwave", 21 | "openzwave", 22 | "open-zwave", 23 | "control-panel", 24 | "gateway", 25 | "hass", 26 | "homeassistant", 27 | "iot", 28 | "nodejs", 29 | "vue", 30 | "vuetify" 31 | ], 32 | "pkg": { 33 | "scripts": [ 34 | "lib/**/*.js", 35 | "config/**/*.js", 36 | "hass/**/*.js", 37 | "app.js" 38 | ], 39 | "assets": [ 40 | "views/**/*", 41 | "static/**/*", 42 | "dist/**/*" 43 | ] 44 | }, 45 | "scripts": { 46 | "dev": "webpack-dev-server --inline --progress --host 0.0.0.0 --config build/webpack.dev.conf.js", 47 | "dev:server": "nodemon bin/www", 48 | "start": "node bin/www", 49 | "lint": "npm-run-all lint:*", 50 | "lint-fix": "npm-run-all lint-fix:*", 51 | "lint:eslint": "eslint --ext .js,.vue .", 52 | "lint-fix:eslint": "eslint --fix --ext .js,.vue .", 53 | "lint:markdownlint": "markdownlint '**/*.md'", 54 | "lint-fix:markdownlint": "markdownlint --fix '**/*.md'", 55 | "lint:prettier": "prettier-standard --check", 56 | "lint-fix:prettier": "prettier-standard --format", 57 | "test": "mocha", 58 | "coverage": "c8 --clean mocha", 59 | "record-coverage": "c8 report --reporter=text-lcov --all --exclude=test/* --exclude=.git --exclude=.eslintrc.js --exclude=.postcssrc.js --exclude=wallaby.js > ./coverage/lcov.info", 60 | "test-watch-cover": "nodemon --exec c8 npm test", 61 | "test-watch": "nodemon --exec npm test", 62 | "build": "node build/build.js", 63 | "pkg": "sudo chmod +x package.sh && ./package.sh", 64 | "changelog": "auto-changelog -p && git add CHANGELOG.md", 65 | "release": "read -p 'GITHUB_TOKEN: ' GITHUB_TOKEN && export GITHUB_TOKEN=$GITHUB_TOKEN && release-it" 66 | }, 67 | "release-it": { 68 | "github": { 69 | "release": true, 70 | "assets": [ 71 | "pkg/*.zip" 72 | ] 73 | }, 74 | "git": { 75 | "tagName": "v${version}" 76 | }, 77 | "hooks": { 78 | "after:bump": "npm run changelog", 79 | "before:release": "./package.sh --release" 80 | }, 81 | "npm": { 82 | "publish": false 83 | } 84 | }, 85 | "auto-changelog": { 86 | "unreleased": true, 87 | "commitLimit": false, 88 | "replaceText": { 89 | "^-[\\s]*": "" 90 | } 91 | }, 92 | "dependencies": { 93 | "@babel/polyfill": "^7.12.1", 94 | "ansi_up": "^4.0.4", 95 | "app-root-path": "^3.0.0", 96 | "axios": "^0.20.0", 97 | "axios-progress-bar": "^1.2.0", 98 | "bluebird": "^3.7.2", 99 | "body-parser": "~1.19.0", 100 | "connect-history-api-fallback": "^1.6.0", 101 | "cookie-parser": "^1.4.5", 102 | "cors": "^2.8.5", 103 | "debug": "^4.2.0", 104 | "ejs": "^3.1.5", 105 | "express": "^4.17.1", 106 | "jsonfile": "^6.0.1", 107 | "morgan": "~1.10.0", 108 | "mqtt": "^4.2.1", 109 | "mqtt-nedb-store": "^0.1.1", 110 | "native-url": "^0.3.4", 111 | "nedb": "^1.8.0", 112 | "openzwave-shared": "^1.7.1", 113 | "prismjs": "^1.22.0", 114 | "serialport": "^9.0.2", 115 | "serve-favicon": "^2.5.0", 116 | "socket.io": "^2.3.0", 117 | "socket.io-client": "^2.3.1", 118 | "tail": "^2.0.4", 119 | "uniqid": "^5.2.0", 120 | "vue": "^2.6.12", 121 | "vue-d3-network": "^0.1.28", 122 | "vue-prism-editor": "^1.2.2", 123 | "vue-router": "^3.4.3", 124 | "vuetify": "^2.3.10", 125 | "vuex": "^3.5.1" 126 | }, 127 | "devDependencies": { 128 | "@babel/core": "^7.11.5", 129 | "@babel/plugin-proposal-class-properties": "^7.10.4", 130 | "@babel/plugin-proposal-decorators": "^7.10.5", 131 | "@babel/plugin-proposal-export-namespace-from": "^7.10.4", 132 | "@babel/plugin-proposal-function-sent": "^7.10.4", 133 | "@babel/plugin-proposal-json-strings": "^7.10.4", 134 | "@babel/plugin-proposal-numeric-separator": "^7.10.4", 135 | "@babel/plugin-proposal-throw-expressions": "^7.10.4", 136 | "@babel/plugin-syntax-dynamic-import": "^7.8.3", 137 | "@babel/plugin-syntax-import-meta": "^7.10.4", 138 | "@babel/plugin-syntax-jsx": "^7.10.4", 139 | "@babel/plugin-transform-runtime": "^7.11.5", 140 | "@babel/preset-env": "^7.11.5", 141 | "auto-changelog": "^2.2.0", 142 | "autoprefixer": "^9.8.6", 143 | "babel-eslint": "^10.1.0", 144 | "babel-helper-vue-jsx-merge-props": "^2.0.3", 145 | "babel-loader": "^8.1.0", 146 | "babel-plugin-syntax-jsx": "^6.18.0", 147 | "babel-plugin-transform-vue-jsx": "^3.7.0", 148 | "c8": "^7.3.0", 149 | "chai": "^4.2.0", 150 | "chai-as-promised": "^7.1.1", 151 | "chalk": "^4.1.0", 152 | "copy-webpack-plugin": "^6.1.0", 153 | "css-loader": "^4.2.2", 154 | "deepmerge": "^4.2.2", 155 | "eslint": "^7.8.1", 156 | "eslint-config-standard": "^14.1.1", 157 | "eslint-friendly-formatter": "^4.0.1", 158 | "eslint-loader": "^4.0.2", 159 | "eslint-plugin-babel": "^5.3.1", 160 | "eslint-plugin-import": "^2.22.0", 161 | "eslint-plugin-node": "^11.1.0", 162 | "eslint-plugin-promise": "^4.2.1", 163 | "eslint-plugin-standard": "^4.0.1", 164 | "eslint-plugin-vue": "^6.2.2", 165 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 166 | "fibers": "^5.0.0", 167 | "file-loader": "^6.1.0", 168 | "friendly-errors-webpack-plugin": "^1.7.0", 169 | "html-webpack-plugin": "^4.5.0", 170 | "lodash": "^4.17.20", 171 | "markdownlint-cli": "^0.23.2", 172 | "material-design-icons-iconfont": "^6.0.1", 173 | "mini-css-extract-plugin": "^0.11.0", 174 | "mocha": "^8.1.3", 175 | "node-notifier": "^8.0.0", 176 | "nodemon": "^2.0.4", 177 | "npm-run-all": "^4.1.5", 178 | "optimize-css-assets-webpack-plugin": "^5.0.4", 179 | "ora": "^5.1.0", 180 | "portfinder": "^1.0.28", 181 | "postcss-import": "^12.0.1", 182 | "postcss-loader": "^3.0.0", 183 | "postcss-url": "^8.0.0", 184 | "prettier-standard": "^16.4.1", 185 | "proxyquire": "^2.1.3", 186 | "release-it": "^14.0.3", 187 | "rewire": "^5.0.0", 188 | "rimraf": "^3.0.2", 189 | "sass": "^1.26.10", 190 | "sass-loader": "^10.0.1", 191 | "semver": "^7.3.2", 192 | "shelljs": "^0.8.4", 193 | "sinon": "^9.0.3", 194 | "sinon-chai": "^3.5.0", 195 | "terser-webpack-plugin": "^4.1.0", 196 | "url-loader": "^4.1.0", 197 | "vue-loader": "^15.9.3", 198 | "vue-style-loader": "^4.1.2", 199 | "vue-template-compiler": "^2.6.12", 200 | "webpack": "^4.44.1", 201 | "webpack-bundle-analyzer": "^3.8.0", 202 | "webpack-cli": "^3.3.12", 203 | "webpack-dev-server": "^3.11.0", 204 | "webpack-merge": "^5.1.3" 205 | }, 206 | "engines": { 207 | "node": ">= 6.0.0", 208 | "npm": ">= 3.0.0" 209 | }, 210 | "browserslist": [ 211 | "> 1%", 212 | "last 2 versions", 213 | "not ie <= 8" 214 | ] 215 | } 216 | -------------------------------------------------------------------------------- /package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | ask() { 5 | # http://djm.me/ask 6 | while true; do 7 | 8 | if [ "${2:-}" = "Y" ]; then 9 | prompt="Y/n" 10 | default=Y 11 | elif [ "${2:-}" = "N" ]; then 12 | prompt="y/N" 13 | default=N 14 | else 15 | prompt="y/n" 16 | default= 17 | fi 18 | 19 | # Ask the question 20 | read -p "$1 [$prompt] " REPLY 21 | 22 | # Default? 23 | if [ -z "$REPLY" ]; then 24 | REPLY=$default 25 | fi 26 | 27 | # Check if the reply is valid 28 | case "$REPLY" in 29 | Y*|y*) return 0 ;; 30 | N*|n*) return 1 ;; 31 | esac 32 | 33 | done 34 | } 35 | 36 | APP="zwave2mqtt" 37 | PKG_FOLDER="pkg" 38 | 39 | echo "Destination folder: $PKG_FOLDER" 40 | echo "App-name: $APP" 41 | 42 | VERSION=$(node -p "require('./package.json').version") 43 | echo "Version: $VERSION" 44 | 45 | NODE_MAJOR=$(node -v | egrep -o '[0-9].' | head -n 1) 46 | 47 | echo "## Clear $PKG_FOLDER folder" 48 | rm -rf $PKG_FOLDER/* 49 | 50 | 51 | if [ ! -z "$1" ]; then 52 | echo "## Building application..." 53 | echo '' 54 | npm run build 55 | echo "Executing command: pkg package.json -t node12-linux-x64 --out-path $PKG_FOLDER" 56 | pkg package.json -t node12-linux-x64 --out-path $PKG_FOLDER 57 | else 58 | 59 | if ask "Re-build $APP?"; then 60 | echo "## Building application" 61 | npm run build 62 | fi 63 | 64 | echo '###################################################' 65 | echo '## Choose architecture to build' 66 | echo '###################################################' 67 | echo ' ' 68 | echo 'Your architecture is' $(arch) 69 | PS3="Architecture: >" 70 | options=( 71 | "x64" 72 | "armv7" 73 | "armv6" 74 | "x86" 75 | "alpine" 76 | ) 77 | echo '' 78 | select option in "${options[@]}"; do 79 | case "$REPLY" in 80 | 1) 81 | echo "## Creating application package in $PKG_FOLDER folder" 82 | pkg package.json -t node$NODE_MAJOR-linux-x64 --out-path $PKG_FOLDER 83 | break 84 | ;; 85 | 2) 86 | echo "## Creating application package in $PKG_FOLDER folder" 87 | pkg package.json -t node$NODE_MAJOR-linux-armv7 --out-path $PKG_FOLDER --public-packages=* 88 | break 89 | ;; 90 | 3) 91 | echo "## Creating application package in $PKG_FOLDER folder" 92 | pkg package.json -t node$NODE_MAJOR-linux-armv6 --out-path $PKG_FOLDER --public-packages=* 93 | break 94 | ;; 95 | 4) 96 | echo "## Creating application package in $PKG_FOLDER folder" 97 | pkg package.json -t node$NODE_MAJOR-linux-x86 --out-path $PKG_FOLDER 98 | break 99 | ;; 100 | 5) 101 | echo "## Creating application package in $PKG_FOLDER folder" 102 | pkg package.json -t node$NODE_MAJOR-alpine-x64 --out-path $PKG_FOLDER 103 | break 104 | ;; 105 | *) 106 | echo '####################' 107 | echo '## Invalid option ##' 108 | echo '####################' 109 | exit 110 | esac 111 | done 112 | fi 113 | 114 | echo "## Check for .node files to include in executable folder" 115 | mapfile -t TO_INCLUDE < <(find ./node_modules/ -type f -name "*.node" | grep -v obj.target) 116 | 117 | TOTAL_INCLUDE=${#TO_INCLUDE[@]} 118 | 119 | echo "## Found $TOTAL_INCLUDE files to include" 120 | 121 | i=0 122 | 123 | while [ "$i" -lt "$TOTAL_INCLUDE" ] 124 | do 125 | IFS='/' path=(${TO_INCLUDE[$i]}) 126 | file=${path[-1]} 127 | echo "## Copying $file to $PKG_FOLDER folder" 128 | cp "${TO_INCLUDE[$i]}" "./$PKG_FOLDER" 129 | let "i = $i + 1" 130 | done 131 | 132 | echo "## Create folders needed" 133 | cd $PKG_FOLDER 134 | mkdir store -p 135 | echo "## Create zip file $APP-v$VERSION" 136 | zip -r $APP-v$VERSION.zip * 137 | -------------------------------------------------------------------------------- /pkg/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/pkg/.gitkeep -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px 'Lucida Grande', Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00b7ff; 8 | } 9 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 90 | 91 | 267 | -------------------------------------------------------------------------------- /src/apis/ConfigApis.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { loadProgressBar } from 'axios-progress-bar' 3 | 4 | function getBasePath () { 5 | return document.baseURI.replace(/\/$/, '') 6 | } 7 | 8 | axios.defaults.socketUrl = getBasePath() 9 | axios.defaults.baseURL = `${axios.defaults.socketUrl}/api` 10 | 11 | loadProgressBar() 12 | 13 | export default { 14 | getBasePath () { 15 | return getBasePath() 16 | }, 17 | getSocketPath () { 18 | const innerPath = document.baseURI 19 | .split('/') 20 | .splice(3) 21 | .join('/') 22 | const socketPath = `/${innerPath}/socket.io`.replace('//', '/') 23 | return socketPath === '/socket.io' ? undefined : socketPath 24 | }, 25 | getConfig () { 26 | return axios.get('/settings').then(response => { 27 | return response.data 28 | }) 29 | }, 30 | updateConfig (data) { 31 | return axios.post('/settings', data).then(response => { 32 | return response.data 33 | }) 34 | }, 35 | exportConfig () { 36 | return axios.get('/exportConfig').then(response => { 37 | return response.data 38 | }) 39 | }, 40 | importConfig (data) { 41 | return axios.post('/importConfig', data).then(response => { 42 | return response.data 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/assets/css/my-mesh.css: -------------------------------------------------------------------------------- 1 | .node.controller { 2 | stroke: #9c27b0; 3 | fill: #e1bee7; 4 | } 5 | 6 | .node.sleep { 7 | stroke: #ffeb3b; 8 | fill: #fff9c4; 9 | } 10 | 11 | .node.alive { 12 | stroke: #4caf50; 13 | fill: #c8e6c9; 14 | } 15 | 16 | .node.failed, 17 | .node.removed, 18 | .node.initializing { 19 | stroke: #9e9e9e; 20 | fill: #f5f5f5; 21 | } 22 | 23 | .node.dead { 24 | stroke: #f44336; 25 | fill: #ffcdd2; 26 | } 27 | 28 | .node.selected { 29 | stroke: #1976d2; 30 | fill: #03a9f4; 31 | } 32 | 33 | .node-label { 34 | fill: #1976d2; 35 | } 36 | 37 | .details { 38 | position: absolute; 39 | top: 150px; 40 | left: 30px; 41 | background: #ccccccaa; 42 | border: 2px solid black; 43 | border-radius: 20px; 44 | } 45 | -------------------------------------------------------------------------------- /src/assets/css/my-progress.css: -------------------------------------------------------------------------------- 1 | #nprogress .spinner { 2 | right: 50% !important; 3 | } 4 | 5 | ::-webkit-scrollbar { 6 | height: 5px; 7 | width: 4px; 8 | background: transparent; 9 | padding-right: 10; 10 | } 11 | 12 | ::-webkit-scrollbar-thumb { 13 | background: rgba(255, 255, 255, 0.05); 14 | border-radius: 1ex; 15 | -webkit-border-radius: 1ex; 16 | } 17 | 18 | ::-webkit-scrollbar-corner { 19 | background: transparent; 20 | } 21 | 22 | /* Hacky hack. Remove scrollbars for 320 width screens */ 23 | @media screen and (max-width: 320px) { 24 | body::-webkit-scrollbar { 25 | display: none; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/src/assets/logo.png -------------------------------------------------------------------------------- /src/components/Confirm.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 99 | -------------------------------------------------------------------------------- /src/components/Mesh.vue: -------------------------------------------------------------------------------- 1 | 120 | 375 | -------------------------------------------------------------------------------- /src/components/ValueId.vue: -------------------------------------------------------------------------------- 1 | 95 | 96 | 114 | -------------------------------------------------------------------------------- /src/components/custom/file-input.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 116 | -------------------------------------------------------------------------------- /src/components/dialogs/DialogGatewayValue.vue: -------------------------------------------------------------------------------- 1 | 179 | 180 | 289 | 290 | 296 | -------------------------------------------------------------------------------- /src/components/dialogs/DialogSceneValue.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 104 | -------------------------------------------------------------------------------- /src/components/nodes-table/filter-options.vue: -------------------------------------------------------------------------------- 1 | 96 | 97 | 146 | -------------------------------------------------------------------------------- /src/components/nodes-table/index.vue: -------------------------------------------------------------------------------- 1 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /src/components/nodes-table/nodes-table.css: -------------------------------------------------------------------------------- 1 | .td-large { 2 | text-overflow: ellipsis; 3 | white-space: nowrap; 4 | overflow-x: hidden; 5 | max-width: 20em; 6 | } 7 | 8 | .td-medium { 9 | text-overflow: ellipsis; 10 | white-space: nowrap; 11 | overflow-x: hidden; 12 | max-width: 15em; 13 | } 14 | 15 | .td-small { 16 | text-overflow: ellipsis; 17 | white-space: nowrap; 18 | overflow-x: hidden; 19 | max-width: 10em; 20 | } 21 | 22 | .v-chip { 23 | text-overflow: ellipsis; 24 | white-space: nowrap; 25 | overflow-x: hidden; 26 | } 27 | -------------------------------------------------------------------------------- /src/components/nodes-table/nodes-table.js: -------------------------------------------------------------------------------- 1 | import { NodeCollection } from '@/modules/NodeCollection' 2 | import { Settings } from '@/modules/Settings' 3 | import filterOptions from '@/components/nodes-table/filter-options.vue' 4 | 5 | export default { 6 | props: { 7 | nodes: Array, 8 | showHidden: Boolean 9 | }, 10 | components: { 11 | filterOptions 12 | }, 13 | data: () => ({ 14 | settings: new Settings(localStorage), 15 | nodeTableItems: undefined, 16 | selectedNode: undefined, 17 | filters: {}, 18 | sorting: {}, 19 | headers: [ 20 | { text: 'ID', value: 'node_id' }, 21 | { text: 'Type', value: 'type' }, 22 | { text: 'Product', value: 'product' }, 23 | { text: 'Name', value: 'name' }, 24 | { text: 'Location', value: 'loc' }, 25 | { text: 'Secure', value: 'secure' }, 26 | { text: 'Status', value: 'status' }, 27 | { text: 'Last Active', value: 'lastActive' } 28 | ] 29 | }), 30 | methods: { 31 | initFilters () { 32 | return { 33 | ids: { type: 'number' }, 34 | types: { type: 'string' }, 35 | products: { type: 'string' }, 36 | names: { type: 'string' }, 37 | locations: { type: 'string' }, 38 | secures: { type: 'boolean' }, 39 | states: { type: 'string' }, 40 | lastActives: { type: 'date' } 41 | } 42 | }, 43 | initSorting () { 44 | return { 45 | by: ['node_id'], 46 | desc: [false] 47 | } 48 | }, 49 | loadSetting (key, defaultVal) { 50 | return this.settings.load(key, defaultVal) 51 | }, 52 | storeSetting (key, val) { 53 | this.settings.store(key, val) 54 | }, 55 | resetFilter () { 56 | this.filters = this.initFilters() 57 | }, 58 | nodeSelected (node) { 59 | this.selectedNode = node 60 | this.$emit('node-selected', { node }) 61 | }, 62 | productName (node) { 63 | const manufacturer = node.manufacturer ? ` (${node.manufacturer})` : '' 64 | return node.ready ? `${node.product}${manufacturer}` : '' 65 | } 66 | }, 67 | mounted () { 68 | this.filters = this.loadSetting('nodes_filters', this.initFilters()) 69 | this.sorting = this.loadSetting('nodes_sorting', this.initSorting()) 70 | this.nodeTableItems = this.loadSetting('nodes_itemsPerPage', 10) 71 | }, 72 | watch: { 73 | nodeTableItems (val) { 74 | this.storeSetting('nodes_itemsPerPage', val) 75 | }, 76 | filters: { 77 | handler (val) { 78 | this.storeSetting('nodes_filters', val) 79 | }, 80 | deep: true 81 | }, 82 | sorting: { 83 | handler (val) { 84 | this.storeSetting('nodes_sorting', val) 85 | }, 86 | deep: true 87 | } 88 | }, 89 | computed: { 90 | nodeCollection () { 91 | return new NodeCollection(this.nodes) 92 | }, 93 | relevantNodes () { 94 | return this.nodeCollection.filter('failed', failed => { 95 | return this.showHidden ? true : !failed 96 | }) 97 | }, 98 | filteredNodes () { 99 | return this.relevantNodes 100 | .betweenNumber( 101 | 'node_id', 102 | this.filters.ids ? this.filters.ids.min : null, 103 | this.filters.ids ? this.filters.ids.max : null 104 | ) 105 | .betweenDate( 106 | 'lastActive', 107 | this.filters.lastActives ? this.filters.lastActives.min : null, 108 | this.filters.lastActives ? this.filters.lastActives.max : null 109 | ) 110 | 111 | .contains( 112 | ['product', 'manufacturer'], 113 | this.filters.products ? this.filters.products.search : '' 114 | ) 115 | .contains(['type'], this.filters.types ? this.filters.types.search : '') 116 | .contains(['name'], this.filters.names ? this.filters.names.search : '') 117 | .contains( 118 | ['loc'], 119 | this.filters.locations ? this.filters.locations.search : '' 120 | ) 121 | .contains( 122 | ['status'], 123 | this.filters.states ? this.filters.states.search : '' 124 | ) 125 | 126 | .equalsAny( 127 | 'node_id', 128 | this.filters.ids 129 | ? this.filters.ids.selections 130 | ? this.filters.ids.selections 131 | : [] 132 | : [] 133 | ) 134 | .equalsAny( 135 | 'type', 136 | this.filters.types 137 | ? this.filters.types.selections 138 | ? this.filters.types.selections 139 | : [] 140 | : [] 141 | ) 142 | .equalsAny( 143 | 'product', 144 | this.filters.products 145 | ? this.filters.products.selections 146 | ? this.filters.products.selections 147 | : [] 148 | : [] 149 | ) 150 | .equalsAny( 151 | 'name', 152 | this.filters.names 153 | ? this.filters.names.selections 154 | ? this.filters.names.selections 155 | : [] 156 | : [] 157 | ) 158 | .equalsAny( 159 | 'loc', 160 | this.filters.locations 161 | ? this.filters.locations.selections 162 | ? this.filters.locations.selections 163 | : [] 164 | : [] 165 | ) 166 | .equalsAny( 167 | 'status', 168 | this.filters.states 169 | ? this.filters.states.selections 170 | ? this.filters.states.selections 171 | : [] 172 | : [] 173 | ) 174 | 175 | .equals( 176 | 'secure', 177 | this.filters.secures ? this.filters.secures.bool : null 178 | ) 179 | }, 180 | tableNodes () { 181 | return this.filteredNodes.nodes 182 | }, 183 | ids () { 184 | return this.relevantNodes.values('node_id') 185 | }, 186 | products () { 187 | return this.relevantNodes.values('product') 188 | }, 189 | names () { 190 | return this.relevantNodes.values('name') 191 | }, 192 | locations () { 193 | return this.relevantNodes.values('loc') 194 | }, 195 | secures () { 196 | return [undefined, false, true] 197 | }, 198 | states () { 199 | return this.relevantNodes.values('status') 200 | }, 201 | types () { 202 | return this.relevantNodes.values('type') 203 | }, 204 | lastActives () { 205 | return this.relevantNodes.values('lastActive') 206 | } 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import '@babel/polyfill' 4 | 5 | import Vue from 'vue' 6 | import App from './App' 7 | import router from './router' 8 | import store from './store' 9 | import vuetify from '@/plugins/vuetify' // path to vuetify export 10 | 11 | import 'axios-progress-bar/dist/nprogress.css' 12 | import 'vue-d3-network/dist/vue-d3-network.css' 13 | 14 | // Custom assets CSS JS 15 | require('./assets/css/my-progress.css') 16 | require('./assets/css/my-mesh.css') 17 | 18 | Vue.config.productionTip = false 19 | Vue.config.devtools = true 20 | 21 | /* eslint-disable no-new */ 22 | new Vue({ 23 | vuetify, 24 | router, 25 | store, 26 | components: { App }, 27 | template: '' 28 | }).$mount('#app') 29 | -------------------------------------------------------------------------------- /src/modules/NodeCollection.js: -------------------------------------------------------------------------------- 1 | export class NodeCollection { 2 | constructor (nodes) { 3 | this.nodes = nodes 4 | } 5 | 6 | _isUndefined (value) { 7 | return value === undefined || value === null || value === '' 8 | } 9 | 10 | _strValue (str, caseSensitive) { 11 | return caseSensitive ? `${str}` : `${str}`.toLowerCase() 12 | } 13 | 14 | _createStringFilter (filterValue, caseSensitive) { 15 | if (this._isUndefined(filterValue)) { 16 | filterValue = '' 17 | } 18 | const strFilter = this._strValue(filterValue, caseSensitive) 19 | return value => this._strValue(value, caseSensitive).indexOf(strFilter) >= 0 20 | } 21 | 22 | _filterByProps (node, properties, filter) { 23 | const mergedProps = [properties].reduce( 24 | (merged, prop) => merged.concat(prop), 25 | [] 26 | ) 27 | return mergedProps.find(prop => filter(node[prop])) 28 | } 29 | 30 | filter (properties, filter) { 31 | const filtered = this.nodes.filter(node => 32 | this._filterByProps(node, properties, filter) 33 | ) 34 | return new NodeCollection(filtered) 35 | } 36 | 37 | contains (properties, value, caseSensitive = false) { 38 | return this.filter( 39 | properties, 40 | this._createStringFilter(value, caseSensitive) 41 | ) 42 | } 43 | 44 | equals (properties, value) { 45 | return this.filter( 46 | properties, 47 | nodeValue => this._isUndefined(value) || value === nodeValue 48 | ) 49 | } 50 | 51 | betweenNumber (properties, minValue, maxValue) { 52 | return this.filter( 53 | properties, 54 | nodeValue => 55 | (this._isUndefined(minValue) || minValue <= nodeValue) && 56 | (this._isUndefined(maxValue) || maxValue >= nodeValue) 57 | ) 58 | } 59 | 60 | betweenDate (properties, minValue, maxValue) { 61 | return this.filter(properties, nodeValue => { 62 | const nodeValueTime = new Date(nodeValue).getTime() 63 | return ( 64 | (this._isUndefined(minValue) || 65 | new Date(minValue).getTime() <= nodeValueTime) && 66 | (this._isUndefined(maxValue) || 67 | new Date(maxValue).getTime() >= nodeValueTime) 68 | ) 69 | }) 70 | } 71 | 72 | equalsAny (properties, values) { 73 | return this.filter( 74 | properties, 75 | nodeValue => values.length === 0 || values.indexOf(nodeValue) >= 0 76 | ) 77 | } 78 | 79 | values (property) { 80 | const uniqueMap = {} 81 | this.nodes.forEach(node => { 82 | const strVal = this._strValue(node[property]) 83 | uniqueMap[strVal] = uniqueMap[strVal] || node[property] 84 | }) 85 | return Object.keys(uniqueMap) 86 | .sort() 87 | .map(key => uniqueMap[key]) 88 | } 89 | } 90 | 91 | export default NodeCollection 92 | -------------------------------------------------------------------------------- /src/modules/NodeCollection.test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import { NodeCollection } from './NodeCollection' 3 | 4 | describe('NodeCollection', () => { 5 | describe('#constructor', () => { 6 | it('uses the nodes passed in as the collection nodes', () => { 7 | const collection = new NodeCollection([{ id: 1 }]) 8 | chai.expect(collection.nodes).to.eql([{ id: 1 }]) 9 | }) 10 | }) 11 | describe('#filter', () => { 12 | const isOdd = num => num % 2 13 | it('returns nodes with the property matching the filter', () => { 14 | const collection = new NodeCollection([ 15 | { id: 1 }, 16 | { id: 2 }, 17 | { id: 3 }, 18 | { id: 4 } 19 | ]) 20 | const filtered = collection.filter('id', isOdd) 21 | chai.expect(filtered.nodes).to.eql([{ id: 1 }, { id: 3 }]) 22 | }) 23 | it('returns nodes with any of the properties matching the filter', () => { 24 | const collection = new NodeCollection([ 25 | { id: 1, value: 2 }, 26 | { id: 2, value: 1 }, 27 | { id: 3, value: 2 }, 28 | { id: 4, value: 2 } 29 | ]) 30 | const filtered = collection.filter(['id', 'value'], isOdd) 31 | chai.expect(filtered.nodes).to.eql([ 32 | { id: 1, value: 2 }, 33 | { id: 2, value: 1 }, 34 | { id: 3, value: 2 } 35 | ]) 36 | }) 37 | }) 38 | describe('#contains', () => { 39 | const stringCollection = new NodeCollection([ 40 | { id: 'pippo' }, 41 | { id: 'paRanza' }, 42 | { id: 'PipPo' }, 43 | { id: 'Ames' } 44 | ]) 45 | 46 | it('returns nodes with the properties containing the value', () => { 47 | const collection = new NodeCollection([ 48 | { id: 100 }, 49 | { id: '210' }, 50 | { id: 20 }, 51 | { id: '300' } 52 | ]) 53 | const filtered = collection.contains('id', '10') 54 | chai.expect(filtered.nodes).to.eql([{ id: 100 }, { id: '210' }]) 55 | }) 56 | it('matches values over multiple properties', () => { 57 | const collection = new NodeCollection([ 58 | { id: 100, name: 'sample' }, 59 | { id: '210', name: 'trinity' }, 60 | { id: 20, name: '10 packs' }, 61 | { id: '300', name: 'fazuoli' } 62 | ]) 63 | const filtered = collection.contains(['id', 'name'], '10') 64 | chai.expect(filtered.nodes).to.eql([ 65 | { id: 100, name: 'sample' }, 66 | { id: '210', name: 'trinity' }, 67 | { id: 20, name: '10 packs' } 68 | ]) 69 | }) 70 | it('is case insensitive by default', () => { 71 | const filtered = stringCollection.contains('id', 'piPPo') 72 | chai.expect(filtered.nodes).to.eql([{ id: 'pippo' }, { id: 'PipPo' }]) 73 | }) 74 | it('accepts a case sensitive flag', () => { 75 | const filtered = stringCollection.contains('id', 'PipPo', true) 76 | chai.expect(filtered.nodes).to.eql([{ id: 'PipPo' }]) 77 | }) 78 | }) 79 | describe('#equals', () => { 80 | it('returns nodes with the properties with equal value', () => { 81 | const collection = new NodeCollection([ 82 | { id: 10 }, 83 | { id: '10' }, 84 | { id: 20 }, 85 | { id: '20' } 86 | ]) 87 | const filtered = collection.equals('id', 10) 88 | chai.expect(filtered.nodes).to.eql([{ id: 10 }]) 89 | }) 90 | it('works over multiple properties', () => { 91 | const collection = new NodeCollection([ 92 | { id: 10, sample: '20' }, 93 | { id: '10', sample: 30 }, 94 | { id: 20, sample: '10' }, 95 | { id: '20', sample: 10 } 96 | ]) 97 | const filtered = collection.equals(['id', 'sample'], 10) 98 | chai.expect(filtered.nodes).to.eql([ 99 | { id: 10, sample: '20' }, 100 | { id: '20', sample: 10 } 101 | ]) 102 | }) 103 | }) 104 | describe('#equalsAny', () => { 105 | it('returns all nodes when values has no elements', () => { 106 | const collection = new NodeCollection([ 107 | { id: 10 }, 108 | { id: '10' }, 109 | { id: 20 }, 110 | { id: '20' } 111 | ]) 112 | const filtered = collection.equalsAny('id', []) 113 | chai 114 | .expect(filtered.nodes) 115 | .to.eql([{ id: 10 }, { id: '10' }, { id: 20 }, { id: '20' }]) 116 | }) 117 | it('returns nodes with the properties equal to any of the values', () => { 118 | const collection = new NodeCollection([ 119 | { id: 10 }, 120 | { id: '10' }, 121 | { id: 20 }, 122 | { id: '20' } 123 | ]) 124 | const filtered = collection.equalsAny('id', [10, '20']) 125 | chai.expect(filtered.nodes).to.eql([{ id: 10 }, { id: '20' }]) 126 | }) 127 | it('works over multiple properties', () => { 128 | const collection = new NodeCollection([ 129 | { id: 10, sample: 20 }, 130 | { id: '10', sample: '20' }, 131 | { id: 20, sample: '10' }, 132 | { id: '20', sample: 'zdub' } 133 | ]) 134 | const filtered = collection.equalsAny(['id', 'sample'], [10, '20']) 135 | chai.expect(filtered.nodes).to.eql([ 136 | { id: 10, sample: 20 }, 137 | { id: '10', sample: '20' }, 138 | { id: '20', sample: 'zdub' } 139 | ]) 140 | }) 141 | }) 142 | describe('#betweenNumber', () => { 143 | it('returns all values if min/max are undefined', () => { 144 | const collection = new NodeCollection([ 145 | { id: 10, sample: 10 }, 146 | { id: 20, sample: 20 }, 147 | { id: 30, sample: 30 } 148 | ]) 149 | const filtered = collection.betweenNumber('id', undefined, undefined) 150 | chai.expect(filtered.nodes).to.eql([ 151 | { id: 10, sample: 10 }, 152 | { id: 20, sample: 20 }, 153 | { id: 30, sample: 30 } 154 | ]) 155 | }) 156 | it('returns all values if min/max are null', () => { 157 | const collection = new NodeCollection([ 158 | { id: 10, sample: 10 }, 159 | { id: 20, sample: 20 }, 160 | { id: 30, sample: 30 } 161 | ]) 162 | const filtered = collection.betweenNumber('id', null, null) 163 | chai.expect(filtered.nodes).to.eql([ 164 | { id: 10, sample: 10 }, 165 | { id: 20, sample: 20 }, 166 | { id: 30, sample: 30 } 167 | ]) 168 | }) 169 | it('returns all values that are greater or equal a min value', () => { 170 | const collection = new NodeCollection([ 171 | { id: 10, sample: 10 }, 172 | { id: 20, sample: 20 }, 173 | { id: 30, sample: 30 } 174 | ]) 175 | const filtered = collection.betweenNumber('id', 20, null) 176 | chai.expect(filtered.nodes).to.eql([ 177 | { id: 20, sample: 20 }, 178 | { id: 30, sample: 30 } 179 | ]) 180 | }) 181 | it('returns all values that are less than or equal a max value', () => { 182 | const collection = new NodeCollection([ 183 | { id: 10, sample: 10 }, 184 | { id: 20, sample: 20 }, 185 | { id: 30, sample: 30 } 186 | ]) 187 | const filtered = collection.betweenNumber('id', null, 20) 188 | chai.expect(filtered.nodes).to.eql([ 189 | { id: 10, sample: 10 }, 190 | { id: 20, sample: 20 } 191 | ]) 192 | }) 193 | it('returns all values that between or equal a min and a max value', () => { 194 | const collection = new NodeCollection([ 195 | { id: 10, sample: 10 }, 196 | { id: 20, sample: 20 }, 197 | { id: 30, sample: 30 } 198 | ]) 199 | const filtered = collection.betweenNumber('id', 15, 25) 200 | chai.expect(filtered.nodes).to.eql([{ id: 20, sample: 20 }]) 201 | }) 202 | }) 203 | describe('#betweenDate', () => { 204 | it('returns all date values if min/max are undefined', () => { 205 | const collection = new NodeCollection([ 206 | { id: 10, lastActive: new Date(2020, 11, 9, 0, 0) }, 207 | { id: 20, lastActive: new Date(2020, 11, 10, 0, 0) }, 208 | { id: 30, lastActive: new Date(2020, 11, 11, 0, 0) } 209 | ]) 210 | const filtered = collection.betweenDate( 211 | 'lastActive', 212 | undefined, 213 | undefined 214 | ) 215 | chai.expect(filtered.nodes).to.eql([ 216 | { id: 10, lastActive: new Date(2020, 11, 9, 0, 0) }, 217 | { id: 20, lastActive: new Date(2020, 11, 10, 0, 0) }, 218 | { id: 30, lastActive: new Date(2020, 11, 11, 0, 0) } 219 | ]) 220 | }) 221 | it('returns all date values if min/max are null', () => { 222 | const collection = new NodeCollection([ 223 | { id: 10, lastActive: new Date(2020, 11, 9, 0, 0) }, 224 | { id: 20, lastActive: new Date(2020, 11, 10, 0, 0) }, 225 | { id: 30, lastActive: new Date(2020, 11, 11, 0, 0) } 226 | ]) 227 | const filtered = collection.betweenDate('lastActive', null, null) 228 | chai.expect(filtered.nodes).to.eql([ 229 | { id: 10, lastActive: new Date(2020, 11, 9, 0, 0) }, 230 | { id: 20, lastActive: new Date(2020, 11, 10, 0, 0) }, 231 | { id: 30, lastActive: new Date(2020, 11, 11, 0, 0) } 232 | ]) 233 | }) 234 | it('returns all date values that are greater or equal a min date value', () => { 235 | const collection = new NodeCollection([ 236 | { id: 10, lastActive: new Date(2020, 11, 9, 0, 0) }, 237 | { id: 20, lastActive: new Date(2020, 11, 10, 0, 0) }, 238 | { id: 30, lastActive: new Date(2020, 11, 11, 0, 0) } 239 | ]) 240 | const filtered = collection.betweenDate( 241 | 'lastActive', 242 | new Date(2020, 11, 10, 0, 0), 243 | null 244 | ) 245 | chai.expect(filtered.nodes).to.eql([ 246 | { id: 20, lastActive: new Date(2020, 11, 10, 0, 0) }, 247 | { id: 30, lastActive: new Date(2020, 11, 11, 0, 0) } 248 | ]) 249 | }) 250 | it('returns all date values that are less than or equal a max date value', () => { 251 | const collection = new NodeCollection([ 252 | { id: 10, lastActive: new Date(2020, 11, 9, 0, 0) }, 253 | { id: 20, lastActive: new Date(2020, 11, 10, 0, 0) }, 254 | { id: 30, lastActive: new Date(2020, 11, 11, 0, 0) } 255 | ]) 256 | const filtered = collection.betweenDate( 257 | 'lastActive', 258 | null, 259 | new Date(2020, 11, 10, 0, 0) 260 | ) 261 | chai.expect(filtered.nodes).to.eql([ 262 | { id: 10, lastActive: new Date(2020, 11, 9, 0, 0) }, 263 | { id: 20, lastActive: new Date(2020, 11, 10, 0, 0) } 264 | ]) 265 | }) 266 | it('returns all date values that between or equal a min and a max date value', () => { 267 | const collection = new NodeCollection([ 268 | { id: 10, lastActive: new Date(2020, 11, 9, 0, 0) }, 269 | { id: 20, lastActive: new Date(2020, 11, 10, 0, 0) }, 270 | { id: 30, lastActive: new Date(2020, 11, 11, 0, 0) } 271 | ]) 272 | const filtered = collection.betweenDate( 273 | 'lastActive', 274 | new Date(2020, 11, 9, 12, 0), 275 | new Date(2020, 11, 10, 12, 0) 276 | ) 277 | chai 278 | .expect(filtered.nodes) 279 | .to.eql([{ id: 20, lastActive: new Date(2020, 11, 10, 0, 0) }]) 280 | }) 281 | }) 282 | describe('#values', () => { 283 | it('returns a sorted list of unique values for a property - case ignored', () => { 284 | const collection = new NodeCollection([ 285 | { name: 'Giacomo' }, 286 | { name: 'GiaCOMO' }, 287 | { name: 'Birretta' }, 288 | { name: 10 }, 289 | { name: 'giacomo' }, 290 | { name: 10 } 291 | ]) 292 | chai.expect(collection.values('name')).to.eql([10, 'Birretta', 'Giacomo']) 293 | }) 294 | }) 295 | }) 296 | -------------------------------------------------------------------------------- /src/modules/Settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Type-safe way to store and load settings from localStorage. 3 | * It supports the data types 'boolean', 'number', 'string' and 'object'. 4 | */ 5 | export class Settings { 6 | constructor (storage) { 7 | this.storage = storage || localStorage 8 | } 9 | 10 | /** 11 | * Load a setting from the local storage. 12 | * @param {String} key Key of the setting 13 | * @param {*} defaultVal Default value of the setting 14 | * @return Loaded setting 15 | */ 16 | load (key, defaultVal) { 17 | const valStr = this.storage.getItem(key) 18 | const type = typeof defaultVal 19 | let val 20 | switch (type) { 21 | case 'boolean': 22 | val = valStr === 'false' ? false : valStr === 'true' ? true : defaultVal 23 | break 24 | case 'number': 25 | val = valStr && !isNaN(valStr) ? Number(valStr) : defaultVal 26 | break 27 | case 'string': 28 | val = valStr || valStr === '' ? valStr : defaultVal 29 | break 30 | case 'object': 31 | try { 32 | val = JSON.parse(valStr) 33 | } catch (e) { 34 | val = undefined 35 | } 36 | val = 37 | val && (Object.keys(val).length !== 0 || val.constructor === Object) 38 | ? val 39 | : defaultVal 40 | break 41 | } 42 | return val 43 | } 44 | 45 | /** 46 | * Store a setting to the local storage. 47 | * @param {String} key Key of the setting 48 | * @param {*} val Value of the setting 49 | */ 50 | store (key, val) { 51 | let valStr 52 | if (typeof val === 'object') { 53 | valStr = JSON.stringify(val) 54 | } else { 55 | valStr = String(val) 56 | } 57 | this.storage.setItem(key, valStr) 58 | } 59 | } 60 | 61 | export default Settings 62 | -------------------------------------------------------------------------------- /src/modules/Settings.test.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai' 2 | import { Settings } from './Settings' 3 | 4 | class LocalStorageMock { 5 | constructor () { 6 | this.items = {} 7 | this.isMocked = true 8 | } 9 | 10 | getItem (key) { 11 | return this.items[key] 12 | } 13 | 14 | setItem (key, val) { 15 | this.items[key] = val 16 | } 17 | } 18 | 19 | describe('Settings', () => { 20 | describe('#constructor', () => { 21 | it('uses the storage passed in as settings store', () => { 22 | const settings = new Settings(new LocalStorageMock()) 23 | chai.expect(settings.storage.isMocked).to.eql(true) 24 | }) 25 | }) 26 | describe('#store(non-object)', () => { 27 | it('should store a non-object', () => { 28 | const settings = new Settings(new LocalStorageMock()) 29 | settings.store('key', 10) 30 | chai.expect(settings.storage.items.key).to.eql('10') 31 | }) 32 | }) 33 | describe('#store(object)', () => { 34 | it('should store an object', () => { 35 | const settings = new Settings(new LocalStorageMock()) 36 | settings.store('key', { objkey: 'objval' }) 37 | chai.expect(settings.storage.items.key).to.eql('{"objkey":"objval"}') 38 | }) 39 | }) 40 | describe('#load(stored boolean)', () => { 41 | it('should load a stored boolean', () => { 42 | const settings = new Settings(new LocalStorageMock()) 43 | settings.storage.items.key = 'false' 44 | chai.expect(settings.load('key', true)).to.eql(false) 45 | }) 46 | }) 47 | describe('#load(default boolean)', () => { 48 | it('should load a boolean default value', () => { 49 | const settings = new Settings(new LocalStorageMock()) 50 | settings.storage.items.key = undefined 51 | chai.expect(settings.load('key', true)).to.eql(true) 52 | }) 53 | }) 54 | describe('#load(stored number)', () => { 55 | it('should load a stored number', () => { 56 | const settings = new Settings(new LocalStorageMock()) 57 | settings.storage.items.key = '20' 58 | chai.expect(settings.load('key', 10)).to.eql(20) 59 | }) 60 | }) 61 | describe('#load(default number)', () => { 62 | it('should load a number default value', () => { 63 | const settings = new Settings(new LocalStorageMock()) 64 | settings.storage.items.key = undefined 65 | chai.expect(settings.load('key', 10)).to.eql(10) 66 | }) 67 | }) 68 | describe('#load(stored string)', () => { 69 | it('should load a stored string', () => { 70 | const settings = new Settings(new LocalStorageMock()) 71 | settings.storage.items.key = 'value' 72 | chai.expect(settings.load('key', 'default')).to.eql('value') 73 | }) 74 | }) 75 | describe('#load(default string)', () => { 76 | it('should load a string default value', () => { 77 | const settings = new Settings(new LocalStorageMock()) 78 | settings.storage.items.key = undefined 79 | chai.expect(settings.load('key', 'default')).to.eql('default') 80 | }) 81 | }) 82 | describe('#load(stored object)', () => { 83 | it('should load a stored object', () => { 84 | const settings = new Settings(new LocalStorageMock()) 85 | console.log(settings) 86 | settings.storage.items.key = '{"objkey":"value"}' 87 | console.log(settings) 88 | chai 89 | .expect(settings.load('key', { objkey: 'value' })) 90 | .to.eql({ objkey: 'value' }) 91 | }) 92 | }) 93 | describe('#load(default object)', () => { 94 | it('should load a object default value', () => { 95 | const settings = new Settings(new LocalStorageMock()) 96 | settings.storage.items.key = undefined 97 | chai 98 | .expect(settings.load('key', { objkey: 'default' })) 99 | .to.eql({ objkey: 'default' }) 100 | }) 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /src/plugins/vuetify.js: -------------------------------------------------------------------------------- 1 | // src/plugins/vuetify.js 2 | 3 | import Vue from 'vue' 4 | import Vuetify from 'vuetify' 5 | import 'vuetify/dist/vuetify.min.css' 6 | import 'material-design-icons-iconfont/dist/material-design-icons.css' // Ensure you are using css-loader 7 | 8 | Vue.use(Vuetify) 9 | 10 | const opts = { 11 | icons: { 12 | iconfont: 'md' 13 | } 14 | } 15 | 16 | export default new Vuetify(opts) 17 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import ControlPanel from '@/components/ControlPanel' 4 | import Settings from '@/components/Settings' 5 | import Mesh from '@/components/Mesh' 6 | 7 | Vue.use(Router) 8 | 9 | export default new Router({ 10 | mode: 'history', 11 | routes: [ 12 | { 13 | path: '/', 14 | name: 'Control Panel', 15 | component: ControlPanel, 16 | props: true 17 | }, 18 | { 19 | path: '/settings', 20 | name: 'Settings', 21 | component: Settings, 22 | props: true 23 | }, 24 | { 25 | path: '/mesh', 26 | name: 'Network Graph', 27 | component: Mesh, 28 | props: true 29 | } 30 | ] 31 | }) 32 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import { state, mutations, getters, actions } from './mutations' 4 | 5 | Vue.use(Vuex) 6 | 7 | export default new Vuex.Store({ 8 | state, 9 | mutations, 10 | getters, 11 | actions 12 | }) 13 | -------------------------------------------------------------------------------- /src/store/mutations.js: -------------------------------------------------------------------------------- 1 | export const state = { 2 | serial_ports: [], 3 | zwave: {}, 4 | mqtt: {}, 5 | devices: [], 6 | gateway: {} 7 | } 8 | 9 | export const getters = { 10 | serial_ports: state => state.serial_ports, 11 | zwave: state => state.zwave, 12 | mqtt: state => state.mqtt, 13 | devices: state => state.devices, 14 | gateway: state => state.gateway 15 | } 16 | 17 | export const actions = { 18 | init (store, data) { 19 | if (data) { 20 | store.commit('initSettings', data.settings) 21 | store.commit('initPorts', data.serial_ports) 22 | store.commit('initDevices', data.devices) 23 | } 24 | }, 25 | import (store, settings) { 26 | store.commit('initSettings', settings) 27 | } 28 | } 29 | 30 | export const mutations = { 31 | initSettings (state, conf) { 32 | if (conf) { 33 | state.zwave = conf.zwave || {} 34 | state.mqtt = conf.mqtt || {} 35 | state.gateway = conf.gateway || {} 36 | } 37 | }, 38 | initPorts (state, ports) { 39 | state.serial_ports = ports || [] 40 | }, 41 | initDevices (state, devices) { 42 | if (!state.gateway.values) state.gateway.values = [] 43 | 44 | if (devices) { 45 | // devices is an object where key is the device ID and value contains 46 | // device informations 47 | for (var k in devices) { 48 | var d = devices[k] 49 | d.value = k 50 | 51 | var values = [] 52 | 53 | // device.values is an object where key is the valueID (cmdClass-instance-index) and value contains 54 | // value informations 55 | for (var id in d.values) { 56 | var val = d.values[id] 57 | values.push(val) 58 | } 59 | 60 | d.values = values 61 | 62 | state.devices.push(d) 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/static/.gitkeep -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/static/favicon.ico -------------------------------------------------------------------------------- /static/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/static/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/static/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/static/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /static/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2d89ef 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/static/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/static/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/static/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /static/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 19 | 27 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /static/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Zwave2Mqtt", 4 | "short_name": "Zwave2Mqtt", 5 | "icons": [ 6 | { 7 | "src": "/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "theme_color": "#ffffff", 18 | "background_color": "#ffffff", 19 | "display": "standalone" 20 | } 21 | -------------------------------------------------------------------------------- /static/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/static/logo.png -------------------------------------------------------------------------------- /store/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenZWave/Zwave2Mqtt/717206f57df9fd944e9ae5114cd4985483d8eb7d/store/.gitkeep -------------------------------------------------------------------------------- /test/config/webConfig.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const proxyquire = require('proxyquire') 3 | 4 | chai.use(require('sinon-chai')) 5 | chai.should() 6 | 7 | describe('#webConfig', () => { 8 | const webConfig = proxyquire('../../config/webConfig', { 9 | './app': {} 10 | }) 11 | 12 | describe('Uses defaults if nothing specified', () => { 13 | it('uses "/" as the default base', () => { 14 | webConfig.base.should.equal('/') 15 | }) 16 | it('uses "ZWave To MQTT" as the default title', () => { 17 | webConfig.title.should.equal('ZWave To MQTT') 18 | }) 19 | }) 20 | describe('Uses config values when pecified', () => { 21 | const webConfig = proxyquire('../../config/webConfig', { 22 | './app': { 23 | base: '/sub/path/', 24 | title: 'Custom Title' 25 | } 26 | }) 27 | 28 | it('uses "/sub/path/" as the custom base', () => { 29 | webConfig.base.should.equal('/sub/path/') 30 | }) 31 | 32 | it('uses "Custom Title" as the custom title', () => { 33 | webConfig.title.should.equal('Custom Title') 34 | }) 35 | }) 36 | 37 | describe('Path normalization', () => { 38 | const webConfig = proxyquire('../../config/webConfig', { 39 | './app': { 40 | base: '/sub/path' 41 | } 42 | }) 43 | it('Ensures base paths ends with a slash', () => { 44 | webConfig.base.should.equal('/sub/path/') 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /test/lib/Constants.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const sinon = require('sinon') 3 | const _ = require('lodash') 4 | chai.use(require('sinon-chai')) 5 | chai.should() 6 | 7 | const mod = require('../../lib/Constants') 8 | 9 | describe('#Constants', () => { 10 | describe('#productionType()', () => { 11 | let map 12 | before(() => { 13 | map = mod._productionMap 14 | mod._productionMap = { 1: 'foo' } 15 | }) 16 | after(() => { 17 | mod._productionMap = map 18 | }) 19 | it('known', () => 20 | mod.productionType(1).should.deep.equal({ 21 | objectId: 'foo', 22 | props: { device_class: 'power' }, 23 | sensor: 'energy_production' 24 | })) 25 | it('unknown', () => 26 | mod.productionType(2).should.deep.equal({ 27 | objectId: 'unknown', 28 | props: { device_class: 'power' }, 29 | sensor: 'energy_production' 30 | })) 31 | it('timestamp', () => 32 | mod.productionType(3).should.deep.equal({ 33 | objectId: 'unknown', 34 | props: { device_class: 'timestamp' }, 35 | sensor: 'energy_production' 36 | })) 37 | }) 38 | describe('#meterType()', () => { 39 | let sensorType 40 | var map = mod._metersMap 41 | before(() => { 42 | sensorType = sinon.stub(mod, 'sensorType').returns({}) 43 | mod._metersMap = { 1: 'foo' } 44 | }) 45 | after(() => { 46 | mod._metersMap = map 47 | sensorType.restore() 48 | }) 49 | it('known', () => 50 | mod.meterType(1).should.deep.equal({ objectId: 'foo_meter' })) 51 | it('unknown', () => 52 | mod.meterType(2).should.deep.equal({ objectId: 'unknown_meter' })) 53 | describe('electricity', () => { 54 | before(() => { 55 | sensorType.resetHistory() 56 | _.range(1, 16).forEach(i => mod.meterType(i)) 57 | mod.meterType(48) 58 | mod.meterType(64) 59 | }) 60 | it('electricity', () => sensorType.should.always.have.been.calledWith(4)) 61 | }) 62 | describe('gas', () => { 63 | before(() => { 64 | sensorType.resetHistory() 65 | _.range(16, 32).forEach(i => mod.meterType(i)) 66 | }) 67 | it('gas', () => sensorType.should.always.have.been.calledWith(55)) 68 | }) 69 | describe('water', () => { 70 | before(() => { 71 | sensorType.resetHistory() 72 | _.range(32, 48).forEach(i => mod.meterType(i)) 73 | }) 74 | it('water', () => sensorType.should.always.have.been.calledWith(12)) 75 | }) 76 | }) 77 | describe('#alarmType()', () => { 78 | let map 79 | before(() => { 80 | map = mod._alarmMap 81 | mod._alarmMap = { 1: 'foo' } 82 | }) 83 | after(() => { 84 | mod._alarmMap = map 85 | }) 86 | it('known', () => mod.alarmType(1).should.equal('foo')) 87 | it('unknown', () => mod.alarmType(3).should.equal('unknown_3')) 88 | }) 89 | describe('#sensorType()', () => { 90 | let map 91 | before(() => { 92 | map = mod._sensorMap 93 | mod._sensorMap = { 94 | foo: { 1: 'bar', props: { a: 'b', c: 'd' } }, 95 | bar: { 2: 'foo' } 96 | } 97 | }) 98 | after(() => { 99 | mod._sensorMap = map 100 | }) 101 | it('known', () => 102 | mod.sensorType(1).should.deep.equal({ 103 | sensor: 'foo', 104 | objectId: 'bar', 105 | props: { a: 'b', c: 'd' } 106 | })) 107 | it('no props', () => 108 | mod.sensorType(2).should.deep.equal({ 109 | sensor: 'bar', 110 | objectId: 'foo', 111 | props: {} 112 | })) 113 | it('unknown', () => 114 | mod.sensorType(3).should.deep.equal({ 115 | sensor: 'generic', 116 | objectId: 'unknown_3', 117 | props: {} 118 | })) 119 | }) 120 | describe('#commandClass()', () => { 121 | let map 122 | before(() => { 123 | map = mod._commandClassMap 124 | mod._commandClassMap = { 1: 'foo' } 125 | }) 126 | after(() => { 127 | mod._commandClassMap = map 128 | }) 129 | it('known', () => mod.commandClass(1).should.equal('foo')) 130 | it('unknown', () => mod.commandClass(3).should.equal('unknownClass_3')) 131 | }) 132 | describe('#genericDeviceClass()', () => { 133 | let map 134 | before(() => { 135 | map = mod._genericDeviceClassMap 136 | mod._genericDeviceClassMap = { 137 | 1: { generic: 'foo', specific: { 1: 'bar', 2: 'baz' } } 138 | } 139 | }) 140 | after(() => { 141 | mod._genericDeviceClassMap = map 142 | }) 143 | it('known generic type', () => 144 | mod.genericDeviceClass(1).should.equal('foo')) 145 | it('unknown generic type', () => 146 | mod.genericDeviceClass(3).should.equal('unknownGenericDeviceType_3')) 147 | }) 148 | describe('#specificDeviceClass()', () => { 149 | let map 150 | before(() => { 151 | map = mod._genericDeviceClassMap 152 | mod._genericDeviceClassMap = { 153 | 1: { generic: 'foo', specific: { 1: 'bar', 2: 'baz' } } 154 | } 155 | }) 156 | after(() => { 157 | mod._genericDeviceClassMap = map 158 | }) 159 | it('known specific type', () => 160 | mod.specificDeviceClass(1, 1).should.equal('bar')) 161 | it('unknown specific type', () => 162 | mod.specificDeviceClass(1, 3).should.equal('unknownSpecificDeviceType_3')) 163 | it('unknown generic type 1', () => 164 | mod.specificDeviceClass(2, 1).should.equal('unknownGenericDeviceType_2')) 165 | it('unknown generic type 2', () => 166 | mod.specificDeviceClass(2, 3).should.equal('unknownGenericDeviceType_2')) 167 | }) 168 | }) 169 | -------------------------------------------------------------------------------- /test/lib/Gateway.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const rewire = require('rewire') 3 | chai.use(require('sinon-chai')) 4 | chai.should() 5 | 6 | const mod = rewire('../../lib/Gateway') 7 | 8 | describe('#Gateway', () => { 9 | describe('#setDiscoveryValue()', () => { 10 | let untouchedPayload 11 | const func = mod.__get__('setDiscoveryValue') 12 | let payload 13 | const node = { 14 | values: { 15 | c: { value: 'a' }, 16 | d: { value: null }, 17 | e: false 18 | } 19 | } 20 | beforeEach(() => { 21 | payload = { 22 | a: 1, 23 | b: 'c', 24 | c: 'd', 25 | d: 'e' 26 | } 27 | untouchedPayload = JSON.parse(JSON.stringify(payload)) 28 | }) 29 | 30 | describe('payload prop not string', () => { 31 | it('should not change payload', () => { 32 | func(payload, 'a', node) 33 | payload.should.deep.equal(untouchedPayload) 34 | }) 35 | }) 36 | describe('no valueId', () => { 37 | it('should not change payload', () => { 38 | func(payload, 'd', node) 39 | payload.should.deep.equal(untouchedPayload) 40 | }) 41 | }) 42 | describe('no valueId.value', () => { 43 | it('should not change payload', () => { 44 | func(payload, 'c', node) 45 | payload.should.deep.equal(untouchedPayload) 46 | }) 47 | }) 48 | describe('happy path', () => { 49 | it('should not change payload', () => { 50 | func(payload, 'b', node) 51 | payload.should.deep.equal({ 52 | a: 1, 53 | b: 'a', 54 | c: 'd', 55 | d: 'e' 56 | }) 57 | }) 58 | }) 59 | }) 60 | 61 | afterEach(() => { 62 | mod.__get__('watchers').forEach(v => { 63 | if (v != null) { 64 | v.close() 65 | } 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /test/lib/debug.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const rewire = require('rewire') 3 | chai.should() 4 | 5 | describe('#debug', () => { 6 | const mod = rewire('../../lib/debug') 7 | const fun = mod.__get__('init') 8 | 9 | it('returns debug extend', () => mod('foo').namespace.should.equal('z2m:foo')) 10 | 11 | describe('set process.env.DEBUG', () => { 12 | before(() => { 13 | mod.__get__('log').disable() 14 | process.env.DEBUG = 'ff' 15 | fun() 16 | }) 17 | it('should disable logging', () => 18 | mod.__get__('log').enabled('z2m:aa').should.be.false) 19 | }) 20 | describe('unset process.env.DEBUG', () => { 21 | before(() => { 22 | mod.__get__('log').disable() 23 | delete process.env.DEBUG 24 | fun() 25 | }) 26 | it('should enable logging', () => { 27 | return mod.__get__('log').enabled('z2m:aa').should.be.true 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /test/lib/jsonStore.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const sinon = require('sinon') 3 | const rewire = require('rewire') 4 | 5 | chai.use(require('chai-as-promised')) 6 | chai.use(require('sinon-chai')) 7 | const should = chai.should() 8 | 9 | const mod = rewire('../../lib/jsonStore') 10 | 11 | describe('#jsonStore', () => { 12 | describe('#getFile()', () => { 13 | const fun = mod.__get__('getFile') 14 | const config = { file: 'foo', default: 'defaultbar' } 15 | beforeEach(() => { 16 | sinon.stub(mod.__get__('utils'), 'joinPath') 17 | sinon.stub(mod.__get__('jsonfile'), 'readFile') 18 | }) 19 | afterEach(() => { 20 | mod.__get__('utils').joinPath.restore() 21 | mod.__get__('jsonfile').readFile.restore() 22 | }) 23 | 24 | it('uncaught error', () => { 25 | mod 26 | .__get__('jsonfile') 27 | .readFile.callsArgWith(1, new Error('FOO'), 'mybar') 28 | return fun(config).should.eventually.be.rejectedWith(Error, 'FOO') 29 | }) 30 | 31 | it('data returned', () => { 32 | mod.__get__('jsonfile').readFile.callsArgWith(1, false, 'mybar') 33 | return fun(config).should.eventually.deep.equal({ 34 | file: 'foo', 35 | data: 'mybar' 36 | }) 37 | }) 38 | 39 | it('no data, return default', () => { 40 | mod.__get__('jsonfile').readFile.callsArgWith(1, false, null) 41 | return fun(config).should.eventually.deep.equal({ 42 | file: 'foo', 43 | data: 'defaultbar' 44 | }) 45 | }) 46 | 47 | it('file not found, return default', () => { 48 | mod.__get__('jsonfile').readFile.callsArgWith(1, { code: 'ENOENT' }, null) 49 | return fun(config).should.eventually.deep.equal({ 50 | file: 'foo', 51 | data: 'defaultbar' 52 | }) 53 | }) 54 | }) 55 | 56 | describe('#StorageHelper', () => { 57 | const StorageHelper = mod.__get__('StorageHelper') 58 | it('class test', () => { 59 | const ins = new StorageHelper() 60 | ins.store.should.deep.equal({}) 61 | }) 62 | 63 | describe('#init()', () => { 64 | let getFile 65 | beforeEach(() => { 66 | mod.store = { known: 'no', foobar: 'foo' } 67 | getFile = mod.__get__('getFile') 68 | mod.__set__( 69 | 'getFile', 70 | sinon.stub().resolves({ file: 'foo', data: 'bar' }) 71 | ) 72 | }) 73 | afterEach(() => { 74 | mod.__set__('getFile', getFile) 75 | }) 76 | it('ok', () => 77 | mod.init({ file: 'foobar' }).should.eventually.deep.equal({ 78 | known: 'no', 79 | foobar: 'foo', 80 | foo: 'bar' 81 | })) 82 | it('error', () => { 83 | mod.__set__('getFile', sinon.stub().rejects('fo')) 84 | return mod.init({ file: 'foobar' }).should.eventually.be.rejected 85 | }) 86 | }) 87 | 88 | describe('#get()', () => { 89 | beforeEach(() => { 90 | mod.store = { known: 'foo' } 91 | }) 92 | it('known', () => mod.get({ file: 'known' }).should.equal('foo')) 93 | it('unknown', () => 94 | should.Throw( 95 | () => mod.get({ file: 'unknown' }), 96 | 'Requested file not present in store: unknown' 97 | )) 98 | }) 99 | 100 | describe('#put()', () => { 101 | beforeEach(() => { 102 | sinon.stub(mod.__get__('jsonfile'), 'writeFile') 103 | }) 104 | afterEach(() => { 105 | mod.__get__('jsonfile').writeFile.restore() 106 | }) 107 | it('ok', () => { 108 | mod.__get__('jsonfile').writeFile.callsArgWith(2, null) 109 | return mod 110 | .put({ file: 'foo' }, 'bardata') 111 | .should.eventually.equal('bardata') 112 | }) 113 | it('error', () => { 114 | mod.__get__('jsonfile').writeFile.callsArgWith(2, new Error('bar')) 115 | mod.put({ file: 'foo' }).should.be.rejectedWith('bar') 116 | }) 117 | }) 118 | }) 119 | }) 120 | -------------------------------------------------------------------------------- /test/lib/renderIndex.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const sinon = require('sinon') 3 | const rewire = require('rewire') 4 | const fs = require('fs') 5 | const path = require('path') 6 | 7 | const cssFolder = path.join(__dirname, '..', '..', 'dist', 'static', 'css') 8 | const jsFolder = path.join(__dirname, '..', '..', 'dist', 'static', 'js') 9 | 10 | chai.use(require('sinon-chai')) 11 | chai.should() 12 | 13 | let lastTpl 14 | let lastOptions 15 | 16 | const mockResponse = { 17 | render: (tpl, options) => { 18 | lastTpl = tpl 19 | lastOptions = options 20 | } 21 | } 22 | 23 | describe('#renderIndex', () => { 24 | describe('Processing configuration', () => { 25 | let renderIndex 26 | let mockedReaddir 27 | 28 | beforeEach(() => { 29 | renderIndex = rewire('../../lib/renderIndex') 30 | renderIndex.__set__('webConfig', { 31 | base: '/configured/path' 32 | }) 33 | mockedReaddir = sinon.stub(fs, 'readdirSync') 34 | mockedReaddir.returns([]) 35 | }) 36 | 37 | afterEach(() => { 38 | mockedReaddir.restore() 39 | }) 40 | 41 | it('uses the base from the `X-External-Path` header', () => { 42 | renderIndex( 43 | { 44 | headers: { 45 | 'x-external-path': '/test/base' 46 | } 47 | }, 48 | mockResponse 49 | ) 50 | return lastOptions.config.base.should.equal('/test/base/') 51 | }) 52 | 53 | it('uses configured value if no header is present', () => { 54 | renderIndex( 55 | { 56 | headers: {} 57 | }, 58 | mockResponse 59 | ) 60 | lastOptions.config.base.should.equal('/configured/path/') 61 | }) 62 | }) 63 | 64 | describe('Processing static files', () => { 65 | let mockedReaddir 66 | let renderIndex 67 | 68 | beforeEach(() => { 69 | renderIndex = rewire('../../lib/renderIndex') 70 | mockedReaddir = sinon.stub(fs, 'readdirSync') 71 | }) 72 | 73 | afterEach(() => { 74 | mockedReaddir.restore() 75 | }) 76 | 77 | it('When no dist files present it will have empty css and js files', () => { 78 | mockedReaddir.returns([]) 79 | renderIndex( 80 | { 81 | headers: {} 82 | }, 83 | mockResponse 84 | ) 85 | lastTpl.should.equal('index.ejs') 86 | lastOptions.cssFiles.should.eql([]) 87 | lastOptions.jsFiles.should.eql([]) 88 | }) 89 | 90 | it('When dist files present will only return the ones with the correct extensions', () => { 91 | mockedReaddir 92 | .withArgs(cssFolder) 93 | .returns(['valid-css.css', 'invalid-css.scss']) 94 | mockedReaddir 95 | .withArgs(jsFolder) 96 | .returns(['valid-js.js', 'invalid-js.map']) 97 | renderIndex( 98 | { 99 | headers: {} 100 | }, 101 | mockResponse 102 | ) 103 | lastTpl.should.equal('index.ejs') 104 | lastOptions.cssFiles.should.eql(['static/css/valid-css.css']) 105 | lastOptions.jsFiles.should.eql(['static/js/valid-js.js']) 106 | }) 107 | }) 108 | }) 109 | -------------------------------------------------------------------------------- /test/lib/utils.test.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai') 2 | const rewire = require('rewire') 3 | const sinon = require('sinon') 4 | chai.use(require('sinon-chai')) 5 | chai.should() 6 | 7 | const mod = rewire('../../lib/utils') 8 | 9 | describe('#utils', () => { 10 | describe('#getPath()', () => { 11 | mod.__set__('appRoot', { toString: () => 'foo' }) 12 | it('write && process.pkg', () => { 13 | process.pkg = true 14 | mod.getPath(true).should.equal(process.cwd()) 15 | }) 16 | it('write && !process.pkg', () => { 17 | process.pkg = false 18 | mod.getPath(true).should.equal('foo') 19 | }) 20 | it('!write && process.pkg', () => { 21 | process.pkg = true 22 | mod.getPath(false).should.equal('foo') 23 | }) 24 | it('!write && !process.pkg', () => { 25 | process.pkg = false 26 | mod.getPath(false).should.equal('foo') 27 | }) 28 | }) 29 | describe('#joinPath()', () => { 30 | let path 31 | before(() => { 32 | path = { join: sinon.stub() } 33 | mod.__set__('path', path) 34 | sinon.stub(mod, 'getPath').returns('foo') 35 | }) 36 | after(() => { 37 | mod.getPath.restore() 38 | }) 39 | 40 | it('zero length', () => { 41 | mod.joinPath() 42 | return path.join.should.have.been.calledWith() 43 | }) 44 | it('1 length', () => { 45 | mod.joinPath('foo') 46 | return path.join.should.have.been.calledWith('foo') 47 | }) 48 | it('first arg bool gets new path 0', () => { 49 | mod.joinPath(true, 'bar') 50 | return path.join.should.have.been.calledWithExactly('foo', 'bar') 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /views/error.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

<%= message %>

6 | 7 |

<%= error.status %>

8 |

<%= error.stack %>

9 | 10 | 11 | -------------------------------------------------------------------------------- /views/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= config.title %> 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | <% if (typeof cssFiles !== 'undefined') { %> 24 | <% for ( let css of cssFiles ){ %> 25 | 26 | <% } %> 27 | <% } %> 28 | 29 | 30 | 31 |
32 | 33 | <% if (typeof jsFiles !== 'undefined') { %> 34 | <% for ( let src of jsFiles ){ %> 35 | 36 | <% } %> 37 | <% } %> 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function () { 2 | return { 3 | files: ['lib/**/*.js'], 4 | 5 | tests: ['test/**/*.test.js'], 6 | 7 | testFramework: 'mocha', 8 | 9 | env: { 10 | type: 'node' 11 | }, 12 | 13 | workers: { recycle: true } 14 | } 15 | } 16 | --------------------------------------------------------------------------------