├── .eslintrc.json ├── .gcloudignore ├── .github ├── CODEOWNERS ├── dependabot.yml ├── release-drafter.yml └── workflows │ ├── ci-cd.yml │ ├── docs.yml │ └── draft-release.yml ├── .gitignore ├── .markdownlint.yaml ├── LICENSE ├── NOTICE ├── README.md ├── action.json ├── docs ├── USAGE.md ├── _config.yml └── images │ ├── Screenshot_1.png │ ├── Screenshot_2.png │ ├── Screenshot_3.png │ ├── Screenshot_4.png │ ├── Screenshot_5.png │ ├── Screenshot_6.png │ └── Screenshot_7.png ├── functions ├── apihandler.js ├── commands │ ├── activatescene.js │ ├── appselect.js │ ├── armdisarm.js │ ├── brightnessabsolute.js │ ├── charge.js │ ├── colorabsolute.js │ ├── colorabsolutetemperature.js │ ├── default.js │ ├── getcamerastream.js │ ├── index.js │ ├── lockunlock.js │ ├── medianext.js │ ├── mediapause.js │ ├── mediaprevious.js │ ├── mediaresume.js │ ├── mute.js │ ├── onoff.js │ ├── openclose.js │ ├── selectchannel.js │ ├── setfanspeed.js │ ├── setinput.js │ ├── setmodes.js │ ├── setvolume.js │ ├── startstop.js │ ├── thermostatsetmode.js │ ├── thermostattemperaturesetpoint.js │ ├── thermostattemperaturesetpointhigh.js │ ├── thermostattemperaturesetpointlow.js │ └── volumerelative.js ├── config.js ├── devices │ ├── acunit.js │ ├── airpurifier.js │ ├── awning.js │ ├── blinds.js │ ├── camera.js │ ├── charger.js │ ├── climatesensor.js │ ├── coffeemaker.js │ ├── colorlight.js │ ├── curtain.js │ ├── default.js │ ├── dimmablelight.js │ ├── dishwasher.js │ ├── door.js │ ├── dynamicmodesdevice.js │ ├── dynamicmodeslight.js │ ├── fan.js │ ├── fireplace.js │ ├── garage.js │ ├── gate.js │ ├── hood.js │ ├── humiditysensor.js │ ├── index.js │ ├── lock.js │ ├── modesdevice.js │ ├── modeslight.js │ ├── openclosedevice.js │ ├── outlet.js │ ├── pergola.js │ ├── scene.js │ ├── securitysystem.js │ ├── sensor.js │ ├── shutter.js │ ├── simpleairpurifier.js │ ├── simplefan.js │ ├── simplehood.js │ ├── simplelight.js │ ├── simplesecuritysystem.js │ ├── speaker.js │ ├── specialcolorlight.js │ ├── sprinkler.js │ ├── startstopswitch.js │ ├── switch.js │ ├── temperaturesensor.js │ ├── thermostat.js │ ├── tv.js │ ├── vacuum.js │ ├── valve.js │ ├── washer.js │ ├── waterheater.js │ └── window.js ├── index.js ├── openhab.js ├── package-lock.json ├── package.json └── utilities.js ├── package-lock.json ├── package.json ├── testServer.js └── tests ├── apihandler.test.js ├── commands ├── activatescene.test.js ├── appselect.test.js ├── armdisarm.test.js ├── brightnessabsolute.test.js ├── charge.test.js ├── colorabsolute.test.js ├── colorabsolutetemperature.test.js ├── default.test.js ├── getcamerastream.test.js ├── index.test.js ├── lockunlock.test.js ├── medianext.test.js ├── mediapause.test.js ├── mediaprevious.test.js ├── mediaresume.test.js ├── mute.test.js ├── onoff.test.js ├── openclose.test.js ├── selectchannel.test.js ├── setfanspeed.test.js ├── setinput.test.js ├── setmodes.test.js ├── setvolume.test.js ├── startstop.test.js ├── thermostatsetmode.test.js ├── thermostattemperaturesetpoint.test.js ├── thermostattemperaturesetpointhigh.test.js ├── thermostattemperaturesetpointlow.test.js └── volumerelative.test.js ├── config.test.js ├── devices ├── acunit.test.js ├── camera.test.js ├── charger.test.js ├── climatesensor.test.js ├── colorlight.test.js ├── default.test.js ├── dimmablelight.test.js ├── dynamicmodesdevice.test.js ├── dynamicmodeslight.test.js ├── fan.test.js ├── humiditysensor.test.js ├── index.test.js ├── lock.test.js ├── modesdevice.test.js ├── modeslight.test.js ├── openclosedevice.test.js ├── scene.test.js ├── securitysystem.test.js ├── sensor.test.js ├── simplelight.test.js ├── simplesecuritysystem.test.js ├── speaker.test.js ├── specialcolorlight.test.js ├── startstopswitch.test.js ├── switch.test.js ├── temperaturesensor.test.js ├── thermostat.test.js ├── tv.test.js └── valve.test.js ├── openhab.test.js ├── setenv.js └── utilities.test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "jest": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "prettier" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parserOptions": { 16 | "ecmaVersion": 2019, 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "prettier" 21 | ], 22 | "rules": { 23 | "prettier/prettier": [ 24 | "error", 25 | { 26 | "singleQuote": true, 27 | "trailingComma": "none", 28 | "tabWidth": 2, 29 | "printWidth": 120 30 | } 31 | ], 32 | "no-empty": [ 33 | "error", 34 | { 35 | "allowEmptyCatch": true 36 | } 37 | ], 38 | "max-len": [ 39 | "error", 40 | { 41 | "code": 120, 42 | "tabWidth": 2, 43 | "ignoreUrls": true 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | node_modules 17 | #!include:.gitignore 18 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @marziman @michikrug 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | labels: 8 | - "dependencies" 9 | 10 | - package-ecosystem: "npm" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | labels: 15 | - "dependencies" 16 | 17 | - package-ecosystem: "npm" 18 | directory: "/functions" 19 | schedule: 20 | interval: "weekly" 21 | labels: 22 | - "dependencies" 23 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | categories: 2 | - title: '🚀 Features' 3 | labels: 4 | - 'feature' 5 | - 'enhancement' 6 | - title: '🐛 Bug Fixes' 7 | labels: 8 | - 'fix' 9 | - 'bugfix' 10 | - 'bug' 11 | - title: '🧰 Maintenance' 12 | labels: 13 | - 'maintenance' 14 | - 'dependencies' 15 | - title: '📖 Documentation' 16 | labels: 17 | - 'documentation' 18 | template: | 19 | ## Changes 20 | 21 | $CHANGES 22 | -------------------------------------------------------------------------------- /.github/workflows/ci-cd.yml: -------------------------------------------------------------------------------- 1 | name: CI/CD 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | tags: [ "*" ] 7 | pull_request: 8 | branches: [ main ] 9 | release: 10 | types: [ published ] 11 | 12 | jobs: 13 | markdown-checks: 14 | if: github.event_name == 'pull_request' 15 | name: Markdown Checks 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v4 21 | 22 | - name: Lint Markdown files 23 | uses: nosborn/github-action-markdown-cli@v3.3.0 24 | with: 25 | files: . 26 | 27 | - name: Check spelling 28 | uses: reviewdog/action-misspell@v1 29 | with: 30 | github_token: ${{ secrets.github_token }} 31 | locale: 'US' 32 | fail_on_error: true 33 | filter_mode: 'nofilter' 34 | exclude: '*.json' 35 | 36 | - name: Check style and grammar 37 | uses: reviewdog/action-languagetool@v1 38 | with: 39 | github_token: ${{ secrets.github_token }} 40 | 41 | unit-testing: 42 | name: Unit Testing 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - name: Checkout repository 47 | uses: actions/checkout@v4 48 | 49 | - name: Use Node.js 18.x 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: 18.x 53 | cache: 'npm' 54 | 55 | - name: Install dependencies 56 | run: npm ci 57 | 58 | - name: Run linting 59 | run: npm run lint 60 | 61 | - name: Run tests 62 | run: npm run test-ci 63 | 64 | - name: Upload Test Coverage 65 | uses: actions/upload-artifact@v4 66 | with: 67 | name: coverage 68 | path: coverage/ 69 | 70 | code-analysis: 71 | name: Code Analysis 72 | runs-on: ubuntu-latest 73 | 74 | steps: 75 | - name: Checkout repository 76 | uses: actions/checkout@v4 77 | 78 | - name: Initialize CodeQL 79 | uses: github/codeql-action/init@v3 80 | with: 81 | languages: javascript 82 | 83 | - name: Perform CodeQL Analysis 84 | uses: github/codeql-action/analyze@v3 85 | 86 | deployment: 87 | if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'release' 88 | name: Install and Deploy 89 | runs-on: ubuntu-latest 90 | needs: [unit-testing, code-analysis] 91 | 92 | steps: 93 | - name: Checkout repository 94 | uses: actions/checkout@v4 95 | 96 | - name: Use Node.js 18.x 97 | uses: actions/setup-node@v4 98 | with: 99 | node-version: 18.x 100 | cache: 'npm' 101 | cache-dependency-path: 'functions/package-lock.json' 102 | 103 | - name: Install dependencies 104 | working-directory: ./functions 105 | run: npm ci 106 | 107 | - name: Authenticate to Google Cloud 108 | uses: google-github-actions/auth@v2.1.7 109 | with: 110 | credentials_json: ${{ secrets.GCP_SA_KEY }} 111 | 112 | - name: Deploy to Google Cloud Functions 113 | uses: google-github-actions/deploy-cloud-functions@v2.1.0 114 | with: 115 | name: ${{ github.event_name == 'release' && 'openhabGoogleAssistant' || 'openhabGoogleAssistant_test' }} 116 | runtime: nodejs18 117 | source_dir: ./functions 118 | entry_point: openhabGoogleAssistant 119 | region: us-central1 120 | timeout: 180 121 | memory_mb: 256MB 122 | min_instances: 1 123 | max_instances: 20 124 | env_vars: OH_HOST=myopenhab.org,OH_PORT=443,OH_PATH=/rest/items/ 125 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Update Documentation 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | triggerWorkflowDispatch: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Repository Dispatch 13 | uses: peter-evans/repository-dispatch@v3 14 | with: 15 | token: ${{ secrets.DOCS_REPO_ACCESS_TOKEN }} 16 | repository: openhab/openhab-docs 17 | event-type: update-openhab-google-assistant-docs-event 18 | client-payload: '{"ref": "${{ github.ref }}"}' 19 | -------------------------------------------------------------------------------- /.github/workflows/draft-release.yml: -------------------------------------------------------------------------------- 1 | name: Draft Release 2 | 3 | on: 4 | push: 5 | tags: [ "*" ] 6 | 7 | jobs: 8 | release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Get version tag 12 | id: get_version_tag 13 | run: | 14 | [[ ! "$GITHUB_REF" =~ refs/tags ]] && exit 15 | echo "::set-output name=tag::${GITHUB_REF#refs/tags/}" 16 | - name: Draft Release 17 | uses: release-drafter/release-drafter@v6 18 | with: 19 | name: ${{steps.get_version_tag.outputs.tag}} 20 | tag: ${{steps.get_version_tag.outputs.tag}} 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.eslintcache 2 | /creds.data 3 | /.settings/ 4 | /.vscode/ 5 | /coverage/ 6 | /gactions/ 7 | /node_modules/ 8 | /functions/node_modules/ 9 | -------------------------------------------------------------------------------- /.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | default: true 2 | MD013: false 3 | MD025: false 4 | MD033: false 5 | MD040: false 6 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | This content is produced and maintained by the openHAB project. 2 | 3 | * Project home: https://www.openhab.org 4 | 5 | == Declared Project Licenses 6 | 7 | This program and the accompanying materials are made available under the terms 8 | of the Eclipse Public License 2.0 which is available at 9 | https://www.eclipse.org/legal/epl-2.0/. 10 | 11 | == Source Code 12 | 13 | https://github.com/openhab/openhab-google-assistant 14 | -------------------------------------------------------------------------------- /action.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [ 3 | { 4 | "name": "actions.devices", 5 | "deviceControl": {}, 6 | "fulfillment": { 7 | "conversationName": "automation" 8 | } 9 | } 10 | ], 11 | "conversations": { 12 | "automation": { 13 | "name": "automation", 14 | "url": "" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /docs/images/Screenshot_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhab/openhab-google-assistant/b67f09a19e7d8c797e7e64284a4eec9c408a9782/docs/images/Screenshot_1.png -------------------------------------------------------------------------------- /docs/images/Screenshot_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhab/openhab-google-assistant/b67f09a19e7d8c797e7e64284a4eec9c408a9782/docs/images/Screenshot_2.png -------------------------------------------------------------------------------- /docs/images/Screenshot_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhab/openhab-google-assistant/b67f09a19e7d8c797e7e64284a4eec9c408a9782/docs/images/Screenshot_3.png -------------------------------------------------------------------------------- /docs/images/Screenshot_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhab/openhab-google-assistant/b67f09a19e7d8c797e7e64284a4eec9c408a9782/docs/images/Screenshot_4.png -------------------------------------------------------------------------------- /docs/images/Screenshot_5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhab/openhab-google-assistant/b67f09a19e7d8c797e7e64284a4eec9c408a9782/docs/images/Screenshot_5.png -------------------------------------------------------------------------------- /docs/images/Screenshot_6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhab/openhab-google-assistant/b67f09a19e7d8c797e7e64284a4eec9c408a9782/docs/images/Screenshot_6.png -------------------------------------------------------------------------------- /docs/images/Screenshot_7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openhab/openhab-google-assistant/b67f09a19e7d8c797e7e64284a4eec9c408a9782/docs/images/Screenshot_7.png -------------------------------------------------------------------------------- /functions/apihandler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010-2019 Contributors to the openHAB project 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | 14 | /** 15 | * openHAB REST API handler for requests towards the openHAB REST API 16 | * 17 | * @author Mehmet Arziman - Initial contribution 18 | * @author Michael Krug - Rework 19 | * 20 | */ 21 | const http = require('http'); 22 | const https = require('https'); 23 | 24 | class ApiHandler { 25 | /** 26 | * @param {object} config 27 | */ 28 | constructor(config = { host: '', path: '/rest/items/', port: 80 }) { 29 | if (!config.path.startsWith('/')) { 30 | config.path = `/${config.path}`; 31 | } 32 | if (!config.path.endsWith('/')) { 33 | config.path += '/'; 34 | } 35 | this._config = config; 36 | this._authToken = ''; 37 | } 38 | 39 | /** 40 | * @param {string} authToken 41 | */ 42 | set authToken(authToken) { 43 | this._authToken = authToken; 44 | } 45 | 46 | /** 47 | * @param {string} method 48 | * @param {string} itemName 49 | * @param {number} length 50 | */ 51 | getOptions(method = 'GET', itemName = '', length = 0) { 52 | const queryString = 53 | method === 'GET' 54 | ? `?metadata=ga,synonyms${itemName ? '' : '&fields=groupNames,groupType,name,label,metadata,type,state'}` 55 | : ''; 56 | const options = { 57 | hostname: this._config.host, 58 | port: this._config.port, 59 | method: method, 60 | path: this._config.path + (itemName || '') + queryString, 61 | headers: { 62 | Accept: 'application/json' 63 | } 64 | }; 65 | 66 | if (this._config.userpass) { 67 | options.auth = this._config.userpass; 68 | } else if (this._authToken) { 69 | options.headers.Authorization = `Bearer ${this._authToken}`; 70 | } 71 | 72 | if (method === 'POST') { 73 | options.headers['Content-Type'] = 'text/plain'; 74 | options.headers['Content-Length'] = length; 75 | } 76 | 77 | return options; 78 | } 79 | 80 | /** 81 | * @param {string} itemName 82 | */ 83 | getItem(itemName = '') { 84 | const options = this.getOptions('GET', itemName); 85 | return new Promise((resolve, reject) => { 86 | const protocol = options.port === 443 ? https : http; 87 | const req = protocol.request(options, (response) => { 88 | if (response.statusCode !== 200) { 89 | reject({ statusCode: response.statusCode, message: `getItem - failed for path: ${options.path}` }); 90 | return; 91 | } 92 | 93 | response.setEncoding('utf8'); 94 | let data = ''; 95 | 96 | response.on('data', (chunk) => { 97 | data += chunk; 98 | }); 99 | 100 | response.on('end', () => { 101 | try { 102 | resolve(JSON.parse(data)); 103 | } catch (e) { 104 | reject({ 105 | statusCode: 415, 106 | message: `getItem - JSON parse failed for path: ${options.path} - ${e.toString()}` 107 | }); 108 | } 109 | }); 110 | }); 111 | req.on('error', (error) => { 112 | console.error(`openhabGoogleAssistant - getItem: ERROR ${JSON.stringify(error)}`); 113 | reject(error); 114 | }); 115 | req.end(); 116 | }); 117 | } 118 | 119 | getItems() { 120 | return this.getItem(); 121 | } 122 | 123 | /** 124 | * @param {string} itemName 125 | * @param {string} payload 126 | */ 127 | sendCommand(itemName, payload) { 128 | const options = this.getOptions('POST', itemName, payload.length); 129 | return new Promise((resolve, reject) => { 130 | const protocol = options.port === 443 ? https : http; 131 | const req = protocol.request(options, (response) => { 132 | if (!response.statusCode || ![200, 201].includes(response.statusCode)) { 133 | reject({ statusCode: response.statusCode, message: `sendCommand - failed for path: ${options.path}` }); 134 | return; 135 | } 136 | resolve(true); 137 | }); 138 | req.on('error', (error) => { 139 | console.error(`openhabGoogleAssistant - sendCommand: ERROR ${JSON.stringify(error)}`); 140 | reject(error); 141 | }); 142 | req.write(payload); 143 | req.end(); 144 | }); 145 | } 146 | } 147 | 148 | module.exports = ApiHandler; 149 | -------------------------------------------------------------------------------- /functions/commands/activatescene.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class ActivateScene extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.ActivateScene'; 6 | } 7 | 8 | static validateParams(params) { 9 | return ('deactivate' in params && typeof params.deactivate === 'boolean') || !('deactivate' in params); 10 | } 11 | 12 | static convertParamsToValue(params, _, device) { 13 | let deactivate = params.deactivate; 14 | if (this.isInverted(device)) { 15 | deactivate = !deactivate; 16 | } 17 | return !deactivate ? 'ON' : 'OFF'; 18 | } 19 | } 20 | 21 | module.exports = ActivateScene; 22 | -------------------------------------------------------------------------------- /functions/commands/appselect.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | const TV = require('../devices/tv.js'); 3 | 4 | class AppSelect extends DefaultCommand { 5 | static get type() { 6 | return 'action.devices.commands.appSelect'; 7 | } 8 | 9 | static validateParams(params) { 10 | return ( 11 | ('newApplication' in params && typeof params.newApplication === 'string') || 12 | ('newApplicationName' in params && typeof params.newApplicationName === 'string') 13 | ); 14 | } 15 | 16 | static requiresItem() { 17 | return true; 18 | } 19 | 20 | static getItemName(device) { 21 | const members = this.getMembers(device); 22 | if ('tvApplication' in members) { 23 | return members.tvApplication; 24 | } 25 | throw { statusCode: 400 }; 26 | } 27 | 28 | static convertParamsToValue(params, item) { 29 | const applicationMap = TV.getApplicationMap(item); 30 | if (params.newApplication && params.newApplication in applicationMap) { 31 | return params.newApplication; 32 | } 33 | const search = params.newApplicationName; 34 | for (const key in applicationMap) { 35 | if (applicationMap[key].includes(search)) { 36 | return key; 37 | } 38 | } 39 | throw { errorCode: 'noAvailableApp' }; 40 | } 41 | 42 | static getResponseStates(params, item) { 43 | return { 44 | currentApplication: this.convertParamsToValue(params, item) 45 | }; 46 | } 47 | } 48 | 49 | module.exports = AppSelect; 50 | -------------------------------------------------------------------------------- /functions/commands/armdisarm.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | const SecuritySystem = require('../devices/securitysystem.js'); 3 | 4 | class ArmDisarm extends DefaultCommand { 5 | static get type() { 6 | return 'action.devices.commands.ArmDisarm'; 7 | } 8 | 9 | static validateParams(params) { 10 | return 'arm' in params && typeof params.arm === 'boolean'; 11 | } 12 | 13 | static convertParamsToValue(params, _, device) { 14 | if (params.armLevel && this.getDeviceType(device) === 'SecuritySystem') { 15 | return params.armLevel; 16 | } 17 | let arm = params.arm; 18 | if (this.isInverted(device)) { 19 | arm = !arm; 20 | } 21 | return arm ? 'ON' : 'OFF'; 22 | } 23 | 24 | static getItemName(device, params) { 25 | if (this.getDeviceType(device) === 'SecuritySystem') { 26 | const members = this.getMembers(device); 27 | if (params.armLevel) { 28 | if (SecuritySystem.armLevelMemberName in members) { 29 | return members[SecuritySystem.armLevelMemberName]; 30 | } 31 | throw { statusCode: 400 }; 32 | } 33 | if (SecuritySystem.armedMemberName in members) { 34 | return members[SecuritySystem.armedMemberName]; 35 | } 36 | throw { statusCode: 400 }; 37 | } 38 | return device.id; 39 | } 40 | 41 | static requiresItem() { 42 | return true; 43 | } 44 | 45 | static bypassPin(device, params) { 46 | return !!(device.customData && device.customData.pinOnDisarmOnly && (params.armLevel || params.arm)); 47 | } 48 | 49 | static getResponseStates(params) { 50 | const response = { 51 | isArmed: params.arm 52 | }; 53 | if (params.armLevel) { 54 | response.currentArmLevel = params.armLevel; 55 | } 56 | return response; 57 | } 58 | 59 | static get requiresUpdateValidation() { 60 | return true; 61 | } 62 | 63 | static checkCurrentState(target, state, params) { 64 | if (target === state) { 65 | throw { errorCode: params.armLevel ? 'alreadyInState' : params.arm ? 'alreadyArmed' : 'alreadyDisarmed' }; 66 | } 67 | } 68 | 69 | static validateUpdate(params, item, device) { 70 | if (this.getDeviceType(device) === 'SecuritySystem') { 71 | const members = SecuritySystem.getMembers(item); 72 | const isCurrentlyArmed = 73 | members[SecuritySystem.armedMemberName].state === (this.isInverted(device) ? 'OFF' : 'ON'); 74 | const currentLevel = 75 | SecuritySystem.armLevelMemberName in members ? members[SecuritySystem.armLevelMemberName].state : ''; 76 | const armStatusSuccessful = params.arm === isCurrentlyArmed; 77 | const armLevelSuccessful = params.armLevel ? params.armLevel === currentLevel : true; 78 | if (!armStatusSuccessful || !armLevelSuccessful) { 79 | if (!params.arm) { 80 | throw { errorCode: 'disarmFailure' }; 81 | } else { 82 | const report = SecuritySystem.getStatusReport(item, members); 83 | if (report.length) { 84 | return { 85 | ids: [device.id], 86 | status: 'EXCEPTIONS', 87 | states: { online: true, currentStatusReport: report, ...SecuritySystem.getState(item) } 88 | }; 89 | } 90 | throw { errorCode: 'armFailure' }; 91 | } 92 | } 93 | } else { 94 | if (params.arm !== (item.state === (this.isInverted(device) ? 'OFF' : 'ON'))) { 95 | throw { errorCode: params.arm ? 'armFailure' : 'disarmFailure' }; 96 | } 97 | } 98 | } 99 | } 100 | 101 | module.exports = ArmDisarm; 102 | -------------------------------------------------------------------------------- /functions/commands/brightnessabsolute.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class BrightnessAbsolute extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.BrightnessAbsolute'; 6 | } 7 | 8 | static validateParams(params) { 9 | return 'brightness' in params && typeof params.brightness === 'number'; 10 | } 11 | 12 | static getItemName(device) { 13 | if (this.getDeviceType(device) === 'SpecialColorLight') { 14 | const members = this.getMembers(device); 15 | if ('lightBrightness' in members) { 16 | return members.lightBrightness; 17 | } 18 | throw { statusCode: 400 }; 19 | } 20 | return device.id; 21 | } 22 | 23 | static convertParamsToValue(params) { 24 | return params.brightness.toString(); 25 | } 26 | 27 | static getResponseStates(params) { 28 | return { 29 | brightness: params.brightness 30 | }; 31 | } 32 | } 33 | 34 | module.exports = BrightnessAbsolute; 35 | -------------------------------------------------------------------------------- /functions/commands/charge.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | const Charger = require('../devices/charger.js'); 3 | 4 | class Charge extends DefaultCommand { 5 | static get type() { 6 | return 'action.devices.commands.Charge'; 7 | } 8 | 9 | static validateParams(params) { 10 | return 'charge' in params && typeof params.charge === 'boolean'; 11 | } 12 | 13 | static getItemName(device) { 14 | const members = this.getMembers(device); 15 | if ('chargerCharging' in members) { 16 | return members.chargerCharging; 17 | } 18 | throw { statusCode: 400 }; 19 | } 20 | 21 | static convertParamsToValue(params, _, device) { 22 | let charge = params.charge; 23 | if (this.isInverted(device)) { 24 | charge = !charge; 25 | } 26 | return charge ? 'ON' : 'OFF'; 27 | } 28 | 29 | static getResponseStates(params, item) { 30 | const states = Charger.getState(item); 31 | states.isCharging = params.charge; 32 | return states; 33 | } 34 | } 35 | 36 | module.exports = Charge; 37 | -------------------------------------------------------------------------------- /functions/commands/colorabsolute.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class ColorAbsolute extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.ColorAbsolute'; 6 | } 7 | 8 | static validateParams(params) { 9 | return ( 10 | 'color' in params && 11 | typeof params.color === 'object' && 12 | 'spectrumHSV' in params.color && 13 | typeof params.color.spectrumHSV === 'object' 14 | ); 15 | } 16 | 17 | static getItemName(device) { 18 | if (this.getDeviceType(device) === 'SpecialColorLight') { 19 | const members = this.getMembers(device); 20 | if ('lightColor' in members) { 21 | return members.lightColor; 22 | } 23 | throw { statusCode: 400 }; 24 | } 25 | return device.id; 26 | } 27 | 28 | static convertParamsToValue(params, _, device) { 29 | if (this.getDeviceType(device) !== 'ColorLight' && this.getDeviceType(device) !== 'SpecialColorLight') { 30 | throw { statusCode: 400 }; 31 | } 32 | const hsv = params.color.spectrumHSV; 33 | return [hsv.hue, hsv.saturation * 100, hsv.value * 100].join(','); 34 | } 35 | 36 | static getResponseStates(params) { 37 | return { 38 | color: { 39 | spectrumHsv: params.color.spectrumHSV 40 | } 41 | }; 42 | } 43 | } 44 | 45 | module.exports = ColorAbsolute; 46 | -------------------------------------------------------------------------------- /functions/commands/colorabsolutetemperature.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | const { convertMired, convertRgbToHsv, convertKelvinToRgb } = require('../utilities.js'); 3 | 4 | class ColorAbsoluteTemperature extends DefaultCommand { 5 | static get type() { 6 | return 'action.devices.commands.ColorAbsolute'; 7 | } 8 | 9 | static validateParams(params) { 10 | return ( 11 | 'color' in params && 12 | typeof params.color === 'object' && 13 | 'temperature' in params.color && 14 | typeof params.color.temperature === 'number' 15 | ); 16 | } 17 | 18 | static requiresItem(device) { 19 | return this.getDeviceType(device) !== 'SpecialColorLight'; 20 | } 21 | 22 | static getItemName(device) { 23 | if (this.getDeviceType(device) === 'SpecialColorLight') { 24 | const members = this.getMembers(device); 25 | if ('lightColorTemperature' in members) { 26 | return members.lightColorTemperature; 27 | } 28 | throw { statusCode: 400 }; 29 | } 30 | return device.id; 31 | } 32 | 33 | static convertParamsToValue(params, item, device) { 34 | if (this.getDeviceType(device) === 'SpecialColorLight') { 35 | try { 36 | const customData = device.customData || {}; 37 | const colorUnit = customData.colorUnit; 38 | if (colorUnit === 'kelvin') { 39 | return params.color.temperature.toString(); 40 | } 41 | if (colorUnit === 'mired') { 42 | return convertMired(params.color.temperature).toString(); 43 | } 44 | const { temperatureMinK, temperatureMaxK } = customData.colorTemperatureRange; 45 | let percent = ((params.color.temperature - temperatureMinK) / (temperatureMaxK - temperatureMinK)) * 100; 46 | if (customData.colorTemperatureInverted) { 47 | percent = 100 - percent; 48 | } 49 | return percent.toString(); 50 | } catch (error) { 51 | return '0'; 52 | } 53 | } 54 | const hsv = convertRgbToHsv(convertKelvinToRgb(params.color.temperature)); 55 | const hsvArray = item.state.split(',').map((val) => Number(val)); 56 | return [Math.round(hsv.hue * 100) / 100, Math.round(hsv.saturation * 1000) / 10, hsvArray[2]].join(','); 57 | } 58 | 59 | static getResponseStates(params) { 60 | return { 61 | color: { 62 | temperatureK: params.color.temperature 63 | } 64 | }; 65 | } 66 | } 67 | 68 | module.exports = ColorAbsoluteTemperature; 69 | -------------------------------------------------------------------------------- /functions/commands/getcamerastream.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class GetCameraStream extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.GetCameraStream'; 6 | } 7 | 8 | static validateParams(params) { 9 | return ( 10 | 'StreamToChromecast' in params && 11 | typeof params.StreamToChromecast === 'boolean' && 12 | 'SupportedStreamProtocols' in params && 13 | typeof params.SupportedStreamProtocols === 'object' 14 | ); 15 | } 16 | 17 | static requiresItem() { 18 | return true; 19 | } 20 | 21 | static convertParamsToValue() { 22 | return null; 23 | } 24 | 25 | static getResponseStates(_, item) { 26 | return { 27 | cameraStreamAccessUrl: item.state 28 | }; 29 | } 30 | } 31 | 32 | module.exports = GetCameraStream; 33 | -------------------------------------------------------------------------------- /functions/commands/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const Commands = []; 4 | 5 | fs.readdirSync(__dirname).forEach((file) => { 6 | if (file === 'index.js' || !file.endsWith('.js')) return; 7 | const command = require(`./${file}`); 8 | if (command.type) { 9 | Commands.push(command); 10 | } 11 | }); 12 | 13 | module.exports = { 14 | /** 15 | * @param {string} command 16 | * @param {object} params 17 | */ 18 | getCommandType: (command, params) => { 19 | return Commands.find((commandType) => command === commandType.type && commandType.validateParams(params)); 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /functions/commands/lockunlock.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class LockUnlock extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.LockUnlock'; 6 | } 7 | 8 | static validateParams(params) { 9 | return 'lock' in params && typeof params.lock === 'boolean'; 10 | } 11 | 12 | static convertParamsToValue(params, _, device) { 13 | if (this.getItemType(device) === 'Contact') { 14 | throw { statusCode: 400 }; 15 | } 16 | let lock = params.lock; 17 | if (this.isInverted(device)) { 18 | lock = !lock; 19 | } 20 | return lock ? 'ON' : 'OFF'; 21 | } 22 | 23 | static getResponseStates(params) { 24 | return { 25 | isLocked: params.lock 26 | }; 27 | } 28 | 29 | static checkCurrentState(target, state, params) { 30 | if (target === state) { 31 | throw { errorCode: params.lock ? 'alreadyLocked' : 'alreadyUnlocked' }; 32 | } 33 | } 34 | } 35 | 36 | module.exports = LockUnlock; 37 | -------------------------------------------------------------------------------- /functions/commands/medianext.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class MediaNext extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.mediaNext'; 6 | } 7 | 8 | static getItemName(device) { 9 | const members = this.getMembers(device); 10 | if ('tvTransport' in members) { 11 | return members.tvTransport; 12 | } 13 | throw { statusCode: 400 }; 14 | } 15 | 16 | static convertParamsToValue() { 17 | return 'NEXT'; 18 | } 19 | } 20 | 21 | module.exports = MediaNext; 22 | -------------------------------------------------------------------------------- /functions/commands/mediapause.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class MediaPause extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.mediaPause'; 6 | } 7 | 8 | static getItemName(device) { 9 | const members = this.getMembers(device); 10 | if ('tvTransport' in members) { 11 | return members.tvTransport; 12 | } 13 | throw { statusCode: 400 }; 14 | } 15 | 16 | static convertParamsToValue() { 17 | return 'PAUSE'; 18 | } 19 | } 20 | 21 | module.exports = MediaPause; 22 | -------------------------------------------------------------------------------- /functions/commands/mediaprevious.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class MediaPrevious extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.mediaPrevious'; 6 | } 7 | 8 | static getItemName(device) { 9 | const members = this.getMembers(device); 10 | if ('tvTransport' in members) { 11 | return members.tvTransport; 12 | } 13 | throw { statusCode: 400 }; 14 | } 15 | 16 | static convertParamsToValue() { 17 | return 'PREVIOUS'; 18 | } 19 | } 20 | 21 | module.exports = MediaPrevious; 22 | -------------------------------------------------------------------------------- /functions/commands/mediaresume.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class MediaResume extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.mediaResume'; 6 | } 7 | 8 | static getItemName(device) { 9 | const members = this.getMembers(device); 10 | if ('tvTransport' in members) { 11 | return members.tvTransport; 12 | } 13 | throw { statusCode: 400 }; 14 | } 15 | 16 | static convertParamsToValue() { 17 | return 'PLAY'; 18 | } 19 | } 20 | 21 | module.exports = MediaResume; 22 | -------------------------------------------------------------------------------- /functions/commands/mute.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class Mute extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.mute'; 6 | } 7 | 8 | static validateParams(params) { 9 | return 'mute' in params && typeof params.mute === 'boolean'; 10 | } 11 | 12 | static getItemName(device) { 13 | if (this.getDeviceType(device) === 'TV') { 14 | const members = this.getMembers(device); 15 | if ('tvMute' in members) { 16 | return members.tvMute; 17 | } 18 | if ('tvVolume' in members) { 19 | return members.tvVolume; 20 | } 21 | throw { statusCode: 400 }; 22 | } 23 | return device.id; 24 | } 25 | 26 | static convertParamsToValue(params, item, device) { 27 | let itemType = this.getItemType(device); 28 | if (this.getDeviceType(device) === 'TV') { 29 | const members = this.getMembers(device); 30 | if ('tvMute' in members) { 31 | itemType = 'Switch'; 32 | } 33 | } 34 | let mute = params.mute; 35 | if (itemType !== 'Switch') { 36 | return mute ? '0' : null; 37 | } 38 | if (this.isInverted(device)) { 39 | mute = !mute; 40 | } 41 | return mute ? 'ON' : 'OFF'; 42 | } 43 | 44 | static getResponseStates(params) { 45 | return { 46 | isMuted: params.mute 47 | }; 48 | } 49 | } 50 | 51 | module.exports = Mute; 52 | -------------------------------------------------------------------------------- /functions/commands/onoff.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class OnOff extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.OnOff'; 6 | } 7 | 8 | static validateParams(params) { 9 | return 'on' in params && typeof params.on === 'boolean'; 10 | } 11 | 12 | static getItemName(device) { 13 | const deviceType = this.getDeviceType(device); 14 | if (deviceType.startsWith('DynamicModes')) { 15 | throw { statusCode: 400 }; 16 | } 17 | const members = this.getMembers(device); 18 | if (deviceType === 'SpecialColorLight') { 19 | if ('lightPower' in members) { 20 | return members.lightPower; 21 | } 22 | if ('lightBrightness' in members) { 23 | return members.lightBrightness; 24 | } 25 | throw { statusCode: 400 }; 26 | } 27 | if (deviceType === 'TV') { 28 | if ('tvPower' in members) { 29 | return members.tvPower; 30 | } 31 | throw { statusCode: 400 }; 32 | } 33 | if (['AirPurifier', 'Fan', 'Hood', 'ACUnit'].includes(deviceType) && this.getItemType(device) === 'Group') { 34 | if ('fanPower' in members) { 35 | return members.fanPower; 36 | } 37 | throw { statusCode: 400 }; 38 | } 39 | return device.id; 40 | } 41 | 42 | static convertParamsToValue(params, _, device) { 43 | let on = params.on; 44 | if (this.isInverted(device)) { 45 | on = !on; 46 | } 47 | return on ? 'ON' : 'OFF'; 48 | } 49 | 50 | static getResponseStates(params) { 51 | return { 52 | on: params.on 53 | }; 54 | } 55 | 56 | static checkCurrentState(target, state, params) { 57 | if (target === state) { 58 | throw { errorCode: params.on ? 'alreadyOn' : 'alreadyOff' }; 59 | } 60 | } 61 | } 62 | 63 | module.exports = OnOff; 64 | -------------------------------------------------------------------------------- /functions/commands/openclose.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class OpenClose extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.OpenClose'; 6 | } 7 | 8 | static validateParams(params) { 9 | return 'openPercent' in params && typeof params.openPercent === 'number'; 10 | } 11 | 12 | static convertParamsToValue(params, _, device) { 13 | const itemType = this.getItemType(device); 14 | if (itemType === 'Contact') { 15 | throw { statusCode: 400 }; 16 | } 17 | let openPercent = params.openPercent; 18 | if (this.isInverted(device)) { 19 | openPercent = 100 - openPercent; 20 | } 21 | if (itemType === 'Rollershutter') { 22 | return openPercent === 0 ? 'DOWN' : openPercent === 100 ? 'UP' : (100 - openPercent).toString(); 23 | } 24 | if (itemType === 'Switch') { 25 | return openPercent === 0 ? 'OFF' : 'ON'; 26 | } 27 | return openPercent.toString(); 28 | } 29 | 30 | static getResponseStates(params) { 31 | return { 32 | openPercent: params.openPercent 33 | }; 34 | } 35 | 36 | static checkCurrentState(target, state, params) { 37 | const adjustedTarget = target === 'DOWN' ? '100' : target === 'UP' ? '0' : target; 38 | if (adjustedTarget === state) { 39 | throw { errorCode: params.openPercent === 0 ? 'alreadyClosed' : 'alreadyOpen' }; 40 | } 41 | } 42 | } 43 | 44 | module.exports = OpenClose; 45 | -------------------------------------------------------------------------------- /functions/commands/selectchannel.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | const TV = require('../devices/tv.js'); 3 | 4 | class SelectChannel extends DefaultCommand { 5 | static get type() { 6 | return 'action.devices.commands.selectChannel'; 7 | } 8 | 9 | static validateParams(params) { 10 | return ( 11 | ('channelCode' in params && typeof params.channelCode === 'string') || 12 | ('channelName' in params && typeof params.channelName === 'string') || 13 | ('channelNumber' in params && typeof params.channelNumber === 'string') 14 | ); 15 | } 16 | 17 | static requiresItem() { 18 | return true; 19 | } 20 | 21 | static getItemName(device) { 22 | const members = this.getMembers(device); 23 | if ('tvChannel' in members) { 24 | return members.tvChannel; 25 | } 26 | throw { statusCode: 400 }; 27 | } 28 | 29 | static convertParamsToValue(params, item) { 30 | const channelMap = TV.getChannelMap(item); 31 | if (params.channelNumber && params.channelNumber in channelMap) { 32 | return params.channelNumber; 33 | } 34 | const search = params.channelName || params.channelCode; 35 | for (const number in channelMap) { 36 | if (channelMap[number].some((name) => name.toLowerCase() === search.toLowerCase())) { 37 | return number; 38 | } 39 | } 40 | throw { errorCode: 'noAvailableChannel' }; 41 | } 42 | 43 | static getResponseStates(params, item) { 44 | return { 45 | channelNumber: this.convertParamsToValue(params, item) 46 | }; 47 | } 48 | } 49 | 50 | module.exports = SelectChannel; 51 | -------------------------------------------------------------------------------- /functions/commands/setfanspeed.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class SetFanSpeed extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.SetFanSpeed'; 6 | } 7 | 8 | static validateParams(params) { 9 | return ( 10 | ('fanSpeed' in params && typeof params.fanSpeed === 'string') || 11 | ('fanSpeedPercent' in params && typeof params.fanSpeedPercent === 'number') 12 | ); 13 | } 14 | 15 | static getItemName(device) { 16 | const deviceType = this.getDeviceType(device); 17 | if (['AirPurifier', 'Fan', 'Hood', 'ACUnit'].includes(deviceType) && this.getItemType(device) === 'Group') { 18 | const members = this.getMembers(device); 19 | if ('fanSpeed' in members) { 20 | return members.fanSpeed; 21 | } 22 | throw { statusCode: 400 }; 23 | } 24 | return device.id; 25 | } 26 | 27 | static convertParamsToValue(params) { 28 | return (params.fanSpeed || params.fanSpeedPercent).toString(); 29 | } 30 | 31 | static getResponseStates(params) { 32 | const states = { 33 | currentFanSpeedPercent: params.fanSpeedPercent || Number(params.fanSpeed) 34 | }; 35 | if ('fanSpeed' in params) { 36 | states.currentFanSpeedSetting = params.fanSpeed; 37 | } 38 | return states; 39 | } 40 | } 41 | 42 | module.exports = SetFanSpeed; 43 | -------------------------------------------------------------------------------- /functions/commands/setinput.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class SetInput extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.SetInput'; 6 | } 7 | 8 | static validateParams(params) { 9 | return 'newInput' in params && typeof params.newInput === 'string'; 10 | } 11 | 12 | static getItemName(device) { 13 | const members = this.getMembers(device); 14 | if ('tvInput' in members) { 15 | return members.tvInput; 16 | } 17 | throw { statusCode: 400 }; 18 | } 19 | 20 | static convertParamsToValue(params) { 21 | return params.newInput; 22 | } 23 | 24 | static getResponseStates(params) { 25 | return { 26 | currentInput: params.newInput 27 | }; 28 | } 29 | } 30 | 31 | module.exports = SetInput; 32 | -------------------------------------------------------------------------------- /functions/commands/setmodes.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class SetModes extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.SetModes'; 6 | } 7 | 8 | static validateParams(params) { 9 | return 'updateModeSettings' in params && typeof params.updateModeSettings === 'object'; 10 | } 11 | 12 | static getItemName(device) { 13 | const deviceType = this.getDeviceType(device); 14 | const members = this.getMembers(device); 15 | if (deviceType.startsWith('DynamicModes')) { 16 | if ('modesCurrentMode' in members) { 17 | return members.modesCurrentMode; 18 | } 19 | throw { statusCode: 400 }; 20 | } 21 | if (['AirPurifier', 'Fan', 'Hood', 'ACUnit'].includes(deviceType)) { 22 | if ('fanMode' in members) { 23 | return members.fanMode; 24 | } 25 | throw { statusCode: 400 }; 26 | } 27 | return device.id; 28 | } 29 | 30 | static convertParamsToValue(params) { 31 | const mode = Object.keys(params.updateModeSettings)[0]; 32 | return params.updateModeSettings[mode].toString(); 33 | } 34 | 35 | static getResponseStates(params) { 36 | const mode = Object.keys(params.updateModeSettings)[0]; 37 | return { 38 | currentModeSettings: { 39 | [mode]: params.updateModeSettings[mode] 40 | } 41 | }; 42 | } 43 | } 44 | 45 | module.exports = SetModes; 46 | -------------------------------------------------------------------------------- /functions/commands/setvolume.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class SetVolume extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.setVolume'; 6 | } 7 | 8 | static validateParams(params) { 9 | return 'volumeLevel' in params && typeof params.volumeLevel === 'number'; 10 | } 11 | 12 | static getItemName(device) { 13 | if (this.getDeviceType(device) === 'TV') { 14 | const members = this.getMembers(device); 15 | if ('tvVolume' in members) { 16 | return members.tvVolume; 17 | } 18 | throw { statusCode: 400 }; 19 | } 20 | return device.id; 21 | } 22 | 23 | static convertParamsToValue(params) { 24 | return params.volumeLevel.toString(); 25 | } 26 | 27 | static getResponseStates(params) { 28 | return { 29 | currentVolume: params.volumeLevel 30 | }; 31 | } 32 | 33 | static checkCurrentState(target, state) { 34 | if (target === state) { 35 | throw { 36 | errorCode: state === '100' ? 'volumeAlreadyMax' : state === '0' ? 'volumeAlreadyMin' : 'alreadyInState' 37 | }; 38 | } 39 | } 40 | } 41 | 42 | module.exports = SetVolume; 43 | -------------------------------------------------------------------------------- /functions/commands/startstop.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | 3 | class StartStop extends DefaultCommand { 4 | static get type() { 5 | return 'action.devices.commands.StartStop'; 6 | } 7 | 8 | static validateParams(params) { 9 | return 'start' in params && typeof params.start === 'boolean'; 10 | } 11 | 12 | static convertParamsToValue(params, _, device) { 13 | const itemType = this.getItemType(device); 14 | if (itemType === 'Contact') { 15 | throw { statusCode: 400 }; 16 | } 17 | if (itemType === 'Rollershutter') { 18 | return params.start ? 'MOVE' : 'STOP'; 19 | } 20 | return params.start ? 'ON' : 'OFF'; 21 | } 22 | 23 | static getResponseStates(params) { 24 | return { 25 | isRunning: params.start, 26 | isPaused: !params.start 27 | }; 28 | } 29 | 30 | static checkCurrentState(target, state, params) { 31 | if (target === state) { 32 | throw { errorCode: params.start ? 'alreadyStarted' : 'alreadyStopped' }; 33 | } 34 | } 35 | } 36 | 37 | module.exports = StartStop; 38 | -------------------------------------------------------------------------------- /functions/commands/thermostatsetmode.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | const Thermostat = require('../devices/thermostat.js'); 3 | 4 | class ThermostatSetMode extends DefaultCommand { 5 | static get type() { 6 | return 'action.devices.commands.ThermostatSetMode'; 7 | } 8 | 9 | static validateParams(params) { 10 | return 'thermostatMode' in params && typeof params.thermostatMode === 'string'; 11 | } 12 | 13 | static requiresItem() { 14 | return true; 15 | } 16 | 17 | static getItemName(device) { 18 | const members = this.getMembers(device); 19 | if ('thermostatMode' in members) { 20 | return members.thermostatMode; 21 | } 22 | throw { statusCode: 400 }; 23 | } 24 | 25 | static convertParamsToValue(params, item) { 26 | return Thermostat.translateModeToOpenhab(item, params.thermostatMode); 27 | } 28 | 29 | static getResponseStates(params, item) { 30 | const states = Thermostat.getState(item); 31 | states.thermostatMode = params.thermostatMode; 32 | return states; 33 | } 34 | } 35 | 36 | module.exports = ThermostatSetMode; 37 | -------------------------------------------------------------------------------- /functions/commands/thermostattemperaturesetpoint.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | const Thermostat = require('../devices/thermostat.js'); 3 | const convertCelsiusToFahrenheit = require('../utilities.js').convertCelsiusToFahrenheit; 4 | 5 | class ThermostatTemperatureSetpoint extends DefaultCommand { 6 | static get type() { 7 | return 'action.devices.commands.ThermostatTemperatureSetpoint'; 8 | } 9 | 10 | static validateParams(params) { 11 | return 'thermostatTemperatureSetpoint' in params && typeof params.thermostatTemperatureSetpoint === 'number'; 12 | } 13 | 14 | static requiresItem() { 15 | return true; 16 | } 17 | 18 | static getItemName(device) { 19 | const members = this.getMembers(device); 20 | if ('thermostatTemperatureSetpoint' in members) { 21 | return members.thermostatTemperatureSetpoint; 22 | } 23 | throw { statusCode: 400 }; 24 | } 25 | 26 | static convertParamsToValue(params, item) { 27 | let value = params.thermostatTemperatureSetpoint; 28 | if (Thermostat.useFahrenheit(item)) { 29 | value = convertCelsiusToFahrenheit(value); 30 | } 31 | return value.toString(); 32 | } 33 | 34 | static getResponseStates(params, item) { 35 | const states = Thermostat.getState(item); 36 | states.thermostatTemperatureSetpoint = params.thermostatTemperatureSetpoint; 37 | return states; 38 | } 39 | } 40 | 41 | module.exports = ThermostatTemperatureSetpoint; 42 | -------------------------------------------------------------------------------- /functions/commands/thermostattemperaturesetpointhigh.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | const Thermostat = require('../devices/thermostat.js'); 3 | const convertCelsiusToFahrenheit = require('../utilities.js').convertCelsiusToFahrenheit; 4 | 5 | class ThermostatTemperatureSetpointHigh extends DefaultCommand { 6 | static get type() { 7 | return 'action.devices.commands.ThermostatTemperatureSetpointHigh'; 8 | } 9 | 10 | static validateParams(params) { 11 | return ( 12 | 'thermostatTemperatureSetpointHigh' in params && typeof params.thermostatTemperatureSetpointHigh === 'number' 13 | ); 14 | } 15 | 16 | static requiresItem() { 17 | return true; 18 | } 19 | 20 | static getItemName(device) { 21 | const members = this.getMembers(device); 22 | if ('thermostatTemperatureSetpointHigh' in members) { 23 | return members.thermostatTemperatureSetpointHigh; 24 | } 25 | throw { statusCode: 400 }; 26 | } 27 | 28 | static convertParamsToValue(params, item) { 29 | let value = params.thermostatTemperatureSetpointHigh; 30 | if (Thermostat.useFahrenheit(item)) { 31 | value = convertCelsiusToFahrenheit(value); 32 | } 33 | return value.toString(); 34 | } 35 | 36 | static getResponseStates(params, item) { 37 | const states = Thermostat.getState(item); 38 | states.thermostatTemperatureSetpointHigh = params.thermostatTemperatureSetpointHigh; 39 | return states; 40 | } 41 | } 42 | 43 | module.exports = ThermostatTemperatureSetpointHigh; 44 | -------------------------------------------------------------------------------- /functions/commands/thermostattemperaturesetpointlow.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | const Thermostat = require('../devices/thermostat.js'); 3 | const convertCelsiusToFahrenheit = require('../utilities.js').convertCelsiusToFahrenheit; 4 | 5 | class ThermostatTemperatureSetpointLow extends DefaultCommand { 6 | static get type() { 7 | return 'action.devices.commands.ThermostatTemperatureSetpointLow'; 8 | } 9 | 10 | static validateParams(params) { 11 | return 'thermostatTemperatureSetpointLow' in params && typeof params.thermostatTemperatureSetpointLow === 'number'; 12 | } 13 | 14 | static requiresItem() { 15 | return true; 16 | } 17 | 18 | static getItemName(device) { 19 | const members = this.getMembers(device); 20 | if ('thermostatTemperatureSetpointLow' in members) { 21 | return members.thermostatTemperatureSetpointLow; 22 | } 23 | throw { statusCode: 400 }; 24 | } 25 | 26 | static convertParamsToValue(params, item) { 27 | let value = params.thermostatTemperatureSetpointLow; 28 | if (Thermostat.useFahrenheit(item)) { 29 | value = convertCelsiusToFahrenheit(value); 30 | } 31 | return value.toString(); 32 | } 33 | 34 | static getResponseStates(params, item) { 35 | const states = Thermostat.getState(item); 36 | states.thermostatTemperatureSetpointLow = params.thermostatTemperatureSetpointLow; 37 | return states; 38 | } 39 | } 40 | 41 | module.exports = ThermostatTemperatureSetpointLow; 42 | -------------------------------------------------------------------------------- /functions/commands/volumerelative.js: -------------------------------------------------------------------------------- 1 | const DefaultCommand = require('./default.js'); 2 | const TV = require('../devices/tv.js'); 3 | 4 | class VolumeRelative extends DefaultCommand { 5 | static get type() { 6 | return 'action.devices.commands.volumeRelative'; 7 | } 8 | 9 | static validateParams(params) { 10 | return 'relativeSteps' in params && typeof params.relativeSteps === 'number'; 11 | } 12 | 13 | static requiresItem() { 14 | return true; 15 | } 16 | 17 | static getItemName(device) { 18 | if (this.getDeviceType(device) === 'TV') { 19 | const members = this.getMembers(device); 20 | if ('tvVolume' in members) { 21 | return members.tvVolume; 22 | } 23 | throw { statusCode: 400 }; 24 | } 25 | return device.id; 26 | } 27 | 28 | static convertParamsToValue(params, item, device) { 29 | let state = item.state; 30 | if (this.getDeviceType(device) === 'TV') { 31 | const members = TV.getMembers(item); 32 | if ('tvVolume' in members) { 33 | state = members.tvVolume.state; 34 | } else { 35 | throw { statusCode: 400 }; 36 | } 37 | } 38 | const level = parseInt(state) + params.relativeSteps; 39 | return (level < 0 ? 0 : level > 100 ? 100 : level).toString(); 40 | } 41 | 42 | static getResponseStates(params, item, device) { 43 | return { 44 | currentVolume: parseInt(this.convertParamsToValue(params, item, device)) 45 | }; 46 | } 47 | 48 | static checkCurrentState(target, state) { 49 | if (target === state) { 50 | throw { 51 | errorCode: state === '100' ? 'volumeAlreadyMax' : state === '0' ? 'volumeAlreadyMin' : 'alreadyInState' 52 | }; 53 | } 54 | } 55 | } 56 | 57 | module.exports = VolumeRelative; 58 | -------------------------------------------------------------------------------- /functions/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010-2019 Contributors to the openHAB project 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | 14 | /** 15 | * This is the main Backend endpoint configuration. 16 | * Change the values according your deployment 17 | * 18 | * @author Mehmet Arziman - Initial contribution 19 | * 20 | * host 21 | * REST https host 22 | * 23 | * port 24 | * REST https port 25 | * 26 | * userpass 27 | * Optional username:password for the REST server 28 | * by default oauth2 tokens will be used for authentication, uncomment this 29 | * to use standard BASIC auth when talking directly to a openHAB server. 30 | * 31 | * path 32 | * Base URL path for openHAB items 33 | * 34 | * */ 35 | module.exports = { 36 | // userpass: process.env.OH_USERPASS || 'user@foo.com:Password1', 37 | host: process.env.OH_HOST || '', 38 | port: parseInt(process.env.OH_PORT) || 443, 39 | path: process.env.OH_PATH || '/YOUR/REST/ENDPOINT' 40 | }; 41 | -------------------------------------------------------------------------------- /functions/devices/acunit.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | const Fan = require('./fan'); 3 | const Thermostat = require('./thermostat'); 4 | 5 | class ACUnit extends DefaultDevice { 6 | static get type() { 7 | return 'action.devices.types.AC_UNIT'; 8 | } 9 | 10 | static getTraits(item) { 11 | return [...Fan.getTraits(item), ...Thermostat.getTraits()]; 12 | } 13 | 14 | static get requiredItemTypes() { 15 | return ['Group']; 16 | } 17 | 18 | static matchesDeviceType(item) { 19 | return super.matchesDeviceType(item) && Object.keys(this.getMembers(item)).length > 0; 20 | } 21 | 22 | static getAttributes(item) { 23 | return { 24 | ...Fan.getAttributes(item), 25 | ...Thermostat.getAttributes(item) 26 | }; 27 | } 28 | 29 | static getState(item) { 30 | return { 31 | ...Fan.getState(item), 32 | ...Thermostat.getState(item) 33 | }; 34 | } 35 | 36 | static get supportedMembers() { 37 | return [...Fan.supportedMembers, ...Thermostat.supportedMembers]; 38 | } 39 | } 40 | 41 | module.exports = ACUnit; 42 | -------------------------------------------------------------------------------- /functions/devices/airpurifier.js: -------------------------------------------------------------------------------- 1 | const Fan = require('./fan.js'); 2 | 3 | class AirPurifier extends Fan { 4 | static get type() { 5 | return 'action.devices.types.AIRPURIFIER'; 6 | } 7 | } 8 | 9 | module.exports = AirPurifier; 10 | -------------------------------------------------------------------------------- /functions/devices/awning.js: -------------------------------------------------------------------------------- 1 | const OpenCloseDevice = require('./openclosedevice.js'); 2 | 3 | class Awning extends OpenCloseDevice { 4 | static get type() { 5 | return 'action.devices.types.AWNING'; 6 | } 7 | } 8 | 9 | module.exports = Awning; 10 | -------------------------------------------------------------------------------- /functions/devices/blinds.js: -------------------------------------------------------------------------------- 1 | const OpenCloseDevice = require('./openclosedevice.js'); 2 | 3 | class Blinds extends OpenCloseDevice { 4 | static get type() { 5 | return 'action.devices.types.BLINDS'; 6 | } 7 | } 8 | 9 | module.exports = Blinds; 10 | -------------------------------------------------------------------------------- /functions/devices/camera.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class Camera extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.CAMERA'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.CameraStream']; 10 | } 11 | 12 | static getAttributes(item) { 13 | const config = this.getConfig(item); 14 | return { 15 | cameraStreamSupportedProtocols: (config.protocols || 'hls,dash,smooth_stream,progressive_mp4') 16 | .split(',') 17 | .map((s) => s.trim()), 18 | cameraStreamNeedAuthToken: !!config.token, 19 | cameraStreamNeedDrmEncryption: false 20 | }; 21 | } 22 | 23 | static get requiredItemTypes() { 24 | return ['String']; 25 | } 26 | } 27 | 28 | module.exports = Camera; 29 | -------------------------------------------------------------------------------- /functions/devices/charger.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class Charger extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.CHARGER'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.EnergyStorage']; 10 | } 11 | 12 | static get requiredItemTypes() { 13 | return ['Group']; 14 | } 15 | 16 | static matchesDeviceType(item) { 17 | return super.matchesDeviceType(item) && Object.keys(this.getMembers(item)).length > 0; 18 | } 19 | 20 | static getAttributes(item) { 21 | const config = this.getConfig(item); 22 | const members = this.getMembers(item); 23 | const attributes = { 24 | isRechargeable: !!config.isRechargeable, 25 | queryOnlyEnergyStorage: !('chargerCharging' in members) 26 | }; 27 | return attributes; 28 | } 29 | 30 | static getState(item) { 31 | const state = {}; 32 | const config = this.getConfig(item); 33 | const members = this.getMembers(item); 34 | for (const member in members) { 35 | switch (member) { 36 | case 'chargerCharging': 37 | state.isCharging = members[member].state === 'ON'; 38 | break; 39 | case 'chargerPluggedIn': 40 | state.isPluggedIn = members[member].state === 'ON'; 41 | break; 42 | case 'chargerCapacityRemaining': { 43 | const capacity = Math.round(parseFloat(members[member].state)); 44 | if (!config.unit || config.unit === 'PERCENTAGE') { 45 | let descCapacity = 'UNKNOWN'; 46 | if (capacity <= 10) { 47 | descCapacity = 'CRITICALLY_LOW'; 48 | } else if (capacity <= 40) { 49 | descCapacity = 'LOW'; 50 | } else if (capacity <= 75) { 51 | descCapacity = 'MEDIUM'; 52 | } else if (capacity < 100) { 53 | descCapacity = 'HIGH'; 54 | } else { 55 | descCapacity = 'FULL'; 56 | } 57 | state.descriptiveCapacityRemaining = descCapacity; 58 | } 59 | state.capacityRemaining = [ 60 | { 61 | unit: config.unit || 'PERCENTAGE', 62 | rawValue: capacity 63 | } 64 | ]; 65 | break; 66 | } 67 | case 'chargerCapacityUntilFull': { 68 | state.capacityUntilFull = [ 69 | { 70 | unit: config.unit || 'PERCENTAGE', 71 | rawValue: Math.round(parseFloat(members[member].state)) 72 | } 73 | ]; 74 | break; 75 | } 76 | } 77 | } 78 | return state; 79 | } 80 | 81 | static get supportedMembers() { 82 | return [ 83 | { name: 'chargerCharging', types: ['Switch'] }, 84 | { name: 'chargerPluggedIn', types: ['Switch'] }, 85 | { name: 'chargerCapacityRemaining', types: ['Number', 'Dimmer'] }, 86 | { name: 'chargerCapacityUntilFull', types: ['Number', 'Dimmer'] } 87 | ]; 88 | } 89 | } 90 | 91 | module.exports = Charger; 92 | -------------------------------------------------------------------------------- /functions/devices/climatesensor.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | const convertFahrenheitToCelsius = require('../utilities.js').convertFahrenheitToCelsius; 3 | 4 | class ClimateSensor extends DefaultDevice { 5 | static get type() { 6 | return 'action.devices.types.SENSOR'; 7 | } 8 | 9 | static getTraits(item) { 10 | const traits = []; 11 | const members = this.getMembers(item); 12 | if ('temperatureAmbient' in members) 13 | traits.push('action.devices.traits.TemperatureSetting', 'action.devices.traits.TemperatureControl'); 14 | if ('humidityAmbient' in members) traits.push('action.devices.traits.HumiditySetting'); 15 | return traits; 16 | } 17 | 18 | static getAttributes(item) { 19 | const attributes = {}; 20 | const members = this.getMembers(item); 21 | if ('temperatureAmbient' in members) { 22 | attributes.queryOnlyTemperatureControl = true; 23 | attributes.temperatureUnitForUX = this.useFahrenheit(item) ? 'F' : 'C'; 24 | attributes.queryOnlyTemperatureSetting = true; 25 | attributes.thermostatTemperatureUnit = this.useFahrenheit(item) === true ? 'F' : 'C'; 26 | attributes.temperatureRange = { 27 | minThresholdCelsius: -100, 28 | maxThresholdCelsius: 100 29 | }; 30 | 31 | const config = this.getConfig(item); 32 | if ('temperatureRange' in config) { 33 | const [min, max] = config.temperatureRange.split(',').map((s) => parseFloat(s.trim())); 34 | if (!isNaN(min) && !isNaN(max)) { 35 | attributes.temperatureRange = { 36 | minThresholdCelsius: min, 37 | maxThresholdCelsius: max 38 | }; 39 | } 40 | } 41 | } 42 | if ('humidityAmbient' in members) { 43 | attributes.queryOnlyHumiditySetting = true; 44 | } 45 | return attributes; 46 | } 47 | 48 | static get requiredItemTypes() { 49 | return ['Group']; 50 | } 51 | 52 | static matchesDeviceType(item) { 53 | return ( 54 | item.metadata && 55 | item.metadata.ga && 56 | item.metadata.ga.value.toLowerCase() === 'climatesensor' && 57 | Object.keys(this.getMembers(item)).length > 0 58 | ); 59 | } 60 | 61 | static getState(item) { 62 | const state = {}; 63 | const members = this.getMembers(item); 64 | if ('temperatureAmbient' in members) { 65 | let temperature = Number(parseFloat(members.temperatureAmbient.state).toFixed(1)); 66 | if (this.useFahrenheit(item)) { 67 | temperature = convertFahrenheitToCelsius(temperature); 68 | } 69 | state.thermostatTemperatureAmbient = temperature; 70 | state.temperatureAmbientCelsius = temperature; 71 | state.temperatureSetpointCelsius = temperature; 72 | } 73 | if ('humidityAmbient' in members) { 74 | const config = this.getConfig(item); 75 | const maxHumidity = (config.maxHumidity && parseInt(config.maxHumidity)) || 100; 76 | const humidity = Math.round(parseFloat(members.humidityAmbient.state) * (100 / maxHumidity)); 77 | state.humidityAmbientPercent = humidity; 78 | state.humiditySetpointPercent = humidity; 79 | } 80 | return state; 81 | } 82 | 83 | static get supportedMembers() { 84 | return [ 85 | { name: 'temperatureAmbient', types: ['Number'] }, 86 | { name: 'humidityAmbient', types: ['Number'] } 87 | ]; 88 | } 89 | 90 | static useFahrenheit(item) { 91 | const config = this.getConfig(item); 92 | return config.thermostatTemperatureUnit === 'F' || config.useFahrenheit === true; 93 | } 94 | } 95 | 96 | module.exports = ClimateSensor; 97 | -------------------------------------------------------------------------------- /functions/devices/coffeemaker.js: -------------------------------------------------------------------------------- 1 | const Switch = require('./switch.js'); 2 | 3 | class CoffeeMaker extends Switch { 4 | static get type() { 5 | return 'action.devices.types.COFFEE_MAKER'; 6 | } 7 | } 8 | 9 | module.exports = CoffeeMaker; 10 | -------------------------------------------------------------------------------- /functions/devices/colorlight.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class ColorLight extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.LIGHT'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.OnOff', 'action.devices.traits.Brightness', 'action.devices.traits.ColorSetting']; 10 | } 11 | 12 | static getAttributes(item) { 13 | const attributes = { 14 | colorModel: 'hsv' 15 | }; 16 | const config = this.getConfig(item); 17 | if ('colorTemperatureRange' in config) { 18 | const [min, max] = config.colorTemperatureRange.split(',').map((s) => Number(s.trim())); 19 | if (!isNaN(min) && !isNaN(max)) { 20 | attributes.colorTemperatureRange = { 21 | temperatureMinK: min, 22 | temperatureMaxK: max 23 | }; 24 | } 25 | } 26 | return attributes; 27 | } 28 | 29 | static get requiredItemTypes() { 30 | return ['Color']; 31 | } 32 | 33 | static getState(item) { 34 | const [hue, sat, val] = item.state.split(',').map((s) => Number(s.trim())); 35 | return { 36 | on: val > 0, 37 | brightness: Math.round(val), 38 | color: { 39 | spectrumHSV: { 40 | hue: hue, 41 | saturation: sat / 100, 42 | value: val / 100 43 | } 44 | } 45 | }; 46 | } 47 | } 48 | 49 | module.exports = ColorLight; 50 | -------------------------------------------------------------------------------- /functions/devices/curtain.js: -------------------------------------------------------------------------------- 1 | const OpenCloseDevice = require('./openclosedevice.js'); 2 | 3 | class Curtain extends OpenCloseDevice { 4 | static get type() { 5 | return 'action.devices.types.CURTAIN'; 6 | } 7 | } 8 | 9 | module.exports = Curtain; 10 | -------------------------------------------------------------------------------- /functions/devices/default.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-vars */ 2 | const packageVersion = require('../package.json').version; 3 | 4 | class DefaultDevice { 5 | static get type() { 6 | return ''; 7 | } 8 | 9 | /** 10 | * @param {object} item 11 | * @returns {Array} 12 | */ 13 | static getTraits(item) { 14 | return []; 15 | } 16 | 17 | /** 18 | * @returns {Array} 19 | */ 20 | static get requiredItemTypes() { 21 | return []; 22 | } 23 | 24 | /** 25 | * @param {object} item 26 | * @returns {boolean} 27 | */ 28 | static matchesDeviceType(item) { 29 | return !!( 30 | item.metadata && 31 | item.metadata.ga && 32 | this.type.toLowerCase() === `action.devices.types.${item.metadata.ga.value}`.toLowerCase() 33 | ); 34 | } 35 | 36 | /** 37 | * @param {object} item 38 | */ 39 | static matchesItemType(item) { 40 | return !!( 41 | !this.requiredItemTypes.length || 42 | this.requiredItemTypes.includes((item.groupType || item.type || '').split(':')[0]) 43 | ); 44 | } 45 | 46 | /** 47 | * @param {object} item 48 | */ 49 | static getAttributes(item) { 50 | return {}; 51 | } 52 | 53 | /** 54 | * @param {object} item 55 | */ 56 | static getConfig(item) { 57 | return (item && item.metadata && item.metadata.ga && item.metadata.ga.config) || {}; 58 | } 59 | 60 | /** 61 | * @param {object} item 62 | */ 63 | static getMetadata(item) { 64 | const config = this.getConfig(item); 65 | const itemType = item.groupType || item.type; 66 | const deviceName = config.name || item.label || item.name; 67 | const metadata = { 68 | id: item.name, 69 | type: this.type, 70 | traits: this.getTraits(item), 71 | name: { 72 | name: deviceName, 73 | defaultNames: [deviceName], 74 | nicknames: [ 75 | deviceName, 76 | ...(item.metadata && item.metadata.synonyms 77 | ? item.metadata.synonyms.value.split(',').map((s) => s.trim()) 78 | : []) 79 | ] 80 | }, 81 | willReportState: false, 82 | roomHint: config.roomHint, 83 | structureHint: config.structureHint, 84 | deviceInfo: { 85 | manufacturer: 'openHAB', 86 | model: `${itemType}:${item.name}`, 87 | hwVersion: '3.0.0', 88 | swVersion: packageVersion 89 | }, 90 | attributes: this.getAttributes(item), 91 | customData: { 92 | deviceType: this.name, 93 | itemType: itemType 94 | } 95 | }; 96 | if (!!config.inverted === true) { 97 | metadata.customData.inverted = true; 98 | } 99 | if (!!config.checkState === true) { 100 | metadata.customData.checkState = true; 101 | } 102 | if (!!config.ackNeeded === true || !!config.tfaAck === true) { 103 | metadata.customData.ackNeeded = true; 104 | } 105 | if (typeof config.pinNeeded === 'string' || typeof config.tfaPin === 'string') { 106 | metadata.customData.pinNeeded = config.pinNeeded || config.tfaPin; 107 | if (config.pinOnDisarmOnly === true) { 108 | metadata.customData.pinOnDisarmOnly = true; 109 | } 110 | } 111 | if (config.waitForStateChange) { 112 | metadata.customData.waitForStateChange = parseInt(config.waitForStateChange); 113 | } 114 | if (this.supportedMembers.length) { 115 | const members = this.getMembers(item); 116 | metadata.customData.members = {}; 117 | for (const member in members) { 118 | metadata.customData.members[member] = members[member].name; 119 | } 120 | } 121 | return metadata; 122 | } 123 | 124 | /** 125 | * @param {object} item 126 | */ 127 | static getState(item) { 128 | return {}; 129 | } 130 | 131 | /** 132 | * @returns {Array} 133 | */ 134 | static get supportedMembers() { 135 | return []; 136 | } 137 | 138 | /** 139 | * @returns {object} 140 | */ 141 | static getMembers(item) { 142 | const supportedMembers = this.supportedMembers; 143 | const members = {}; 144 | if (item.members && item.members.length) { 145 | item.members.forEach((member) => { 146 | if (member.metadata && member.metadata.ga) { 147 | const memberType = supportedMembers.find((m) => { 148 | const memberType = (member.groupType || member.type || '').split(':')[0]; 149 | return m.types.includes(memberType) && member.metadata.ga.value.toLowerCase() === m.name.toLowerCase(); 150 | }); 151 | if (memberType) { 152 | members[memberType.name] = { name: member.name, state: member.state }; 153 | } 154 | } 155 | }); 156 | } 157 | return members; 158 | } 159 | } 160 | 161 | module.exports = DefaultDevice; 162 | -------------------------------------------------------------------------------- /functions/devices/dimmablelight.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class DimmableLight extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.LIGHT'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.OnOff', 'action.devices.traits.Brightness']; 10 | } 11 | 12 | static get requiredItemTypes() { 13 | return ['Dimmer']; 14 | } 15 | 16 | static getState(item) { 17 | const brightness = Math.round(parseFloat(item.state)) || 0; 18 | return { 19 | on: brightness > 0, 20 | brightness: brightness 21 | }; 22 | } 23 | } 24 | 25 | module.exports = DimmableLight; 26 | -------------------------------------------------------------------------------- /functions/devices/dishwasher.js: -------------------------------------------------------------------------------- 1 | const StartStopSwitch = require('./startstopswitch.js'); 2 | 3 | class Dishwasher extends StartStopSwitch { 4 | static get type() { 5 | return 'action.devices.types.DISHWASHER'; 6 | } 7 | } 8 | 9 | module.exports = Dishwasher; 10 | -------------------------------------------------------------------------------- /functions/devices/door.js: -------------------------------------------------------------------------------- 1 | const OpenCloseDevice = require('./openclosedevice.js'); 2 | 3 | class Door extends OpenCloseDevice { 4 | static get type() { 5 | return 'action.devices.types.DOOR'; 6 | } 7 | } 8 | 9 | module.exports = Door; 10 | -------------------------------------------------------------------------------- /functions/devices/dynamicmodesdevice.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class DynamicModesDevice extends DefaultDevice { 4 | static getTraits() { 5 | return ['action.devices.traits.Modes']; 6 | } 7 | 8 | static get requiredItemTypes() { 9 | return ['Group']; 10 | } 11 | 12 | static matchesDeviceType(item) { 13 | return super.matchesDeviceType(item) && !!this.getAttributes(item).availableModes; 14 | } 15 | 16 | static getAttributes(item) { 17 | const config = this.getConfig(item); 18 | const members = this.getMembers(item); 19 | if (!config.mode || !('modesSettings' in members) || !members.modesSettings.state.includes('=')) { 20 | return {}; 21 | } 22 | const modeNames = config.mode.split(',').map((s) => s.trim()); 23 | const attributes = { 24 | availableModes: [ 25 | { 26 | name: modeNames[0], 27 | name_values: [ 28 | { 29 | name_synonym: modeNames, 30 | lang: config.lang || 'en' 31 | } 32 | ], 33 | settings: [], 34 | ordered: config.ordered === true 35 | } 36 | ] 37 | }; 38 | members.modesSettings.state.split(',').forEach((setting) => { 39 | try { 40 | const [settingName, settingSynonyms] = setting 41 | .trim() 42 | .split('=') 43 | .map((s) => s.trim()); 44 | attributes.availableModes[0].settings.push({ 45 | setting_name: settingName, 46 | setting_values: [ 47 | { 48 | setting_synonym: [settingName].concat(settingSynonyms.split(':').map((s) => s.trim())), 49 | lang: config.lang || 'en' 50 | } 51 | ] 52 | }); 53 | } catch {} 54 | }); 55 | return attributes; 56 | } 57 | 58 | static getState(item) { 59 | const config = this.getConfig(item); 60 | const members = this.getMembers(item); 61 | const state = {}; 62 | if (config.mode && 'modesCurrentMode' in members) { 63 | const modeNames = config.mode.split(',').map((s) => s.trim()); 64 | state.currentModeSettings = { 65 | [modeNames[0]]: members.modesCurrentMode.state 66 | }; 67 | } 68 | return state; 69 | } 70 | 71 | static get supportedMembers() { 72 | return [ 73 | { name: 'modesCurrentMode', types: ['String', 'Number'] }, 74 | { name: 'modesSettings', types: ['String'] } 75 | ]; 76 | } 77 | } 78 | 79 | module.exports = DynamicModesDevice; 80 | -------------------------------------------------------------------------------- /functions/devices/dynamicmodeslight.js: -------------------------------------------------------------------------------- 1 | const DynamicModesDevice = require('./dynamicmodesdevice.js'); 2 | 3 | class DynamicModesLight extends DynamicModesDevice { 4 | static get type() { 5 | return 'action.devices.types.LIGHT'; 6 | } 7 | } 8 | 9 | module.exports = DynamicModesLight; 10 | -------------------------------------------------------------------------------- /functions/devices/fireplace.js: -------------------------------------------------------------------------------- 1 | const Switch = require('./switch.js'); 2 | 3 | class Fireplace extends Switch { 4 | static get type() { 5 | return 'action.devices.types.FIREPLACE'; 6 | } 7 | } 8 | 9 | module.exports = Fireplace; 10 | -------------------------------------------------------------------------------- /functions/devices/garage.js: -------------------------------------------------------------------------------- 1 | const OpenCloseDevice = require('./openclosedevice.js'); 2 | 3 | class Garage extends OpenCloseDevice { 4 | static get type() { 5 | return 'action.devices.types.GARAGE'; 6 | } 7 | } 8 | 9 | module.exports = Garage; 10 | -------------------------------------------------------------------------------- /functions/devices/gate.js: -------------------------------------------------------------------------------- 1 | const OpenCloseDevice = require('./openclosedevice.js'); 2 | 3 | class Gate extends OpenCloseDevice { 4 | static get type() { 5 | return 'action.devices.types.GATE'; 6 | } 7 | } 8 | 9 | module.exports = Gate; 10 | -------------------------------------------------------------------------------- /functions/devices/hood.js: -------------------------------------------------------------------------------- 1 | const Fan = require('./fan.js'); 2 | 3 | class Hood extends Fan { 4 | static get type() { 5 | return 'action.devices.types.HOOD'; 6 | } 7 | } 8 | 9 | module.exports = Hood; 10 | -------------------------------------------------------------------------------- /functions/devices/humiditysensor.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class HumiditySensor extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.SENSOR'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.HumiditySetting']; 10 | } 11 | 12 | static getAttributes() { 13 | return { 14 | queryOnlyHumiditySetting: true 15 | }; 16 | } 17 | 18 | static get requiredItemTypes() { 19 | return ['Number']; 20 | } 21 | 22 | static matchesDeviceType(item) { 23 | return item.metadata && item.metadata.ga && item.metadata.ga.value.toLowerCase() === 'humiditysensor'; 24 | } 25 | 26 | static getState(item) { 27 | const config = this.getConfig(item); 28 | const maxHumidity = (config.maxHumidity && parseInt(config.maxHumidity)) || 100; 29 | const state = Math.round(parseFloat(item.state) * (100 / maxHumidity)); 30 | return { 31 | humidityAmbientPercent: state, 32 | humiditySetpointPercent: state 33 | }; 34 | } 35 | } 36 | 37 | module.exports = HumiditySensor; 38 | -------------------------------------------------------------------------------- /functions/devices/index.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | 3 | const Devices = []; 4 | 5 | fs.readdirSync(__dirname).forEach((file) => { 6 | if (file === 'index.js' || !file.endsWith('.js')) return; 7 | const device = require(`./${file}`); 8 | if (device.type) { 9 | Devices.push(device); 10 | } 11 | }); 12 | 13 | module.exports = { 14 | /** 15 | * @param {object} item 16 | */ 17 | getDeviceForItem: (item) => { 18 | return ( 19 | item.metadata && 20 | item.metadata.ga && 21 | Devices.find((device) => device.matchesItemType(item) && device.matchesDeviceType(item)) 22 | ); 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /functions/devices/lock.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class Lock extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.LOCK'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.LockUnlock']; 10 | } 11 | 12 | static get requiredItemTypes() { 13 | return ['Switch', 'Contact']; 14 | } 15 | 16 | static getState(item) { 17 | let state = item.state === 'ON' || item.state === 'CLOSED'; 18 | if (this.getConfig(item).inverted === true) { 19 | state = !state; 20 | } 21 | return { 22 | isLocked: state 23 | }; 24 | } 25 | } 26 | 27 | module.exports = Lock; 28 | -------------------------------------------------------------------------------- /functions/devices/modesdevice.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class ModesDevice extends DefaultDevice { 4 | static getTraits() { 5 | return ['action.devices.traits.Modes']; 6 | } 7 | 8 | static matchesDeviceType(item) { 9 | return super.matchesDeviceType(item) && !!this.getAttributes(item).availableModes; 10 | } 11 | 12 | static getAttributes(item) { 13 | const config = this.getConfig(item); 14 | if (!config.mode || !config.settings) { 15 | return {}; 16 | } 17 | const modeNames = config.mode.split(',').map((s) => s.trim()); 18 | const attributes = { 19 | availableModes: [ 20 | { 21 | name: modeNames[0], 22 | name_values: [ 23 | { 24 | name_synonym: modeNames, 25 | lang: config.lang || 'en' 26 | } 27 | ], 28 | settings: [], 29 | ordered: config.ordered === true 30 | } 31 | ] 32 | }; 33 | config.settings.split(',').forEach((setting) => { 34 | try { 35 | const [settingName, settingSynonyms] = setting 36 | .trim() 37 | .split('=') 38 | .map((s) => s.trim()); 39 | attributes.availableModes[0].settings.push({ 40 | setting_name: settingName, 41 | setting_values: [ 42 | { 43 | setting_synonym: [settingName].concat(settingSynonyms.split(':').map((s) => s.trim())), 44 | lang: config.lang || 'en' 45 | } 46 | ] 47 | }); 48 | } catch {} 49 | }); 50 | return attributes; 51 | } 52 | 53 | static get requiredItemTypes() { 54 | return ['Color', 'Dimmer', 'Number', 'Player', 'Rollershutter', 'String', 'Switch']; 55 | } 56 | 57 | static getState(item) { 58 | const config = this.getConfig(item); 59 | const state = {}; 60 | if (config.mode && config.settings) { 61 | const modeNames = config.mode.split(',').map((s) => s.trim()); 62 | state.currentModeSettings = { 63 | [modeNames[0]]: item.state 64 | }; 65 | } 66 | return state; 67 | } 68 | } 69 | 70 | module.exports = ModesDevice; 71 | -------------------------------------------------------------------------------- /functions/devices/modeslight.js: -------------------------------------------------------------------------------- 1 | const ModesDevice = require('./modesdevice.js'); 2 | 3 | class ModesLight extends ModesDevice { 4 | static get type() { 5 | return 'action.devices.types.LIGHT'; 6 | } 7 | } 8 | 9 | module.exports = ModesLight; 10 | -------------------------------------------------------------------------------- /functions/devices/openclosedevice.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class OpenCloseDevice extends DefaultDevice { 4 | static getTraits() { 5 | return ['action.devices.traits.OpenClose', 'action.devices.traits.StartStop']; 6 | } 7 | 8 | static getAttributes(item) { 9 | const attributes = { 10 | pausable: false, 11 | discreteOnlyOpenClose: this.getConfig(item).discreteOnly === true, 12 | queryOnlyOpenClose: this.getConfig(item).queryOnly === true 13 | }; 14 | const itemType = item.groupType || item.type; 15 | if (itemType === 'Switch') { 16 | attributes.discreteOnlyOpenClose = true; 17 | } 18 | if (itemType === 'Contact') { 19 | attributes.discreteOnlyOpenClose = true; 20 | attributes.queryOnlyOpenClose = true; 21 | } 22 | return attributes; 23 | } 24 | 25 | static get requiredItemTypes() { 26 | return ['Rollershutter', 'Switch', 'Contact']; 27 | } 28 | 29 | static getState(item) { 30 | let state = 0; 31 | const itemType = item.groupType || item.type; 32 | if (itemType === 'Rollershutter') { 33 | state = Math.round(parseFloat(item.state)); 34 | } else { 35 | state = item.state === 'ON' || item.state === 'OPEN' ? 0 : 100; 36 | } 37 | return { 38 | openPercent: this.getConfig(item).inverted !== true ? 100 - state : state 39 | }; 40 | } 41 | } 42 | 43 | module.exports = OpenCloseDevice; 44 | -------------------------------------------------------------------------------- /functions/devices/outlet.js: -------------------------------------------------------------------------------- 1 | const Switch = require('./switch.js'); 2 | 3 | class Outlet extends Switch { 4 | static get type() { 5 | return 'action.devices.types.OUTLET'; 6 | } 7 | } 8 | 9 | module.exports = Outlet; 10 | -------------------------------------------------------------------------------- /functions/devices/pergola.js: -------------------------------------------------------------------------------- 1 | const OpenCloseDevice = require('./openclosedevice.js'); 2 | 3 | class Pergola extends OpenCloseDevice { 4 | static get type() { 5 | return 'action.devices.types.PERGOLA'; 6 | } 7 | } 8 | 9 | module.exports = Pergola; 10 | -------------------------------------------------------------------------------- /functions/devices/scene.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class Scene extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.SCENE'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.Scene']; 10 | } 11 | 12 | static get requiredItemTypes() { 13 | return ['Switch']; 14 | } 15 | 16 | static getAttributes(item) { 17 | const config = this.getConfig(item); 18 | return { 19 | sceneReversible: config.sceneReversible !== false 20 | }; 21 | } 22 | } 23 | 24 | module.exports = Scene; 25 | -------------------------------------------------------------------------------- /functions/devices/securitysystem.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | const memberArmed = 'securitySystemArmed'; 4 | const memberArmLevel = 'securitySystemArmLevel'; 5 | const memberZone = 'securitySystemZone'; 6 | const memberTrouble = 'securitySystemTrouble'; 7 | const memberErrorCode = 'securitySystemTroubleCode'; 8 | const zoneStateActive = ['ON', 'OPEN']; 9 | 10 | class SecuritySystem extends DefaultDevice { 11 | static get type() { 12 | return 'action.devices.types.SECURITYSYSTEM'; 13 | } 14 | 15 | static getTraits() { 16 | return ['action.devices.traits.ArmDisarm', 'action.devices.traits.StatusReport']; 17 | } 18 | 19 | static get armedMemberName() { 20 | return memberArmed; 21 | } 22 | 23 | static get armLevelMemberName() { 24 | return memberArmLevel; 25 | } 26 | 27 | static get requiredItemTypes() { 28 | return ['Group']; 29 | } 30 | 31 | static matchesDeviceType(item) { 32 | return super.matchesDeviceType(item) && Object.keys(this.getMembers(item)).length > 0; 33 | } 34 | 35 | static getAttributes(item) { 36 | const config = this.getConfig(item); 37 | if ('armLevels' in config) { 38 | const attributes = { 39 | availableArmLevels: { 40 | levels: [], 41 | ordered: config.ordered === true 42 | } 43 | }; 44 | attributes.availableArmLevels.levels = config.armLevels 45 | .split(',') 46 | .map((level) => level.split('=')) 47 | .map(([levelName, levelSynonym]) => { 48 | return { 49 | level_name: levelName, 50 | level_values: [ 51 | { 52 | level_synonym: [levelSynonym], 53 | lang: config.lang || 'en' 54 | } 55 | ] 56 | }; 57 | }); 58 | return attributes; 59 | } 60 | return {}; 61 | } 62 | 63 | static get supportedMembers() { 64 | return [ 65 | { name: memberArmed, types: ['Switch'] }, 66 | { name: memberArmLevel, types: ['String'] }, 67 | { name: memberZone, types: ['Contact'] }, 68 | { name: memberTrouble, types: ['Switch'] }, 69 | { name: memberErrorCode, types: ['String'] } 70 | ]; 71 | } 72 | 73 | static getMembers(item) { 74 | const supportedMembers = this.supportedMembers; 75 | const members = {}; 76 | if (item.members && item.members.length) { 77 | item.members.forEach((member) => { 78 | if (member.metadata && member.metadata.ga) { 79 | const memberType = supportedMembers.find((m) => { 80 | const memberType = (member.groupType || member.type || '').split(':')[0]; 81 | return m.types.includes(memberType) && member.metadata.ga.value.toLowerCase() === m.name.toLowerCase(); 82 | }); 83 | if (memberType) { 84 | const memberDetails = { name: member.name, state: member.state, config: this.getConfig(member) }; 85 | if (memberType.name === memberZone) { 86 | members.zones = members.zones || []; 87 | members.zones.push(memberDetails); 88 | } else { 89 | members[memberType.name] = memberDetails; 90 | } 91 | } 92 | } 93 | }); 94 | } 95 | return members; 96 | } 97 | 98 | static getState(item) { 99 | const state = { 100 | isArmed: false 101 | }; 102 | 103 | const members = this.getMembers(item); 104 | if (memberArmed in members) { 105 | state.isArmed = members[memberArmed].state === 'ON'; 106 | if (state.isArmed && memberArmLevel in members) { 107 | state.currentArmLevel = members[memberArmLevel].state; 108 | } 109 | state.currentStatusReport = this.getStatusReport(item, members); 110 | } 111 | 112 | if (this.getConfig(item).inverted === true) { 113 | state.isArmed = !state.isArmed; 114 | } 115 | 116 | return state; 117 | } 118 | 119 | static getStatusReport(item, members) { 120 | const report = []; 121 | const isTrouble = memberTrouble in members && members[memberTrouble].state === 'ON'; 122 | 123 | if (isTrouble) { 124 | report.push({ 125 | blocking: false, 126 | deviceTarget: item.name, 127 | priority: 0, 128 | statusCode: (memberErrorCode in members && members[memberErrorCode].state) || 'noIssuesReported' 129 | }); 130 | } 131 | 132 | if (members.zones) { 133 | for (const zone of members.zones) { 134 | if (zoneStateActive.includes(zone.state)) { 135 | let statusCode = 'notSupported'; 136 | switch (zone.config.zoneType) { 137 | case 'OpenClose': 138 | statusCode = 'deviceOpen'; 139 | break; 140 | case 'Motion': 141 | statusCode = 'motionDetected'; 142 | break; 143 | } 144 | report.push({ 145 | blocking: zone.config.blocking === true, 146 | deviceTarget: zone.name, 147 | priority: 1, 148 | statusCode: statusCode 149 | }); 150 | } 151 | } 152 | } 153 | 154 | return report; 155 | } 156 | } 157 | 158 | module.exports = SecuritySystem; 159 | -------------------------------------------------------------------------------- /functions/devices/sensor.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class Sensor extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.SENSOR'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.SensorState']; 10 | } 11 | 12 | static get requiredItemTypes() { 13 | return ['Number', 'String', 'Dimmer', 'Switch', 'Rollershutter', 'Contact']; 14 | } 15 | 16 | static matchesDeviceType(item) { 17 | return super.matchesDeviceType(item) && !!this.getAttributes(item).sensorStatesSupported; 18 | } 19 | 20 | static getAttributes(item) { 21 | const config = this.getConfig(item); 22 | if (!('sensorName' in config) || (!('valueUnit' in config) && !('states' in config))) return {}; 23 | const attributes = { sensorStatesSupported: [{ name: config.sensorName }] }; 24 | if ('valueUnit' in config) { 25 | attributes.sensorStatesSupported[0].numericCapabilities = { 26 | rawValueUnit: config.valueUnit 27 | }; 28 | } 29 | if ('states' in config) { 30 | attributes.sensorStatesSupported[0].descriptiveCapabilities = { 31 | availableStates: config.states.split(',').map((s) => s.trim().split('=')[0].trim()) 32 | }; 33 | } 34 | return attributes; 35 | } 36 | 37 | static getState(item) { 38 | const config = this.getConfig(item); 39 | const state = { 40 | currentSensorStateData: [ 41 | { 42 | name: config.sensorName, 43 | currentSensorState: this.translateStateToGoogle(item) 44 | } 45 | ] 46 | }; 47 | const rawValue = parseFloat(item.state); 48 | if (!isNaN(rawValue)) state.currentSensorStateData[0].rawValue = rawValue; 49 | return state; 50 | } 51 | 52 | static translateStateToGoogle(item) { 53 | const config = this.getConfig(item); 54 | if ('states' in config) { 55 | const states = config.states.split(',').map((s) => s.trim()); 56 | for (const state of states) { 57 | const [key, value] = state.split('=').map((s) => s.trim()); 58 | if (value === item.state || value === parseFloat(item.state).toFixed(0)) { 59 | return key; 60 | } 61 | } 62 | } 63 | return ''; 64 | } 65 | } 66 | 67 | module.exports = Sensor; 68 | -------------------------------------------------------------------------------- /functions/devices/shutter.js: -------------------------------------------------------------------------------- 1 | const OpenCloseDevice = require('./openclosedevice.js'); 2 | 3 | class Shutter extends OpenCloseDevice { 4 | static get type() { 5 | return 'action.devices.types.SHUTTER'; 6 | } 7 | } 8 | 9 | module.exports = Shutter; 10 | -------------------------------------------------------------------------------- /functions/devices/simpleairpurifier.js: -------------------------------------------------------------------------------- 1 | const Switch = require('./switch.js'); 2 | 3 | class SimpleAirPurifier extends Switch { 4 | static get type() { 5 | return 'action.devices.types.AIRPURIFIER'; 6 | } 7 | } 8 | 9 | module.exports = SimpleAirPurifier; 10 | -------------------------------------------------------------------------------- /functions/devices/simplefan.js: -------------------------------------------------------------------------------- 1 | const Switch = require('./switch.js'); 2 | 3 | class SimpleFan extends Switch { 4 | static get type() { 5 | return 'action.devices.types.FAN'; 6 | } 7 | } 8 | 9 | module.exports = SimpleFan; 10 | -------------------------------------------------------------------------------- /functions/devices/simplehood.js: -------------------------------------------------------------------------------- 1 | const Switch = require('./switch.js'); 2 | 3 | class SimpleHood extends Switch { 4 | static get type() { 5 | return 'action.devices.types.HOOD'; 6 | } 7 | } 8 | 9 | module.exports = SimpleHood; 10 | -------------------------------------------------------------------------------- /functions/devices/simplelight.js: -------------------------------------------------------------------------------- 1 | const Switch = require('./switch.js'); 2 | 3 | class SimpleLight extends Switch { 4 | static get type() { 5 | return 'action.devices.types.LIGHT'; 6 | } 7 | } 8 | 9 | module.exports = SimpleLight; 10 | -------------------------------------------------------------------------------- /functions/devices/simplesecuritysystem.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class SimpleSecuritySystem extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.SECURITYSYSTEM'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.ArmDisarm']; 10 | } 11 | 12 | static get requiredItemTypes() { 13 | return ['Switch']; 14 | } 15 | 16 | static getState(item) { 17 | let state = item.state === 'ON'; 18 | if (this.getConfig(item).inverted === true) { 19 | state = !state; 20 | } 21 | return { 22 | isArmed: state 23 | }; 24 | } 25 | } 26 | 27 | module.exports = SimpleSecuritySystem; 28 | -------------------------------------------------------------------------------- /functions/devices/speaker.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class Speaker extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.SPEAKER'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.Volume']; 10 | } 11 | 12 | static getAttributes(item) { 13 | const config = this.getConfig(item); 14 | const attributes = { 15 | volumeMaxLevel: 100, 16 | volumeCanMuteAndUnmute: false 17 | }; 18 | if ('volumeMaxLevel' in config) { 19 | attributes.volumeMaxLevel = Number(config.volumeMaxLevel); 20 | } 21 | if ('volumeDefaultPercentage' in config) { 22 | attributes.volumeDefaultPercentage = Number(config.volumeDefaultPercentage); 23 | } 24 | if ('levelStepSize' in config) { 25 | attributes.levelStepSize = Number(config.levelStepSize); 26 | } 27 | return attributes; 28 | } 29 | 30 | static get requiredItemTypes() { 31 | return ['Dimmer']; 32 | } 33 | 34 | static getState(item) { 35 | return { 36 | currentVolume: Math.round(parseFloat(item.state)) || 0 37 | }; 38 | } 39 | } 40 | 41 | module.exports = Speaker; 42 | -------------------------------------------------------------------------------- /functions/devices/specialcolorlight.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | const convertMired = require('../utilities.js').convertMired; 3 | 4 | class SpecialColorLight extends DefaultDevice { 5 | static get type() { 6 | return 'action.devices.types.LIGHT'; 7 | } 8 | 9 | static getTraits() { 10 | return ['action.devices.traits.OnOff', 'action.devices.traits.Brightness', 'action.devices.traits.ColorSetting']; 11 | } 12 | 13 | static get requiredItemTypes() { 14 | return ['Group']; 15 | } 16 | 17 | static matchesDeviceType(item) { 18 | const members = this.getMembers(item); 19 | return !!( 20 | item.metadata && 21 | item.metadata.ga && 22 | item.metadata.ga.value.toLowerCase() === 'specialcolorlight' && 23 | Object.keys(members).length > 1 && 24 | (!('lightColorTemperature' in members) || 25 | this.getColorUnit(item) !== 'percent' || 26 | !!this.getAttributes(item).colorTemperatureRange) 27 | ); 28 | } 29 | 30 | static getAttributes(item) { 31 | const attributes = {}; 32 | const members = this.getMembers(item); 33 | if ('lightColor' in members) { 34 | attributes.colorModel = 'hsv'; 35 | } 36 | const config = this.getConfig(item); 37 | if ('colorTemperatureRange' in config) { 38 | const [min, max] = config.colorTemperatureRange.split(',').map((s) => Number(s.trim())); 39 | if (!isNaN(min) && !isNaN(max)) { 40 | const colorUnit = this.getColorUnit(item); 41 | attributes.colorTemperatureRange = { 42 | temperatureMinK: colorUnit === 'mired' ? convertMired(max) : min, 43 | temperatureMaxK: colorUnit === 'mired' ? convertMired(min) : max 44 | }; 45 | } 46 | } 47 | return attributes; 48 | } 49 | 50 | static getMetadata(item) { 51 | const metadata = super.getMetadata(item); 52 | const colorTemperatureRange = this.getAttributes(item).colorTemperatureRange; 53 | if (colorTemperatureRange) { 54 | metadata.customData.colorTemperatureRange = colorTemperatureRange; 55 | } 56 | const colorUnit = this.getColorUnit(item); 57 | if (colorUnit !== 'percent') { 58 | metadata.customData.colorUnit = colorUnit; 59 | } 60 | if (this.getColorTemperatureInverted(item)) { 61 | metadata.customData.colorTemperatureInverted = true; 62 | } 63 | return metadata; 64 | } 65 | 66 | static getState(item) { 67 | const state = {}; 68 | const members = this.getMembers(item); 69 | for (const member in members) { 70 | switch (member) { 71 | case 'lightPower': 72 | state.on = members[member].state === 'ON'; 73 | break; 74 | case 'lightBrightness': 75 | state.brightness = Math.round(parseFloat(members[member].state)) || 0; 76 | if (!('lightPower' in members)) { 77 | state.on = state.brightness > 0; 78 | } 79 | break; 80 | case 'lightColor': 81 | try { 82 | const [hue, sat, val] = members[member].state.split(',').map((s) => Number(s.trim())); 83 | if (val > 0) { 84 | state.color = { 85 | spectrumHSV: { 86 | hue: hue, 87 | saturation: sat / 100, 88 | value: val / 100 89 | } 90 | }; 91 | } 92 | } catch (error) { 93 | // 94 | } 95 | break; 96 | case 'lightColorTemperature': 97 | if (state.color) { 98 | break; 99 | } 100 | try { 101 | const colorUnit = this.getColorUnit(item); 102 | if (colorUnit === 'kelvin') { 103 | state.color = { 104 | temperatureK: Math.round(parseFloat(members[member].state)) 105 | }; 106 | } else if (colorUnit === 'mired') { 107 | state.color = { 108 | temperatureK: convertMired(Math.round(parseFloat(members[member].state))) 109 | }; 110 | } else { 111 | const { temperatureMinK, temperatureMaxK } = this.getAttributes(item).colorTemperatureRange; 112 | let percent = parseFloat(members[member].state); 113 | if (this.getColorTemperatureInverted(item)) { 114 | percent = 100 - percent; 115 | } 116 | state.color = { 117 | temperatureK: temperatureMinK + Math.round(((temperatureMaxK - temperatureMinK) / 100) * percent || 0) 118 | }; 119 | } 120 | } catch (error) { 121 | // 122 | } 123 | break; 124 | } 125 | } 126 | return state; 127 | } 128 | 129 | static get supportedMembers() { 130 | return [ 131 | { name: 'lightPower', types: ['Switch'] }, 132 | { name: 'lightColor', types: ['Color'] }, 133 | { name: 'lightBrightness', types: ['Dimmer', 'Number'] }, 134 | { name: 'lightColorTemperature', types: ['Dimmer', 'Number'] } 135 | ]; 136 | } 137 | 138 | static getColorUnit(item) { 139 | const colorUnit = this.getConfig(item).colorUnit || 'percent'; 140 | return colorUnit.toLowerCase(); 141 | } 142 | 143 | static getColorTemperatureInverted(item) { 144 | return !!this.getConfig(item).colorTemperatureInverted === true; 145 | } 146 | } 147 | 148 | module.exports = SpecialColorLight; 149 | -------------------------------------------------------------------------------- /functions/devices/sprinkler.js: -------------------------------------------------------------------------------- 1 | const StartStopSwitch = require('./startstopswitch.js'); 2 | 3 | class Sprinkler extends StartStopSwitch { 4 | static get type() { 5 | return 'action.devices.types.SPRINKLER'; 6 | } 7 | } 8 | 9 | module.exports = Sprinkler; 10 | -------------------------------------------------------------------------------- /functions/devices/startstopswitch.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class StartStopSwitch extends DefaultDevice { 4 | static getTraits() { 5 | return ['action.devices.traits.StartStop']; 6 | } 7 | 8 | static get requiredItemTypes() { 9 | return ['Switch']; 10 | } 11 | 12 | static getAttributes() { 13 | return { pausable: false }; 14 | } 15 | 16 | static getState(item) { 17 | let state = item.state === 'ON'; 18 | if (this.getConfig(item).inverted === true) { 19 | state = !state; 20 | } 21 | return { 22 | isRunning: state 23 | }; 24 | } 25 | } 26 | 27 | module.exports = StartStopSwitch; 28 | -------------------------------------------------------------------------------- /functions/devices/switch.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class Switch extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.SWITCH'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.OnOff']; 10 | } 11 | 12 | static get requiredItemTypes() { 13 | return ['Switch']; 14 | } 15 | 16 | static getState(item) { 17 | let state = item.state === 'ON'; 18 | if (this.getConfig(item).inverted === true) { 19 | state = !state; 20 | } 21 | return { 22 | on: state 23 | }; 24 | } 25 | } 26 | 27 | module.exports = Switch; 28 | -------------------------------------------------------------------------------- /functions/devices/temperaturesensor.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | const convertFahrenheitToCelsius = require('../utilities.js').convertFahrenheitToCelsius; 3 | 4 | class TemperatureSensor extends DefaultDevice { 5 | static get type() { 6 | return 'action.devices.types.SENSOR'; 7 | } 8 | 9 | static getTraits() { 10 | return ['action.devices.traits.TemperatureSetting', 'action.devices.traits.TemperatureControl']; 11 | } 12 | 13 | static getAttributes(item) { 14 | const attributes = { 15 | queryOnlyTemperatureSetting: true, 16 | thermostatTemperatureUnit: this.useFahrenheit(item) ? 'F' : 'C', 17 | queryOnlyTemperatureControl: true, 18 | temperatureUnitForUX: this.useFahrenheit(item) ? 'F' : 'C', 19 | temperatureRange: { 20 | minThresholdCelsius: -100, 21 | maxThresholdCelsius: 100 22 | } 23 | }; 24 | const config = this.getConfig(item); 25 | if ('temperatureRange' in config) { 26 | const [min, max] = config.temperatureRange.split(',').map((s) => parseFloat(s.trim())); 27 | if (!isNaN(min) && !isNaN(max)) { 28 | attributes.temperatureRange = { 29 | minThresholdCelsius: min, 30 | maxThresholdCelsius: max 31 | }; 32 | } 33 | } 34 | return attributes; 35 | } 36 | 37 | static get requiredItemTypes() { 38 | return ['Number']; 39 | } 40 | 41 | static matchesDeviceType(item) { 42 | return item.metadata && item.metadata.ga && item.metadata.ga.value.toLowerCase() === 'temperaturesensor'; 43 | } 44 | 45 | static getState(item) { 46 | let state = Number(parseFloat(item.state).toFixed(1)); 47 | if (this.useFahrenheit(item)) { 48 | state = convertFahrenheitToCelsius(state); 49 | } 50 | return { 51 | thermostatTemperatureAmbient: state, 52 | temperatureAmbientCelsius: state, 53 | temperatureSetpointCelsius: state 54 | }; 55 | } 56 | 57 | static useFahrenheit(item) { 58 | const config = this.getConfig(item); 59 | return config.thermostatTemperatureUnit === 'F' || config.useFahrenheit === true; 60 | } 61 | } 62 | 63 | module.exports = TemperatureSensor; 64 | -------------------------------------------------------------------------------- /functions/devices/thermostat.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | const convertFahrenheitToCelsius = require('../utilities.js').convertFahrenheitToCelsius; 3 | 4 | class Thermostat extends DefaultDevice { 5 | static get type() { 6 | return 'action.devices.types.THERMOSTAT'; 7 | } 8 | 9 | static getTraits() { 10 | return ['action.devices.traits.TemperatureSetting']; 11 | } 12 | 13 | static get requiredItemTypes() { 14 | return ['Group']; 15 | } 16 | 17 | static matchesDeviceType(item) { 18 | return super.matchesDeviceType(item) && Object.keys(this.getMembers(item)).length > 0; 19 | } 20 | 21 | static getAttributes(item) { 22 | const config = this.getConfig(item); 23 | const attributes = { 24 | thermostatTemperatureUnit: this.useFahrenheit(item) ? 'F' : 'C' 25 | }; 26 | if ('thermostatTemperatureRange' in config) { 27 | const [min, max] = config.thermostatTemperatureRange.split(',').map((s) => parseFloat(s.trim())); 28 | if (!isNaN(min) && !isNaN(max)) { 29 | attributes.thermostatTemperatureRange = { 30 | minThresholdCelsius: min, 31 | maxThresholdCelsius: max 32 | }; 33 | } 34 | } 35 | const members = this.getMembers(item); 36 | if ( 37 | 'thermostatTemperatureAmbient' in members && 38 | !('thermostatMode' in members) && 39 | !('thermostatTemperatureSetpoint' in members) 40 | ) { 41 | attributes.queryOnlyTemperatureSetting = true; 42 | } else { 43 | attributes.availableThermostatModes = Object.keys(this.getModeMap(item)); 44 | } 45 | return attributes; 46 | } 47 | 48 | static getState(item) { 49 | const state = {}; 50 | const members = this.getMembers(item); 51 | for (const member in members) { 52 | if (member === 'thermostatMode') { 53 | state[member] = this.translateModeToGoogle(item, members[member].state); 54 | } else { 55 | state[member] = Number(parseFloat(members[member].state).toFixed(1)); 56 | if (member.indexOf('Temperature') > 0 && this.useFahrenheit(item)) { 57 | state[member] = convertFahrenheitToCelsius(state[member]); 58 | } 59 | if (member.indexOf('Humidity') > 0) { 60 | const config = this.getConfig(item); 61 | const maxHumidity = (config.maxHumidity && parseInt(config.maxHumidity)) || 100; 62 | state[member] = Math.round(parseFloat(members[member].state) * (100 / maxHumidity)); 63 | } 64 | } 65 | } 66 | return state; 67 | } 68 | 69 | static get supportedMembers() { 70 | return [ 71 | { name: 'thermostatMode', types: ['Number', 'String', 'Switch'] }, 72 | { name: 'thermostatTemperatureSetpoint', types: ['Number'] }, 73 | { name: 'thermostatTemperatureSetpointHigh', types: ['Number'] }, 74 | { name: 'thermostatTemperatureSetpointLow', types: ['Number'] }, 75 | { name: 'thermostatTemperatureAmbient', types: ['Number'] }, 76 | { name: 'thermostatHumidityAmbient', types: ['Number'] } 77 | ]; 78 | } 79 | 80 | static useFahrenheit(item) { 81 | const config = this.getConfig(item); 82 | return config.thermostatTemperatureUnit === 'F' || config.useFahrenheit === true; 83 | } 84 | 85 | static getModeMap(item) { 86 | const config = this.getConfig(item); 87 | let thermostatModes = ['off', 'heat', 'cool', 'on', 'heatcool', 'auto', 'eco']; 88 | if ('thermostatModes' in config) { 89 | thermostatModes = config.thermostatModes.split(',').map((s) => s.trim()); 90 | } 91 | const modeMap = {}; 92 | thermostatModes.forEach((pair) => { 93 | const [key, value] = pair.split('=').map((s) => s.trim()); 94 | modeMap[key] = value ? value.split(':').map((s) => s.trim()) : [key]; 95 | }); 96 | return modeMap; 97 | } 98 | 99 | static translateModeToOpenhab(item, mode) { 100 | const modeMap = this.getModeMap(item); 101 | if (mode in modeMap) { 102 | return modeMap[mode][0]; 103 | } 104 | throw { statusCode: 400 }; 105 | } 106 | 107 | static translateModeToGoogle(item, mode) { 108 | const modeMap = this.getModeMap(item); 109 | for (const key in modeMap) { 110 | if (modeMap[key].includes(mode)) { 111 | return key; 112 | } 113 | } 114 | return 'on'; 115 | } 116 | } 117 | 118 | module.exports = Thermostat; 119 | -------------------------------------------------------------------------------- /functions/devices/vacuum.js: -------------------------------------------------------------------------------- 1 | const StartStopSwitch = require('./startstopswitch.js'); 2 | 3 | class Vacuum extends StartStopSwitch { 4 | static get type() { 5 | return 'action.devices.types.VACUUM'; 6 | } 7 | } 8 | 9 | module.exports = Vacuum; 10 | -------------------------------------------------------------------------------- /functions/devices/valve.js: -------------------------------------------------------------------------------- 1 | const DefaultDevice = require('./default.js'); 2 | 3 | class Valve extends DefaultDevice { 4 | static get type() { 5 | return 'action.devices.types.VALVE'; 6 | } 7 | 8 | static getTraits() { 9 | return ['action.devices.traits.OpenClose']; 10 | } 11 | 12 | static get requiredItemTypes() { 13 | return ['Switch']; 14 | } 15 | 16 | static getState(item) { 17 | let state = item.state === 'ON'; 18 | if (this.getConfig(item).inverted === true) { 19 | state = !state; 20 | } 21 | return { 22 | openPercent: state ? 100 : 0 23 | }; 24 | } 25 | } 26 | 27 | module.exports = Valve; 28 | -------------------------------------------------------------------------------- /functions/devices/washer.js: -------------------------------------------------------------------------------- 1 | const StartStopSwitch = require('./startstopswitch.js'); 2 | 3 | class Washer extends StartStopSwitch { 4 | static get type() { 5 | return 'action.devices.types.WASHER'; 6 | } 7 | } 8 | 9 | module.exports = Washer; 10 | -------------------------------------------------------------------------------- /functions/devices/waterheater.js: -------------------------------------------------------------------------------- 1 | const Switch = require('./switch.js'); 2 | 3 | class WaterHeater extends Switch { 4 | static get type() { 5 | return 'action.devices.types.WATERHEATER'; 6 | } 7 | } 8 | 9 | module.exports = WaterHeater; 10 | -------------------------------------------------------------------------------- /functions/devices/window.js: -------------------------------------------------------------------------------- 1 | const OpenCloseDevice = require('./openclosedevice.js'); 2 | 3 | class Window extends OpenCloseDevice { 4 | static get type() { 5 | return 'action.devices.types.WINDOW'; 6 | } 7 | } 8 | 9 | module.exports = Window; 10 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010-2019 Contributors to the openHAB project 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | 14 | /** 15 | * Main entry point for incoming intents from Google Assistant. 16 | * 17 | * @author Mehmet Arziman - Initial contribution 18 | * @author Michael Krug - Rework 19 | * 20 | */ 21 | const app = require('actions-on-google').smarthome(); 22 | const OpenHAB = require('./openhab.js'); 23 | const ApiHandler = require('./apihandler.js'); 24 | const config = require('./config.js'); 25 | 26 | const apiHandler = new ApiHandler(config); 27 | const openHAB = new OpenHAB(apiHandler); 28 | 29 | app.onDisconnect(() => openHAB.onDisconnect()); 30 | app.onExecute((body, headers) => openHAB.onExecute(body, headers)); 31 | app.onQuery((body, headers) => openHAB.onQuery(body, headers)); 32 | app.onSync((body, headers) => openHAB.onSync(body, headers)); 33 | 34 | exports.openhabGoogleAssistant = app; 35 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openhab.google-assistant-smarthome.cloud-function", 3 | "version": "4.0.0", 4 | "description": "A Google Assistant, Actions on Google based implementation for openHAB", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/openhab/openhab-google-assistant.git" 8 | }, 9 | "author": "Mehmet Arziman", 10 | "contributors": [ 11 | "Michael Krug" 12 | ], 13 | "license": "EPL-2.0", 14 | "dependencies": { 15 | "actions-on-google": "^3.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /functions/utilities.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2010-2019 Contributors to the openHAB project 3 | * 4 | * See the NOTICE file(s) distributed with this work for additional 5 | * information. 6 | * 7 | * This program and the accompanying materials are made available under the 8 | * terms of the Eclipse Public License 2.0 which is available at 9 | * http://www.eclipse.org/legal/epl-2.0 10 | * 11 | * SPDX-License-Identifier: EPL-2.0 12 | */ 13 | 14 | /** 15 | * Various reusable utilify functions 16 | * 17 | * @author Michael Krug 18 | * 19 | */ 20 | 21 | module.exports = { 22 | /** 23 | * @param {number} value temperature in Fahrenheit 24 | * @returns {number} temperature value converted to Celsius 25 | */ 26 | convertFahrenheitToCelsius: (value) => Number((((value - 32) * 5) / 9).toFixed(1)), 27 | /** 28 | * @param {number} value temperature in Celsius 29 | * @returns {number} temperature value converted to Fahrenheit 30 | */ 31 | convertCelsiusToFahrenheit: (value) => Math.round((value * 9) / 5 + 32), 32 | /** 33 | * @param {number} value color temperature as Kelvin 34 | * @returns {object} color temperature value converted to RGB 35 | */ 36 | convertKelvinToRgb: (value) => { 37 | const temp = value / 100; 38 | const r = temp <= 66 ? 255 : 329.698727446 * (temp - 60) ** -0.1332047592; 39 | const g = 40 | temp <= 66 ? 99.4708025861 * Math.log(temp) - 161.1195681661 : 288.1221695283 * (temp - 60) ** -0.0755148492; 41 | const b = temp <= 66 ? (temp <= 19 ? 0 : 138.5177312231 * Math.log(temp - 10) - 305.0447927307) : 255; 42 | return { 43 | r: r < 0 ? 0 : r > 255 ? 255 : Math.round(r), 44 | g: g < 0 ? 0 : g > 255 ? 255 : Math.round(g), 45 | b: b < 0 ? 0 : b > 255 ? 255 : Math.round(b) 46 | }; 47 | }, 48 | /** 49 | * @param {number} value color temperature as Kelvin or Mired 50 | * @returns {number} color temperature value converted to Mired or Kelvin 51 | */ 52 | convertMired: (value) => Math.round(10 ** 6 / value), 53 | /** 54 | * @param {object} rgb color as RGB 55 | * @returns {object} color value converted to HSV 56 | */ 57 | convertRgbToHsv: ({ r, g, b }) => { 58 | r /= 255; 59 | g /= 255; 60 | b /= 255; 61 | const v = Math.max(r, g, b); 62 | const n = v - Math.min(r, g, b); 63 | const h = n && (v === r ? (g - b) / n : v === g ? 2 + (b - r) / n : 4 + (r - g) / n); 64 | return { 65 | hue: Math.round(60 * (h < 0 ? h + 6 : h) * 100) / 100, 66 | saturation: Math.round(v && (n / v) * 100) / 100, 67 | value: Math.round(v * 100) / 100 68 | }; 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "openhab.google-assistant-smarthome", 3 | "version": "4.0.0", 4 | "description": "A Google Assistant, Actions on Google based implementation for openHAB", 5 | "main": "functions/index.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/openhab/openhab-google-assistant.git" 9 | }, 10 | "author": "Mehmet Arziman", 11 | "contributors": [ 12 | "Michael Krug" 13 | ], 14 | "scripts": { 15 | "start": "node testServer.js", 16 | "test": "jest --silent --coverage", 17 | "test-ci": "jest --silent --coverage --ci", 18 | "lint": "eslint --ext .js --cache .", 19 | "fix": "eslint --fix --ext .js --cache .", 20 | "release-major": "cd functions && npm --no-git-tag-version version major && git add package*.json && cd .. && npm version major -f -m \":bookmark: Release (major): %s\"", 21 | "release-minor": "cd functions && npm --no-git-tag-version version minor && git add package*.json && cd .. && npm version minor -f -m \":bookmark: Release (minor): %s\"", 22 | "release-patch": "cd functions && npm --no-git-tag-version version patch && git add package*.json && cd .. && npm version patch -f -m \":bookmark: Release (patch): %s\"" 23 | }, 24 | "jest": { 25 | "setupFiles": [ 26 | "./tests/setenv.js" 27 | ] 28 | }, 29 | "license": "EPL-2.0", 30 | "bugs": { 31 | "url": "https://github.com/openhab/openhab-google-assistant/issues" 32 | }, 33 | "homepage": "https://github.com/openhab/openhab-google-assistant", 34 | "dependencies": { 35 | "express": "^4.21.1" 36 | }, 37 | "devDependencies": { 38 | "@types/jest": "^29.5.14", 39 | "eslint": "^8.57.0", 40 | "eslint-config-prettier": "^9.1.0", 41 | "eslint-plugin-prettier": "^5.2.1", 42 | "jest": "^29.7.0", 43 | "nock": "^13.5.6", 44 | "prettier": "3.4.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /testServer.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | 3 | const app = express(); 4 | const openhabGA = require('./functions/index.js'); 5 | 6 | app.use(express.json()); 7 | 8 | app.use('/', (req, res) => { 9 | openhabGA.openhabGoogleAssistant(req, res); 10 | }); 11 | 12 | const port = process.env.OH_SERVER_PORT || 3000; 13 | app.listen(port, () => { 14 | console.log('Server is listening on port %s', port); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/commands/activatescene.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/activatescene.js'); 2 | 3 | describe('ActivateScene Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(true); 6 | expect(Command.validateParams({ deactivate: true })).toBe(true); 7 | }); 8 | 9 | describe('convertParamsToValue', () => { 10 | test('convertParamsToValue', () => { 11 | expect(Command.convertParamsToValue({ deactivate: true }, {}, {})).toBe('OFF'); 12 | expect(Command.convertParamsToValue({ deactivate: false }, {}, {})).toBe('ON'); 13 | }); 14 | 15 | test('convertParamsToValue inverted', () => { 16 | expect(Command.convertParamsToValue({ deactivate: true }, {}, { customData: { inverted: true } })).toBe('ON'); 17 | expect(Command.convertParamsToValue({ deactivate: false }, {}, { customData: { inverted: true } })).toBe('OFF'); 18 | }); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/commands/appselect.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/appselect.js'); 2 | 3 | describe('appSelect Command', () => { 4 | const paramsKey = { newApplication: 'netflix' }; 5 | const paramsName = { newApplicationName: 'Net Flix' }; 6 | 7 | test('validateParams', () => { 8 | expect(Command.validateParams({})).toBe(false); 9 | expect(Command.validateParams(paramsKey)).toBe(true); 10 | expect(Command.validateParams(paramsName)).toBe(true); 11 | }); 12 | 13 | test('requiresItem', () => { 14 | expect(Command.requiresItem()).toBe(true); 15 | }); 16 | 17 | test('getItemName', () => { 18 | expect(() => { 19 | Command.getItemName({ id: 'Item' }); 20 | }).toThrow(); 21 | const device = { 22 | customData: { 23 | members: { 24 | tvApplication: 'ApplicationItem' 25 | } 26 | } 27 | }; 28 | expect(Command.getItemName(device)).toBe('ApplicationItem'); 29 | }); 30 | 31 | test('convertParamsToValue', () => { 32 | const item = { 33 | metadata: { 34 | ga: { 35 | config: { 36 | availableApplications: 'youtube=YouTube:Tube,netflix=Net Flix:Flix' 37 | } 38 | } 39 | } 40 | }; 41 | expect(Command.convertParamsToValue(paramsKey, item)).toBe('netflix'); 42 | expect(Command.convertParamsToValue(paramsName, item)).toBe('netflix'); 43 | expect(Command.convertParamsToValue({ newApplicationName: 'Tube' }, item)).toBe('youtube'); 44 | expect(() => { 45 | Command.convertParamsToValue({ newApplication: 'wrong' }, item); 46 | }).toThrow(); 47 | expect(() => { 48 | Command.convertParamsToValue({ newApplicationName: 'wrong' }, item); 49 | }).toThrow(); 50 | }); 51 | 52 | test('getResponseStates', () => { 53 | const item = { 54 | metadata: { 55 | ga: { 56 | config: { 57 | availableApplications: 'youtube=YouTube,netflix=Net Flix' 58 | } 59 | } 60 | } 61 | }; 62 | expect(Command.getResponseStates(paramsKey, item)).toStrictEqual({ currentApplication: 'netflix' }); 63 | expect(Command.getResponseStates(paramsName, item)).toStrictEqual({ currentApplication: 'netflix' }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/commands/brightnessabsolute.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/brightnessabsolute.js'); 2 | 3 | describe('BrightnessAbsolute Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(false); 6 | expect(Command.validateParams({ brightness: 100 })).toBe(true); 7 | expect(Command.validateParams({ brightness: '100' })).toBe(false); 8 | }); 9 | 10 | test('getItemName', () => { 11 | expect(Command.getItemName({ id: 'Item' })).toBe('Item'); 12 | expect(Command.getItemName({ id: 'Item', customData: {} })).toBe('Item'); 13 | expect(() => { 14 | Command.getItemName({ id: 'Item', customData: { deviceType: 'SpecialColorLight' } }); 15 | }).toThrow(); 16 | const device = { 17 | id: 'Item', 18 | customData: { 19 | deviceType: 'SpecialColorLight', 20 | members: { 21 | lightBrightness: 'BrightnessItem' 22 | } 23 | } 24 | }; 25 | expect(Command.getItemName(device)).toBe('BrightnessItem'); 26 | }); 27 | 28 | test('convertParamsToValue', () => { 29 | expect(Command.convertParamsToValue({ brightness: 0 })).toBe('0'); 30 | expect(Command.convertParamsToValue({ brightness: 100 })).toBe('100'); 31 | }); 32 | 33 | test('getResponseStates', () => { 34 | expect(Command.getResponseStates({ brightness: 0 })).toStrictEqual({ brightness: 0 }); 35 | expect(Command.getResponseStates({ brightness: 100 })).toStrictEqual({ brightness: 100 }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/commands/charge.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/charge.js'); 2 | 3 | describe('Charge Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(false); 6 | expect(Command.validateParams({ charge: true })).toBe(true); 7 | }); 8 | 9 | test('getItemName', () => { 10 | expect(() => { 11 | Command.getItemName({ id: 'Item' }); 12 | }).toThrow(); 13 | 14 | const device = { 15 | customData: { 16 | members: { 17 | chargerCharging: 'ChargingItem' 18 | } 19 | } 20 | }; 21 | expect(Command.getItemName(device)).toBe('ChargingItem'); 22 | }); 23 | 24 | describe('convertParamsToValue', () => { 25 | test('convertParamsToValue', () => { 26 | expect(Command.convertParamsToValue({ charge: true }, {}, {})).toBe('ON'); 27 | }); 28 | 29 | test('convertParamsToValue inverted', () => { 30 | expect(Command.convertParamsToValue({ charge: true }, {}, { customData: { inverted: true } })).toBe('OFF'); 31 | }); 32 | }); 33 | 34 | test('getResponseStates', () => { 35 | const item = { 36 | members: [ 37 | { 38 | type: 'Switch', 39 | metadata: { 40 | ga: { 41 | value: 'chargerCharging' 42 | } 43 | }, 44 | state: 'OFF' 45 | }, 46 | { 47 | type: 'Number', 48 | metadata: { 49 | ga: { 50 | value: 'chargerCapacityRemaining' 51 | } 52 | }, 53 | state: '50' 54 | } 55 | ] 56 | }; 57 | expect(Command.getResponseStates({ charge: true }, item)).toStrictEqual({ 58 | isCharging: true, 59 | descriptiveCapacityRemaining: 'MEDIUM', 60 | capacityRemaining: [ 61 | { 62 | rawValue: 50, 63 | unit: 'PERCENTAGE' 64 | } 65 | ] 66 | }); 67 | expect(Command.getResponseStates({ charge: false }, item)).toStrictEqual({ 68 | isCharging: false, 69 | descriptiveCapacityRemaining: 'MEDIUM', 70 | capacityRemaining: [ 71 | { 72 | rawValue: 50, 73 | unit: 'PERCENTAGE' 74 | } 75 | ] 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /tests/commands/colorabsolute.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/colorabsolute.js'); 2 | 3 | describe('ColorAbsolute Command', () => { 4 | const params = { 5 | color: { 6 | spectrumHSV: { hue: 10, saturation: 0.2, value: 0.3 } 7 | } 8 | }; 9 | 10 | test('validateParams', () => { 11 | expect(Command.validateParams({})).toBe(false); 12 | expect(Command.validateParams({ color: {} })).toBe(false); 13 | expect(Command.validateParams(params)).toBe(true); 14 | }); 15 | 16 | test('getItemName', () => { 17 | expect(Command.getItemName({ id: 'Item' })).toBe('Item'); 18 | expect(Command.getItemName({ id: 'Item' }, {})).toBe('Item'); 19 | expect(Command.getItemName({ id: 'Item' }, { customData: {} })).toBe('Item'); 20 | expect(Command.getItemName({ id: 'Item' }, { customData: { deviceType: 'ColorLight' } })).toBe('Item'); 21 | expect(() => { 22 | Command.getItemName({ id: 'Item', customData: { deviceType: 'SpecialColorLight' } }); 23 | }).toThrow(); 24 | expect( 25 | Command.getItemName({ 26 | id: 'Item', 27 | customData: { 28 | deviceType: 'SpecialColorLight', 29 | members: { 30 | lightColor: 'ColorItem' 31 | } 32 | } 33 | }) 34 | ).toBe('ColorItem'); 35 | }); 36 | 37 | test('convertParamsToValue', () => { 38 | expect(Command.convertParamsToValue(params, {}, { customData: { deviceType: 'ColorLight' } })).toBe('10,20,30'); 39 | expect(Command.convertParamsToValue(params, {}, { customData: { deviceType: 'SpecialColorLight' } })).toBe( 40 | '10,20,30' 41 | ); 42 | expect(() => Command.convertParamsToValue(params, {}, { customData: { deviceType: 'Light' } })).toThrow(); 43 | }); 44 | 45 | test('getResponseStates', () => { 46 | expect(Command.getResponseStates(params)).toStrictEqual({ 47 | color: { 48 | spectrumHsv: { hue: 10, saturation: 0.2, value: 0.3 } 49 | } 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/commands/colorabsolutetemperature.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/colorabsolutetemperature.js'); 2 | 3 | describe('ColorAbsoluteTemperature Command', () => { 4 | const params = { 5 | color: { 6 | temperature: 2000 7 | } 8 | }; 9 | 10 | test('validateParams', () => { 11 | expect(Command.validateParams({})).toBe(false); 12 | expect(Command.validateParams({ color: {} })).toBe(false); 13 | expect(Command.validateParams(params)).toBe(true); 14 | }); 15 | 16 | test('requiresItem', () => { 17 | expect(Command.requiresItem({})).toBe(true); 18 | expect(Command.requiresItem({ customData: { deviceType: 'SpecialColorLight' } })).toBe(false); 19 | }); 20 | 21 | test('getItemName', () => { 22 | expect(Command.getItemName({ id: 'Item' })).toBe('Item'); 23 | expect(Command.getItemName({ id: 'Item', customData: {} })).toBe('Item'); 24 | expect(() => { 25 | Command.getItemName({ id: 'Item', customData: { deviceType: 'SpecialColorLight' } }); 26 | }).toThrow(); 27 | expect( 28 | Command.getItemName({ 29 | id: 'Item', 30 | customData: { 31 | deviceType: 'SpecialColorLight', 32 | members: { 33 | lightColorTemperature: 'ColorItem' 34 | } 35 | } 36 | }) 37 | ).toBe('ColorItem'); 38 | }); 39 | 40 | describe('convertParamsToValue', () => { 41 | test('convertParamsToValue', () => { 42 | expect(Command.convertParamsToValue(params, { state: '100,100,50' }, {})).toBe('30.62,95,50'); 43 | }); 44 | 45 | test('convertParamsToValue SpecialColorLight Percent', () => { 46 | const device = { 47 | customData: { 48 | deviceType: 'SpecialColorLight', 49 | colorUnit: 'percent', 50 | colorTemperatureRange: { temperatureMinK: 1000, temperatureMaxK: 5000 } 51 | } 52 | }; 53 | expect(Command.convertParamsToValue(params, {}, device)).toBe('25'); 54 | }); 55 | 56 | test('convertParamsToValue SpecialColorLight Percent Inverted', () => { 57 | const device = { 58 | customData: { 59 | deviceType: 'SpecialColorLight', 60 | colorUnit: 'percent', 61 | colorTemperatureRange: { temperatureMinK: 1000, temperatureMaxK: 5000 }, 62 | colorTemperatureInverted: true 63 | } 64 | }; 65 | expect(Command.convertParamsToValue(params, {}, device)).toBe('75'); 66 | }); 67 | 68 | test('convertParamsToValue SpecialColorLight Invalid', () => { 69 | const device = { 70 | customData: { 71 | deviceType: 'SpecialColorLight' 72 | } 73 | }; 74 | expect(Command.convertParamsToValue(params, { state: '100,100,50' }, device)).toBe('0'); 75 | }); 76 | 77 | test('convertParamsToValue SpecialColorLight Kelvin', () => { 78 | const device = { customData: { deviceType: 'SpecialColorLight', colorUnit: 'kelvin' } }; 79 | expect(Command.convertParamsToValue(params, {}, device)).toBe('2000'); 80 | }); 81 | 82 | test('convertParamsToValue SpecialColorLight Mired', () => { 83 | const device = { customData: { deviceType: 'SpecialColorLight', colorUnit: 'mired' } }; 84 | expect(Command.convertParamsToValue(params, {}, device)).toBe('500'); 85 | }); 86 | }); 87 | 88 | test('getResponseStates', () => { 89 | expect(Command.getResponseStates(params)).toStrictEqual({ 90 | color: { 91 | temperatureK: 2000 92 | } 93 | }); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /tests/commands/getcamerastream.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/getcamerastream.js'); 2 | 3 | describe('GetCameraStream Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(false); 6 | expect(Command.validateParams({ StreamToChromecast: true })).toBe(false); 7 | expect(Command.validateParams({ StreamToChromecast: true, SupportedStreamProtocols: {} })).toBe(true); 8 | }); 9 | 10 | test('requiresItem', () => { 11 | expect(Command.requiresItem()).toBe(true); 12 | }); 13 | 14 | test('convertParamsToValue', () => { 15 | expect(Command.convertParamsToValue()).toBe(null); 16 | }); 17 | 18 | test('getResponseStates', () => { 19 | expect(Command.getResponseStates({}, { state: 'https://example.org' })).toStrictEqual({ 20 | cameraStreamAccessUrl: 'https://example.org' 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/commands/index.test.js: -------------------------------------------------------------------------------- 1 | const getCommandType = require('../../functions/commands/index.js').getCommandType; 2 | 3 | describe('Commands Index', () => { 4 | test('getCommandType', () => { 5 | const command = getCommandType('action.devices.commands.OnOff', { on: true }); 6 | expect(command).not.toBeUndefined(); 7 | expect(command.name).toBe('OnOff'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/commands/lockunlock.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/lockunlock.js'); 2 | 3 | describe('LockUnlock Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(false); 6 | expect(Command.validateParams({ lock: true })).toBe(true); 7 | }); 8 | 9 | describe('convertParamsToValue', () => { 10 | test('convertParamsToValue', () => { 11 | expect(Command.convertParamsToValue({ lock: true }, {}, {})).toBe('ON'); 12 | expect(Command.convertParamsToValue({ lock: false }, {}, {})).toBe('OFF'); 13 | }); 14 | test('convertParamsToValue inverted', () => { 15 | expect(Command.convertParamsToValue({ lock: true }, {}, { customData: { inverted: true } })).toBe('OFF'); 16 | expect(Command.convertParamsToValue({ lock: false }, {}, { customData: { inverted: true } })).toBe('ON'); 17 | }); 18 | test('convertParamsToValue Contact', () => { 19 | expect(() => { 20 | Command.convertParamsToValue({ lock: true }, {}, { customData: { itemType: 'Contact' } }); 21 | }).toThrow(); 22 | }); 23 | }); 24 | 25 | test('getResponseStates', () => { 26 | expect(Command.getResponseStates({ lock: true })).toStrictEqual({ isLocked: true }); 27 | expect(Command.getResponseStates({ lock: false })).toStrictEqual({ isLocked: false }); 28 | }); 29 | 30 | test('checkCurrentState', () => { 31 | expect.assertions(4); 32 | 33 | expect(Command.checkCurrentState('ON', 'OFF', { lock: true })).toBeUndefined(); 34 | try { 35 | Command.checkCurrentState('ON', 'ON', { lock: true }); 36 | } catch (e) { 37 | expect(e.errorCode).toBe('alreadyLocked'); 38 | } 39 | 40 | expect(Command.checkCurrentState('OFF', 'ON', { lock: false })).toBeUndefined(); 41 | try { 42 | Command.checkCurrentState('OFF', 'OFF', { lock: false }); 43 | } catch (e) { 44 | expect(e.errorCode).toBe('alreadyUnlocked'); 45 | } 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/commands/medianext.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/medianext.js'); 2 | 3 | describe('mediaNext Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(true); 6 | }); 7 | 8 | test('getItemName', () => { 9 | const device = { 10 | customData: { 11 | members: { 12 | tvTransport: 'TransportItem' 13 | } 14 | } 15 | }; 16 | expect(Command.getItemName(device)).toBe('TransportItem'); 17 | expect(() => { 18 | Command.getItemName({ customData: { members: {} } }); 19 | }).toThrow(); 20 | }); 21 | 22 | test('convertParamsToValue', () => { 23 | expect(Command.convertParamsToValue()).toBe('NEXT'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/commands/mediapause.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/mediapause.js'); 2 | 3 | describe('mediaPause Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(true); 6 | }); 7 | 8 | test('getItemName', () => { 9 | const device = { 10 | customData: { 11 | members: { 12 | tvTransport: 'TransportItem' 13 | } 14 | } 15 | }; 16 | expect(Command.getItemName(device)).toBe('TransportItem'); 17 | expect(() => { 18 | Command.getItemName({ customData: { members: {} } }); 19 | }).toThrow(); 20 | }); 21 | 22 | test('convertParamsToValue', () => { 23 | expect(Command.convertParamsToValue()).toBe('PAUSE'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/commands/mediaprevious.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/mediaprevious.js'); 2 | 3 | describe('mediaPrevious Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(true); 6 | }); 7 | 8 | test('getItemName', () => { 9 | const device = { 10 | customData: { 11 | members: { 12 | tvTransport: 'TransportItem' 13 | } 14 | } 15 | }; 16 | expect(Command.getItemName(device)).toBe('TransportItem'); 17 | expect(() => { 18 | Command.getItemName({ customData: { members: {} } }); 19 | }).toThrow(); 20 | }); 21 | 22 | test('convertParamsToValue', () => { 23 | expect(Command.convertParamsToValue()).toBe('PREVIOUS'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/commands/mediaresume.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/mediaresume.js'); 2 | 3 | describe('mediaResume Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(true); 6 | }); 7 | 8 | test('getItemName', () => { 9 | const device = { 10 | customData: { 11 | members: { 12 | tvTransport: 'TransportItem' 13 | } 14 | } 15 | }; 16 | expect(Command.getItemName(device)).toBe('TransportItem'); 17 | expect(() => { 18 | Command.getItemName({ customData: { members: {} } }); 19 | }).toThrow(); 20 | }); 21 | 22 | test('convertParamsToValue', () => { 23 | expect(Command.convertParamsToValue()).toBe('PLAY'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tests/commands/mute.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/mute.js'); 2 | 3 | describe('Mute Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(false); 6 | expect(Command.validateParams({ mute: true })).toBe(true); 7 | }); 8 | 9 | describe('getItemName', () => { 10 | test('getItemName', () => { 11 | expect(Command.getItemName({ id: 'Item' })).toBe('Item'); 12 | expect(Command.getItemName({ id: 'Item', customData: {} })).toBe('Item'); 13 | }); 14 | 15 | test('getItemName TV no members', () => { 16 | expect(() => { 17 | Command.getItemName({ id: 'Item', customData: { deviceType: 'TV' } }); 18 | }).toThrow(); 19 | }); 20 | 21 | test('getItemName TV mute', () => { 22 | const device = { 23 | id: 'Item', 24 | customData: { 25 | deviceType: 'TV', 26 | members: { 27 | tvMute: 'MuteItem' 28 | } 29 | } 30 | }; 31 | expect(Command.getItemName(device)).toBe('MuteItem'); 32 | }); 33 | 34 | test('getItemName TV volume', () => { 35 | const device = { 36 | id: 'Item', 37 | customData: { 38 | deviceType: 'TV', 39 | members: { 40 | tvVolume: 'VolumeItem' 41 | } 42 | } 43 | }; 44 | expect(Command.getItemName(device)).toBe('VolumeItem'); 45 | }); 46 | }); 47 | 48 | describe('convertParamsToValue', () => { 49 | test('convertParamsToValue no Switch', () => { 50 | expect(Command.convertParamsToValue({ mute: true }, {}, {})).toBe('0'); 51 | expect(Command.convertParamsToValue({ mute: false }, {}, {})).toBe(null); 52 | }); 53 | 54 | test('convertParamsToValue Switch', () => { 55 | expect(Command.convertParamsToValue({ mute: true }, {}, { customData: { itemType: 'Switch' } })).toBe('ON'); 56 | expect(Command.convertParamsToValue({ mute: false }, {}, { customData: { itemType: 'Switch' } })).toBe('OFF'); 57 | }); 58 | 59 | test('convertParamsToValue inverted', () => { 60 | expect( 61 | Command.convertParamsToValue({ mute: true }, {}, { customData: { itemType: 'Switch', inverted: true } }) 62 | ).toBe('OFF'); 63 | expect( 64 | Command.convertParamsToValue({ mute: false }, {}, { customData: { itemType: 'Switch', inverted: true } }) 65 | ).toBe('ON'); 66 | }); 67 | 68 | test('convertParamsToValue TV mute', () => { 69 | expect( 70 | Command.convertParamsToValue({ mute: true }, undefined, { 71 | customData: { deviceType: 'TV', members: { tvMute: 'MuteItem' } } 72 | }) 73 | ).toBe('ON'); 74 | }); 75 | 76 | test('convertParamsToValue TV volume', () => { 77 | expect( 78 | Command.convertParamsToValue({ mute: true }, undefined, { 79 | customData: { deviceType: 'TV', members: { tvVolume: 'VolumeItem' } } 80 | }) 81 | ).toBe('0'); 82 | }); 83 | }); 84 | 85 | test('getResponseStates', () => { 86 | expect(Command.getResponseStates({ mute: true })).toStrictEqual({ isMuted: true }); 87 | expect(Command.getResponseStates({ mute: false })).toStrictEqual({ isMuted: false }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /tests/commands/onoff.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/onoff.js'); 2 | 3 | describe('OnOff Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(false); 6 | expect(Command.validateParams({ on: true })).toBe(true); 7 | }); 8 | 9 | describe('getItemName', () => { 10 | test('getItemName', () => { 11 | expect(Command.getItemName({ id: 'Item' })).toBe('Item'); 12 | expect(Command.getItemName({ id: 'Item', customData: {} })).toBe('Item'); 13 | }); 14 | 15 | test('getItemName DynamicModesLight', () => { 16 | expect(() => { 17 | Command.getItemName({ id: 'Item', customData: { deviceType: 'DynamicModesLight' } }); 18 | }).toThrow(); 19 | }); 20 | 21 | test('getItemName SpecialColorLight', () => { 22 | expect(() => { 23 | Command.getItemName({ id: 'Item', customData: { deviceType: 'SpecialColorLight' } }); 24 | }).toThrow(); 25 | const device = { 26 | id: 'Item', 27 | customData: { 28 | deviceType: 'SpecialColorLight', 29 | members: { 30 | lightBrightness: 'BrightnessItem' 31 | } 32 | } 33 | }; 34 | expect(Command.getItemName(device)).toBe('BrightnessItem'); 35 | const device_power = { 36 | id: 'Item', 37 | customData: { 38 | deviceType: 'SpecialColorLight', 39 | members: { 40 | lightPower: 'PowerItem' 41 | } 42 | } 43 | }; 44 | expect(Command.getItemName(device_power)).toBe('PowerItem'); 45 | }); 46 | 47 | test('getItemName TV', () => { 48 | expect(() => { 49 | Command.getItemName({ name: 'Item', customData: { deviceType: 'TV' } }); 50 | }).toThrow(); 51 | const device = { 52 | id: 'Item', 53 | customData: { 54 | deviceType: 'TV', 55 | members: { 56 | tvPower: 'PowerItem' 57 | } 58 | } 59 | }; 60 | expect(Command.getItemName(device)).toBe('PowerItem'); 61 | }); 62 | 63 | test('getItemName Fan', () => { 64 | expect(() => { 65 | Command.getItemName({ name: 'Item', customData: { deviceType: 'Fan', itemType: 'Group' } }); 66 | }).toThrow(); 67 | const device = { 68 | id: 'Item', 69 | customData: { 70 | deviceType: 'Fan', 71 | itemType: 'Group', 72 | members: { 73 | fanPower: 'PowerItem' 74 | } 75 | } 76 | }; 77 | expect(Command.getItemName(device)).toBe('PowerItem'); 78 | expect(Command.getItemName({ id: 'Item', customData: { deviceType: 'Fan', itemType: 'Dimmer' } })).toBe('Item'); 79 | }); 80 | 81 | test('getItemName ACUnit', () => { 82 | expect(() => { 83 | Command.getItemName({ name: 'Item', customData: { deviceType: 'ACUnit', itemType: 'Group' } }); 84 | }).toThrow(); 85 | const device = { 86 | id: 'Item', 87 | customData: { 88 | deviceType: 'ACUnit', 89 | itemType: 'Group', 90 | members: { 91 | fanPower: 'PowerItem' 92 | } 93 | } 94 | }; 95 | expect(Command.getItemName(device)).toBe('PowerItem'); 96 | }); 97 | }); 98 | 99 | describe('convertParamsToValue', () => { 100 | test('convertParamsToValue', () => { 101 | expect(Command.convertParamsToValue({ on: true }, {}, {})).toBe('ON'); 102 | }); 103 | 104 | test('convertParamsToValue inverted', () => { 105 | expect(Command.convertParamsToValue({ on: true }, {}, { customData: { inverted: true } })).toBe('OFF'); 106 | }); 107 | }); 108 | 109 | test('getResponseStates', () => { 110 | expect(Command.getResponseStates({ on: true })).toStrictEqual({ on: true }); 111 | expect(Command.getResponseStates({ on: false })).toStrictEqual({ on: false }); 112 | }); 113 | 114 | test('checkCurrentState', () => { 115 | expect.assertions(4); 116 | 117 | expect(Command.checkCurrentState('ON', 'OFF', { on: true })).toBeUndefined(); 118 | try { 119 | Command.checkCurrentState('ON', 'ON', { on: true }); 120 | } catch (e) { 121 | expect(e.errorCode).toBe('alreadyOn'); 122 | } 123 | 124 | expect(Command.checkCurrentState('OFF', 'ON', { on: false })).toBeUndefined(); 125 | try { 126 | Command.checkCurrentState('OFF', 'OFF', { on: false }); 127 | } catch (e) { 128 | expect(e.errorCode).toBe('alreadyOff'); 129 | } 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /tests/commands/openclose.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/openclose.js'); 2 | 3 | describe('OpenClose Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(false); 6 | expect(Command.validateParams({ openPercent: 100 })).toBe(true); 7 | expect(Command.validateParams({ openPercent: '5' })).toBe(false); 8 | }); 9 | 10 | describe('convertParamsToValue', () => { 11 | test('convertParamsToValue', () => { 12 | expect(Command.convertParamsToValue({ openPercent: 0 }, {}, {})).toBe('0'); 13 | expect(Command.convertParamsToValue({ openPercent: 20 }, {}, {})).toBe('20'); 14 | expect(Command.convertParamsToValue({ openPercent: 50 }, {}, {})).toBe('50'); 15 | expect(Command.convertParamsToValue({ openPercent: 70 }, {}, {})).toBe('70'); 16 | expect(Command.convertParamsToValue({ openPercent: 100 }, {}, {})).toBe('100'); 17 | }); 18 | 19 | test('convertParamsToValue inverted', () => { 20 | const device = { customData: { inverted: true } }; 21 | expect(Command.convertParamsToValue({ openPercent: 0 }, {}, device)).toBe('100'); 22 | expect(Command.convertParamsToValue({ openPercent: 20 }, {}, device)).toBe('80'); 23 | expect(Command.convertParamsToValue({ openPercent: 50 }, {}, device)).toBe('50'); 24 | expect(Command.convertParamsToValue({ openPercent: 70 }, {}, device)).toBe('30'); 25 | expect(Command.convertParamsToValue({ openPercent: 100 }, {}, device)).toBe('0'); 26 | }); 27 | 28 | test('convertParamsToValue Rollershutter', () => { 29 | const device = { customData: { itemType: 'Rollershutter' } }; 30 | expect(Command.convertParamsToValue({ openPercent: 0 }, {}, device)).toBe('DOWN'); 31 | expect(Command.convertParamsToValue({ openPercent: 20 }, {}, device)).toBe('80'); 32 | expect(Command.convertParamsToValue({ openPercent: 100 }, {}, device)).toBe('UP'); 33 | }); 34 | 35 | test('convertParamsToValue Switch', () => { 36 | const device = { customData: { itemType: 'Switch' } }; 37 | expect(Command.convertParamsToValue({ openPercent: 0 }, {}, device)).toBe('OFF'); 38 | expect(Command.convertParamsToValue({ openPercent: 20 }, {}, device)).toBe('ON'); 39 | expect(Command.convertParamsToValue({ openPercent: 100 }, {}, device)).toBe('ON'); 40 | }); 41 | 42 | test('convertParamsToValue Contact', () => { 43 | const device = { customData: { itemType: 'Contact' } }; 44 | expect(() => { 45 | Command.convertParamsToValue({}, {}, device); 46 | }).toThrow(); 47 | }); 48 | }); 49 | 50 | test('getResponseStates', () => { 51 | expect(Command.getResponseStates({ openPercent: 10 })).toStrictEqual({ openPercent: 10 }); 52 | }); 53 | 54 | describe('checkCurrentState', () => { 55 | test('Switch', () => { 56 | expect.assertions(8); 57 | 58 | expect(Command.checkCurrentState('ON', 'OFF', { openPercent: 100 })).toBeUndefined(); 59 | try { 60 | Command.checkCurrentState('ON', 'ON', { openPercent: 100 }); 61 | } catch (e) { 62 | expect(e.errorCode).toBe('alreadyOpen'); 63 | } 64 | 65 | expect(Command.checkCurrentState('OFF', 'ON', { openPercent: 0 })).toBeUndefined(); 66 | try { 67 | Command.checkCurrentState('OFF', 'OFF', { openPercent: 0 }); 68 | } catch (e) { 69 | expect(e.errorCode).toBe('alreadyClosed'); 70 | } 71 | 72 | expect(Command.checkCurrentState('UP', '100', { openPercent: 100 })).toBeUndefined(); 73 | try { 74 | Command.checkCurrentState('UP', '0', { openPercent: 100 }); 75 | } catch (e) { 76 | expect(e.errorCode).toBe('alreadyOpen'); 77 | } 78 | 79 | expect(Command.checkCurrentState('DOWN', '0', { openPercent: 0 })).toBeUndefined(); 80 | try { 81 | Command.checkCurrentState('DOWN', '100', { openPercent: 0 }); 82 | } catch (e) { 83 | expect(e.errorCode).toBe('alreadyClosed'); 84 | } 85 | }); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /tests/commands/selectchannel.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/selectchannel.js'); 2 | 3 | describe('selectChannel Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(false); 6 | expect(Command.validateParams({ channelCode: 'channel1' })).toBe(true); 7 | expect(Command.validateParams({ channelName: 'Channel 1' })).toBe(true); 8 | expect(Command.validateParams({ channelNumber: '1' })).toBe(true); 9 | }); 10 | 11 | test('requiresItem', () => { 12 | expect(Command.requiresItem()).toBe(true); 13 | }); 14 | 15 | test('getItemName', () => { 16 | expect(() => { 17 | Command.getItemName({ id: 'Item' }); 18 | }).toThrow(); 19 | const device = { 20 | customData: { 21 | members: { 22 | tvChannel: 'ChannelItem' 23 | } 24 | } 25 | }; 26 | expect(Command.getItemName(device)).toBe('ChannelItem'); 27 | }); 28 | 29 | test('convertParamsToValue', () => { 30 | const item = { 31 | metadata: { 32 | ga: { 33 | config: { 34 | availableChannels: '1=channel1=ARD,2=channel2=ZDF,3=channel3=Disney Channel' 35 | } 36 | } 37 | } 38 | }; 39 | expect(Command.convertParamsToValue({ channelCode: 'channel1' }, item)).toBe('1'); 40 | expect(Command.convertParamsToValue({ channelCode: 'channel3' }, item)).toBe('3'); 41 | expect(Command.convertParamsToValue({ channelName: 'ARD' }, item)).toBe('1'); 42 | expect(Command.convertParamsToValue({ channelName: 'Ard' }, item)).toBe('1'); 43 | expect(Command.convertParamsToValue({ channelName: 'Disney Channel' }, item)).toBe('3'); 44 | expect(Command.convertParamsToValue({ channelNumber: '1' }, item)).toBe('1'); 45 | expect(() => { 46 | Command.convertParamsToValue({ channelNumber: '0' }, item); 47 | }).toThrow(); 48 | expect(() => { 49 | Command.convertParamsToValue({ channelName: 'wrong' }, item); 50 | }).toThrow(); 51 | }); 52 | 53 | test('getResponseStates', () => { 54 | const item = { 55 | metadata: { 56 | ga: { 57 | config: { 58 | availableChannels: '1=channel1=ARD,2=channel2=ZDF' 59 | } 60 | } 61 | } 62 | }; 63 | expect(Command.getResponseStates({ channelName: 'ZDF' }, item)).toStrictEqual({ channelNumber: '2' }); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/commands/setfanspeed.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/setfanspeed.js'); 2 | 3 | describe('SetFanSpeed Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(false); 6 | expect(Command.validateParams({ fanSpeed: '50' })).toBe(true); 7 | expect(Command.validateParams({ fanSpeedPercent: 50 })).toBe(true); 8 | }); 9 | 10 | describe('getItemName', () => { 11 | test('getItemName', () => { 12 | expect(Command.getItemName({ id: 'Item' })).toBe('Item'); 13 | expect(Command.getItemName({ id: 'Item', customData: {} })).toBe('Item'); 14 | }); 15 | 16 | test('getItemName Fan', () => { 17 | expect(() => { 18 | Command.getItemName({ id: 'Item', customData: { deviceType: 'Fan', itemType: 'Group' } }); 19 | }).toThrow(); 20 | const device = { 21 | customData: { 22 | deviceType: 'Fan', 23 | itemType: 'Group', 24 | members: { 25 | fanSpeed: 'SpeedItem' 26 | } 27 | } 28 | }; 29 | expect(Command.getItemName(device)).toBe('SpeedItem'); 30 | expect(Command.getItemName({ id: 'Item', customData: { deviceType: 'Fan', itemType: 'Dimmer' } })).toBe('Item'); 31 | }); 32 | 33 | test('getItemName ACUnit', () => { 34 | expect(() => { 35 | Command.getItemName({ id: 'Item', customData: { deviceType: 'ACUnit', itemType: 'Group' } }); 36 | }).toThrow(); 37 | const device = { 38 | customData: { 39 | deviceType: 'ACUnit', 40 | itemType: 'Group', 41 | members: { 42 | fanSpeed: 'SpeedItem' 43 | } 44 | } 45 | }; 46 | expect(Command.getItemName(device)).toBe('SpeedItem'); 47 | }); 48 | }); 49 | 50 | test('convertParamsToValue', () => { 51 | expect(Command.convertParamsToValue({ fanSpeed: '50' })).toStrictEqual('50'); 52 | expect(Command.convertParamsToValue({ fanSpeedPercent: 50 })).toStrictEqual('50'); 53 | }); 54 | 55 | test('getResponseStates', () => { 56 | expect(Command.getResponseStates({ fanSpeed: '50' })).toStrictEqual({ 57 | currentFanSpeedPercent: 50, 58 | currentFanSpeedSetting: '50' 59 | }); 60 | expect(Command.getResponseStates({ fanSpeedPercent: 50 })).toStrictEqual({ 61 | currentFanSpeedPercent: 50 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/commands/setinput.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/setinput.js'); 2 | 3 | describe('SetInput Command', () => { 4 | const params = { newInput: 'hdmi1' }; 5 | 6 | test('validateParams', () => { 7 | expect(Command.validateParams({})).toBe(false); 8 | expect(Command.validateParams(params)).toBe(true); 9 | }); 10 | 11 | test('getItemName', () => { 12 | expect(() => { 13 | Command.getItemName({ id: 'Item' }); 14 | }).toThrow(); 15 | const device = { 16 | customData: { 17 | members: { 18 | tvInput: 'InputItem' 19 | } 20 | } 21 | }; 22 | expect(Command.getItemName(device)).toBe('InputItem'); 23 | }); 24 | 25 | test('convertParamsToValue', () => { 26 | expect(Command.convertParamsToValue(params)).toBe('hdmi1'); 27 | }); 28 | 29 | test('getResponseStates', () => { 30 | expect(Command.getResponseStates(params)).toStrictEqual({ currentInput: 'hdmi1' }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/commands/setmodes.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/setmodes.js'); 2 | 3 | describe('SetModes Command', () => { 4 | const params = { updateModeSettings: { mode: 'value' } }; 5 | 6 | test('validateParams', () => { 7 | expect(Command.validateParams({})).toBe(false); 8 | expect(Command.validateParams(params)).toBe(true); 9 | }); 10 | 11 | describe('getItemName', () => { 12 | test('getItemName', () => { 13 | expect(Command.getItemName({ id: 'Item' })).toBe('Item'); 14 | expect(Command.getItemName({ id: 'Item', customData: {} })).toBe('Item'); 15 | }); 16 | 17 | test('getItemName DynamicModesLight', () => { 18 | expect(() => { 19 | Command.getItemName({ id: 'Item', customData: { deviceType: 'DynamicModesLight' } }); 20 | }).toThrow(); 21 | const device = { 22 | id: 'Item', 23 | customData: { 24 | deviceType: 'DynamicModesLight', 25 | members: { 26 | modesCurrentMode: 'CurrentMode' 27 | } 28 | } 29 | }; 30 | expect(Command.getItemName(device)).toBe('CurrentMode'); 31 | }); 32 | 33 | test('getItemName Fan', () => { 34 | expect(() => { 35 | Command.getItemName({ name: 'Item', customData: { deviceType: 'Fan' } }); 36 | }).toThrow(); 37 | const device = { 38 | id: 'Item', 39 | customData: { 40 | deviceType: 'Fan', 41 | members: { 42 | fanMode: 'ModeItem' 43 | } 44 | } 45 | }; 46 | expect(Command.getItemName(device)).toBe('ModeItem'); 47 | }); 48 | }); 49 | 50 | test('convertParamsToValue', () => { 51 | expect(Command.convertParamsToValue(params)).toBe('value'); 52 | }); 53 | 54 | test('getResponseStates', () => { 55 | expect(Command.getResponseStates(params)).toStrictEqual({ 56 | currentModeSettings: { mode: 'value' } 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/commands/setvolume.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/setvolume.js'); 2 | 3 | describe('setVolume Command', () => { 4 | const params = { volumeLevel: 20 }; 5 | 6 | test('validateParams', () => { 7 | expect(Command.validateParams({})).toBe(false); 8 | expect(Command.validateParams(params)).toBe(true); 9 | }); 10 | 11 | describe('getItemName', () => { 12 | test('getItemName', () => { 13 | expect(Command.getItemName({ id: 'Item' })).toBe('Item'); 14 | expect(Command.getItemName({ id: 'Item', customData: {} })).toBe('Item'); 15 | }); 16 | 17 | test('getItemName TV', () => { 18 | expect(() => { 19 | Command.getItemName({ id: 'Item', customData: { deviceType: 'TV' } }); 20 | }).toThrow(); 21 | const device = { 22 | customData: { 23 | deviceType: 'TV', 24 | members: { 25 | tvVolume: 'VolumeItem' 26 | } 27 | } 28 | }; 29 | expect(Command.getItemName(device)).toBe('VolumeItem'); 30 | }); 31 | }); 32 | 33 | test('convertParamsToValue', () => { 34 | expect(Command.convertParamsToValue(params)).toBe('20'); 35 | }); 36 | 37 | test('getResponseStates', () => { 38 | expect(Command.getResponseStates(params)).toStrictEqual({ currentVolume: 20 }); 39 | }); 40 | 41 | test('checkCurrentState', () => { 42 | expect.assertions(6); 43 | 44 | expect(Command.checkCurrentState('100', '10')).toBeUndefined(); 45 | try { 46 | Command.checkCurrentState('100', '100'); 47 | } catch (e) { 48 | expect(e.errorCode).toBe('volumeAlreadyMax'); 49 | } 50 | 51 | expect(Command.checkCurrentState('0', '10')).toBeUndefined(); 52 | try { 53 | Command.checkCurrentState('0', '0'); 54 | } catch (e) { 55 | expect(e.errorCode).toBe('volumeAlreadyMin'); 56 | } 57 | 58 | expect(Command.checkCurrentState('20', '40')).toBeUndefined(); 59 | try { 60 | Command.checkCurrentState('20', '20'); 61 | } catch (e) { 62 | expect(e.errorCode).toBe('alreadyInState'); 63 | } 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /tests/commands/startstop.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/startstop.js'); 2 | 3 | describe('StartStop Command', () => { 4 | test('validateParams', () => { 5 | expect(Command.validateParams({})).toBe(false); 6 | expect(Command.validateParams({ start: true })).toBe(true); 7 | expect(Command.validateParams({ start: '1' })).toBe(false); 8 | }); 9 | 10 | describe('convertParamsToValue', () => { 11 | test('convertParamsToValue', () => { 12 | expect(Command.convertParamsToValue({ start: true }, {}, {})).toBe('ON'); 13 | expect(Command.convertParamsToValue({ start: false }, {}, {})).toBe('OFF'); 14 | }); 15 | 16 | test('convertParamsToValue Rollershutter', () => { 17 | const device = { customData: { itemType: 'Rollershutter' } }; 18 | expect(Command.convertParamsToValue({ start: true }, {}, device)).toBe('MOVE'); 19 | expect(Command.convertParamsToValue({ start: false }, {}, device)).toBe('STOP'); 20 | }); 21 | 22 | test('convertParamsToValue Contact', () => { 23 | const device = { customData: { itemType: 'Contact' } }; 24 | expect(() => { 25 | Command.convertParamsToValue({}, {}, device); 26 | }).toThrow(); 27 | }); 28 | }); 29 | 30 | test('getResponseStates', () => { 31 | expect(Command.getResponseStates({ start: true })).toStrictEqual({ isRunning: true, isPaused: false }); 32 | expect(Command.getResponseStates({ start: false })).toStrictEqual({ isRunning: false, isPaused: true }); 33 | }); 34 | 35 | test('checkCurrentState', () => { 36 | expect.assertions(4); 37 | 38 | expect(Command.checkCurrentState('ON', 'OFF', { start: true })).toBeUndefined(); 39 | try { 40 | Command.checkCurrentState('ON', 'ON', { start: true }); 41 | } catch (e) { 42 | expect(e.errorCode).toBe('alreadyStarted'); 43 | } 44 | 45 | expect(Command.checkCurrentState('OFF', 'ON', { start: false })).toBeUndefined(); 46 | try { 47 | Command.checkCurrentState('OFF', 'OFF', { start: false }); 48 | } catch (e) { 49 | expect(e.errorCode).toBe('alreadyStopped'); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/commands/thermostatsetmode.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/thermostatsetmode.js'); 2 | 3 | describe('ThermostatSetMode Command', () => { 4 | const params = { thermostatMode: 'eco' }; 5 | 6 | test('validateParams', () => { 7 | expect(Command.validateParams({})).toBe(false); 8 | expect(Command.validateParams(params)).toBe(true); 9 | }); 10 | 11 | test('requiresItem', () => { 12 | expect(Command.requiresItem()).toBe(true); 13 | }); 14 | 15 | test('getItemName', () => { 16 | expect(() => { 17 | Command.getItemName({ id: 'Item' }); 18 | }).toThrow(); 19 | const device = { 20 | customData: { 21 | members: { 22 | thermostatMode: 'ModeItem' 23 | } 24 | } 25 | }; 26 | expect(Command.getItemName(device)).toBe('ModeItem'); 27 | }); 28 | 29 | test('convertParamsToValue', () => { 30 | const item = { 31 | metadata: { 32 | ga: { 33 | config: { 34 | thermostatModes: 'eco=ECO' 35 | } 36 | } 37 | } 38 | }; 39 | expect(Command.convertParamsToValue(params, item)).toBe('ECO'); 40 | }); 41 | 42 | test('getResponseStates', () => { 43 | const item = { 44 | members: [ 45 | { 46 | metadata: { 47 | ga: { 48 | value: 'thermostatMode' 49 | } 50 | } 51 | } 52 | ] 53 | }; 54 | expect(Command.getResponseStates(params, item)).toStrictEqual({ thermostatMode: 'eco' }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /tests/commands/thermostattemperaturesetpoint.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/thermostattemperaturesetpoint.js'); 2 | 3 | describe('ThermostatTemperatureSetpoint Command', () => { 4 | const params = { thermostatTemperatureSetpoint: 20 }; 5 | 6 | test('validateParams', () => { 7 | expect(Command.validateParams({})).toBe(false); 8 | expect(Command.validateParams(params)).toBe(true); 9 | }); 10 | 11 | test('requiresItem', () => { 12 | expect(Command.requiresItem()).toBe(true); 13 | }); 14 | 15 | test('getItemName', () => { 16 | expect(() => { 17 | Command.getItemName({ id: 'Item' }); 18 | }).toThrow(); 19 | const device = { 20 | customData: { 21 | members: { 22 | thermostatTemperatureSetpoint: 'SetpointItem' 23 | } 24 | } 25 | }; 26 | expect(Command.getItemName(device)).toBe('SetpointItem'); 27 | }); 28 | 29 | test('convertParamsToValue', () => { 30 | const item = { 31 | metadata: { 32 | ga: { 33 | config: { 34 | useFahrenheit: true 35 | } 36 | } 37 | } 38 | }; 39 | expect(Command.convertParamsToValue(params, item)).toBe('68'); 40 | expect(Command.convertParamsToValue(params, {})).toBe('20'); 41 | }); 42 | 43 | test('getResponseStates', () => { 44 | const item = { 45 | members: [ 46 | { 47 | metadata: { 48 | ga: { 49 | value: 'thermostatTemperatureSetpoint' 50 | } 51 | } 52 | } 53 | ] 54 | }; 55 | expect(Command.getResponseStates(params, item)).toStrictEqual({ thermostatTemperatureSetpoint: 20 }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/commands/thermostattemperaturesetpointhigh.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/thermostattemperaturesetpointhigh.js'); 2 | 3 | describe('ThermostatTemperatureSetpointHigh Command', () => { 4 | const params = { thermostatTemperatureSetpointHigh: 20 }; 5 | 6 | test('validateParams', () => { 7 | expect(Command.validateParams({})).toBe(false); 8 | expect(Command.validateParams(params)).toBe(true); 9 | }); 10 | 11 | test('requiresItem', () => { 12 | expect(Command.requiresItem()).toBe(true); 13 | }); 14 | 15 | test('getItemName', () => { 16 | expect(() => { 17 | Command.getItemName({ id: 'Item' }); 18 | }).toThrow(); 19 | const device = { 20 | customData: { 21 | members: { 22 | thermostatTemperatureSetpointHigh: 'SetpointItem' 23 | } 24 | } 25 | }; 26 | expect(Command.getItemName(device)).toBe('SetpointItem'); 27 | }); 28 | 29 | test('convertParamsToValue', () => { 30 | const item = { 31 | metadata: { 32 | ga: { 33 | config: { 34 | useFahrenheit: true 35 | } 36 | } 37 | } 38 | }; 39 | expect(Command.convertParamsToValue(params, item)).toBe('68'); 40 | expect(Command.convertParamsToValue(params, {})).toBe('20'); 41 | }); 42 | 43 | test('getResponseStates', () => { 44 | const item = { 45 | members: [ 46 | { 47 | metadata: { 48 | ga: { 49 | value: 'thermostatTemperatureSetpointHigh' 50 | } 51 | } 52 | } 53 | ] 54 | }; 55 | expect(Command.getResponseStates(params, item)).toStrictEqual({ thermostatTemperatureSetpointHigh: 20 }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/commands/thermostattemperaturesetpointlow.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/thermostattemperaturesetpointlow.js'); 2 | 3 | describe('ThermostatTemperatureSetpointLow Command', () => { 4 | const params = { thermostatTemperatureSetpointLow: 20 }; 5 | 6 | test('validateParams', () => { 7 | expect(Command.validateParams({})).toBe(false); 8 | expect(Command.validateParams(params)).toBe(true); 9 | }); 10 | 11 | test('requiresItem', () => { 12 | expect(Command.requiresItem()).toBe(true); 13 | }); 14 | 15 | test('getItemName', () => { 16 | expect(() => { 17 | Command.getItemName({ id: 'Item' }); 18 | }).toThrow(); 19 | const device = { 20 | customData: { 21 | members: { 22 | thermostatTemperatureSetpointLow: 'SetpointItem' 23 | } 24 | } 25 | }; 26 | expect(Command.getItemName(device)).toBe('SetpointItem'); 27 | }); 28 | 29 | test('convertParamsToValue', () => { 30 | const item = { 31 | metadata: { 32 | ga: { 33 | config: { 34 | useFahrenheit: true 35 | } 36 | } 37 | } 38 | }; 39 | expect(Command.convertParamsToValue(params, item)).toBe('68'); 40 | expect(Command.convertParamsToValue(params, {})).toBe('20'); 41 | }); 42 | 43 | test('getResponseStates', () => { 44 | const item = { 45 | members: [ 46 | { 47 | metadata: { 48 | ga: { 49 | value: 'thermostatTemperatureSetpointLow' 50 | } 51 | } 52 | } 53 | ] 54 | }; 55 | expect(Command.getResponseStates(params, item)).toStrictEqual({ thermostatTemperatureSetpointLow: 20 }); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/commands/volumerelative.test.js: -------------------------------------------------------------------------------- 1 | const Command = require('../../functions/commands/volumerelative.js'); 2 | 3 | describe('volumeRelative Command', () => { 4 | const params = { relativeSteps: 10 }; 5 | 6 | test('validateParams', () => { 7 | expect(Command.validateParams({})).toBe(false); 8 | expect(Command.validateParams(params)).toBe(true); 9 | }); 10 | 11 | test('requiresItem', () => { 12 | expect(Command.requiresItem()).toBe(true); 13 | }); 14 | 15 | describe('getItemName', () => { 16 | test('getItemName', () => { 17 | expect(Command.getItemName({ id: 'Item' })).toBe('Item'); 18 | expect(Command.getItemName({ id: 'Item', customData: {} })).toBe('Item'); 19 | }); 20 | 21 | test('getItemName TV', () => { 22 | expect(() => { 23 | Command.getItemName({ id: 'Item', customData: { deviceType: 'TV' } }); 24 | }).toThrow(); 25 | const device = { 26 | customData: { 27 | deviceType: 'TV', 28 | members: { 29 | tvVolume: 'VolumeItem' 30 | } 31 | } 32 | }; 33 | expect(Command.getItemName(device)).toBe('VolumeItem'); 34 | }); 35 | }); 36 | 37 | describe('convertParamsToValue', () => { 38 | test('convertParamsToValue', () => { 39 | expect(Command.convertParamsToValue(params, { state: 20 }, {})).toBe('30'); 40 | expect(Command.convertParamsToValue({ relativeSteps: 90 }, { state: 20 }, {})).toBe('100'); 41 | expect(Command.convertParamsToValue({ relativeSteps: -30 }, { state: 20 }, {})).toBe('0'); 42 | }); 43 | 44 | test('convertParamsToValue TV', () => { 45 | const item = { 46 | members: [ 47 | { 48 | state: '20', 49 | type: 'Number', 50 | metadata: { 51 | ga: { 52 | value: 'tvVolume' 53 | } 54 | } 55 | } 56 | ] 57 | }; 58 | expect(Command.convertParamsToValue(params, item, { customData: { deviceType: 'TV' } })).toBe('30'); 59 | expect(() => { 60 | Command.convertParamsToValue(params, {}, { customData: { deviceType: 'TV' } }); 61 | }).toThrow(); 62 | }); 63 | }); 64 | 65 | test('getResponseStates', () => { 66 | expect(Command.getResponseStates(params, { state: 20 }, {})).toStrictEqual({ currentVolume: 30 }); 67 | }); 68 | 69 | test('checkCurrentState', () => { 70 | expect.assertions(6); 71 | 72 | expect(Command.checkCurrentState('100', '10')).toBeUndefined(); 73 | try { 74 | Command.checkCurrentState('100', '100'); 75 | } catch (e) { 76 | expect(e.errorCode).toBe('volumeAlreadyMax'); 77 | } 78 | 79 | expect(Command.checkCurrentState('0', '10')).toBeUndefined(); 80 | try { 81 | Command.checkCurrentState('0', '0'); 82 | } catch (e) { 83 | expect(e.errorCode).toBe('volumeAlreadyMin'); 84 | } 85 | 86 | expect(Command.checkCurrentState('20', '40')).toBeUndefined(); 87 | try { 88 | Command.checkCurrentState('20', '20'); 89 | } catch (e) { 90 | expect(e.errorCode).toBe('alreadyInState'); 91 | } 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /tests/config.test.js: -------------------------------------------------------------------------------- 1 | const Config = require('../functions/config.js'); 2 | 3 | describe('Config', () => { 4 | test('All properties available', () => { 5 | expect(Object.keys(Config)).toStrictEqual(['host', 'port', 'path']); 6 | }); 7 | 8 | test('Properties used from ENV', () => { 9 | expect(Config.host).toBe('test.host'); 10 | expect(Config.port).toBe(1234); 11 | expect(Config.path).toBe('/test/items'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/devices/camera.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/camera.js'); 2 | 3 | describe('Camera Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'CAMERA' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'String' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'Number' })).toBe(false); 19 | expect(Device.matchesItemType({ type: 'Group', groupType: 'String' })).toBe(true); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Number' })).toBe(false); 21 | }); 22 | 23 | describe('getAttributes', () => { 24 | test('getAttributes no config', () => { 25 | const item = { 26 | metadata: { 27 | ga: { 28 | config: {} 29 | } 30 | } 31 | }; 32 | expect(Device.getAttributes(item)).toStrictEqual({ 33 | cameraStreamSupportedProtocols: ['hls', 'dash', 'smooth_stream', 'progressive_mp4'], 34 | cameraStreamNeedAuthToken: false, 35 | cameraStreamNeedDrmEncryption: false 36 | }); 37 | }); 38 | 39 | test('getAttributes protocols, token', () => { 40 | const item = { 41 | metadata: { 42 | ga: { 43 | config: { 44 | protocols: 'hls,test', 45 | token: true 46 | } 47 | } 48 | } 49 | }; 50 | expect(Device.getAttributes(item)).toStrictEqual({ 51 | cameraStreamSupportedProtocols: ['hls', 'test'], 52 | cameraStreamNeedAuthToken: true, 53 | cameraStreamNeedDrmEncryption: false 54 | }); 55 | }); 56 | }); 57 | 58 | test('getState', () => { 59 | expect(Device.getState({})).toStrictEqual({}); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /tests/devices/colorlight.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/colorlight.js'); 2 | 3 | describe('ColorLight Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'LIGHT' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'Color' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'Dimmer' })).toBe(false); 19 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Color' })).toBe(true); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Dimmer' })).toBe(false); 21 | }); 22 | 23 | describe('getAttributes', () => { 24 | test('getAttributes colorTemperatureRange', () => { 25 | const item = { 26 | metadata: { 27 | ga: { 28 | config: { 29 | colorTemperatureRange: '1000,2000' 30 | } 31 | } 32 | } 33 | }; 34 | expect(Device.getAttributes(item)).toStrictEqual({ 35 | colorModel: 'hsv', 36 | colorTemperatureRange: { 37 | temperatureMinK: 1000, 38 | temperatureMaxK: 2000 39 | } 40 | }); 41 | }); 42 | 43 | test('getAttributes invalid colorTemperatureRange', () => { 44 | const item = { 45 | metadata: { 46 | ga: { 47 | config: { 48 | colorTemperatureRange: 'a,b' 49 | } 50 | } 51 | } 52 | }; 53 | expect(Device.getAttributes(item)).toStrictEqual({ 54 | colorModel: 'hsv' 55 | }); 56 | }); 57 | }); 58 | 59 | test('getState', () => { 60 | expect(Device.getState({ state: '100,50,10' })).toStrictEqual({ 61 | on: true, 62 | brightness: 10, 63 | color: { 64 | spectrumHSV: { 65 | hue: 100, 66 | saturation: 0.5, 67 | value: 0.1 68 | } 69 | } 70 | }); 71 | }); 72 | }); 73 | -------------------------------------------------------------------------------- /tests/devices/default.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/default.js'); 2 | const packageVersion = require('../../functions/package.json').version; 3 | 4 | describe('Default Device', () => { 5 | const item = { 6 | type: 'Number', 7 | state: '50', 8 | name: 'DefaultDevice', 9 | label: 'Default Device', 10 | metadata: { 11 | ga: { 12 | value: '', 13 | config: { 14 | ackNeeded: true, 15 | checkState: true, 16 | inverted: true, 17 | pinNeeded: '1234', 18 | pinOnDisarmOnly: true, 19 | waitForStateChange: '5' 20 | } 21 | }, 22 | synonyms: { 23 | value: 'Standard Device' 24 | } 25 | } 26 | }; 27 | 28 | test('matchesItemType', () => { 29 | expect(Device.matchesItemType({ type: 'Number' })).toBe(true); 30 | }); 31 | 32 | test('getConfig', () => { 33 | expect(Device.getConfig(item)).toStrictEqual({ 34 | ackNeeded: true, 35 | checkState: true, 36 | inverted: true, 37 | pinNeeded: '1234', 38 | pinOnDisarmOnly: true, 39 | waitForStateChange: '5' 40 | }); 41 | }); 42 | 43 | test('getState', () => { 44 | expect(Device.getState(item)).toStrictEqual({}); 45 | }); 46 | 47 | test('getMetadata', () => { 48 | expect(Device.getMetadata(item)).toStrictEqual({ 49 | attributes: {}, 50 | customData: { 51 | ackNeeded: true, 52 | checkState: true, 53 | deviceType: 'DefaultDevice', 54 | inverted: true, 55 | itemType: 'Number', 56 | pinNeeded: '1234', 57 | pinOnDisarmOnly: true, 58 | waitForStateChange: 5 59 | }, 60 | deviceInfo: { 61 | manufacturer: 'openHAB', 62 | model: 'Number:DefaultDevice', 63 | hwVersion: '3.0.0', 64 | swVersion: packageVersion 65 | }, 66 | id: 'DefaultDevice', 67 | name: { 68 | defaultNames: ['Default Device'], 69 | name: 'Default Device', 70 | nicknames: ['Default Device', 'Standard Device'] 71 | }, 72 | roomHint: undefined, 73 | structureHint: undefined, 74 | traits: [], 75 | type: '', 76 | willReportState: false 77 | }); 78 | }); 79 | 80 | test('getMetadata legacy', () => { 81 | const metadata = Device.getMetadata({ 82 | metadata: { 83 | ga: { 84 | config: { 85 | tfaAck: true, 86 | tfaPin: '1234' 87 | } 88 | } 89 | } 90 | }); 91 | expect(metadata.customData.ackNeeded).toBe(true); 92 | expect(metadata.customData.pinNeeded).toBe('1234'); 93 | }); 94 | 95 | test('getMetadata no label fallback', () => { 96 | const metadata = Device.getMetadata({ 97 | type: 'Number', 98 | state: '50', 99 | name: 'DefaultDevice' 100 | }); 101 | expect(metadata.name.name).toBe('DefaultDevice'); 102 | }); 103 | }); 104 | -------------------------------------------------------------------------------- /tests/devices/dimmablelight.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/dimmablelight.js'); 2 | 3 | describe('DimmableLight Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'LIGHT' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'Dimmer' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'String' })).toBe(false); 19 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Dimmer' })).toBe(true); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'String' })).toBe(false); 21 | }); 22 | 23 | test('getState', () => { 24 | expect(Device.getState({ state: '50 %' })).toStrictEqual({ 25 | on: true, 26 | brightness: 50 27 | }); 28 | expect(Device.getState({ state: '12.56754' })).toStrictEqual({ 29 | on: true, 30 | brightness: 13 31 | }); 32 | expect(Device.getState({ state: 'NULL' })).toStrictEqual({ 33 | on: false, 34 | brightness: 0 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/devices/dynamicmodesdevice.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/dynamicmodesdevice.js'); 2 | 3 | describe('DynamicModesDevice Device', () => { 4 | const item = { 5 | type: 'Group', 6 | metadata: { 7 | ga: { 8 | config: { 9 | mode: 'mode_name,alternate_mode_name', 10 | ordered: true 11 | } 12 | } 13 | }, 14 | members: [ 15 | { 16 | name: 'CurrentMode', 17 | state: 'mode_value', 18 | type: 'String', 19 | metadata: { 20 | ga: { 21 | value: 'modesCurrentMode' 22 | } 23 | } 24 | }, 25 | { 26 | name: 'Settings', 27 | state: 'setting1=mode_value:alternate_mode_value,setting2=mode_value2', 28 | type: 'String', 29 | metadata: { 30 | ga: { 31 | value: 'modesSettings' 32 | } 33 | } 34 | } 35 | ] 36 | }; 37 | 38 | test('matchesItemType', () => { 39 | expect(Device.matchesItemType({ type: 'Color' })).toBe(false); 40 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Color' })).toBe(false); 41 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Dimmer' })).toBe(false); 42 | }); 43 | 44 | describe('getAttributes', () => { 45 | test('getAttributes no config', () => { 46 | const invalid_item = { 47 | metadata: { 48 | ga: { 49 | config: {} 50 | } 51 | } 52 | }; 53 | expect(Device.getAttributes(invalid_item)).toStrictEqual({}); 54 | }); 55 | 56 | test('getAttributes mode', () => { 57 | expect(Device.getAttributes(item)).toStrictEqual({ 58 | availableModes: [ 59 | { 60 | name: 'mode_name', 61 | name_values: [ 62 | { 63 | lang: 'en', 64 | name_synonym: ['mode_name', 'alternate_mode_name'] 65 | } 66 | ], 67 | ordered: true, 68 | settings: [ 69 | { 70 | setting_name: 'setting1', 71 | setting_values: [ 72 | { 73 | lang: 'en', 74 | setting_synonym: ['setting1', 'mode_value', 'alternate_mode_value'] 75 | } 76 | ] 77 | }, 78 | { 79 | setting_name: 'setting2', 80 | setting_values: [ 81 | { 82 | lang: 'en', 83 | setting_synonym: ['setting2', 'mode_value2'] 84 | } 85 | ] 86 | } 87 | ] 88 | } 89 | ] 90 | }); 91 | }); 92 | }); 93 | 94 | test('getState', () => { 95 | expect(Device.getState(item)).toStrictEqual({ 96 | currentModeSettings: { 97 | mode_name: 'mode_value' 98 | } 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /tests/devices/dynamicmodeslight.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/dynamicmodeslight.js'); 2 | 3 | describe('DynamicModesLight Device', () => { 4 | const item = { 5 | type: 'Group', 6 | metadata: { 7 | ga: { 8 | value: 'light', 9 | config: { 10 | mode: 'mode_name,alternate_mode_name', 11 | ordered: true 12 | } 13 | } 14 | }, 15 | members: [ 16 | { 17 | name: 'CurrentMode', 18 | state: 'mode_value', 19 | type: 'String', 20 | metadata: { 21 | ga: { 22 | value: 'modesCurrentMode' 23 | } 24 | } 25 | }, 26 | { 27 | name: 'Settings', 28 | state: 'setting1=mode_value:alternate_mode_value,setting2=mode_value2', 29 | type: 'String', 30 | metadata: { 31 | ga: { 32 | value: 'modesSettings' 33 | } 34 | } 35 | } 36 | ] 37 | }; 38 | 39 | test('matchesDeviceType', () => { 40 | expect(Device.matchesDeviceType(item)).toBe(true); 41 | expect(Device.matchesDeviceType({ metadata: { ga: { value: 'test' } } })).toBe(false); 42 | expect(Device.matchesDeviceType({ metadata: { ga: { value: 'light' } } })).toBe(false); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/devices/humiditysensor.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/humiditysensor.js'); 2 | 3 | describe('HumiditySensor Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'humiditysensor' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'Number' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'Number:Humidity' })).toBe(true); 19 | expect(Device.matchesItemType({ type: 'Dimmer' })).toBe(false); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Dimmer' })).toBe(false); 21 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Number' })).toBe(true); 22 | }); 23 | 24 | describe('getAttributes', () => { 25 | test('getAttributes no config', () => { 26 | expect(Device.getAttributes()).toStrictEqual({ 27 | queryOnlyHumiditySetting: true 28 | }); 29 | }); 30 | }); 31 | 32 | describe('getAttributes', () => { 33 | test('getState no config', () => { 34 | expect(Device.getState({ state: '9.6 %' })).toStrictEqual({ 35 | humidityAmbientPercent: 10, 36 | humiditySetpointPercent: 10 37 | }); 38 | }); 39 | 40 | test('getState maxHumidity', () => { 41 | expect(Device.getState({ state: '0.34', metadata: { ga: { config: { maxHumidity: 1 } } } })).toStrictEqual({ 42 | humidityAmbientPercent: 34, 43 | humiditySetpointPercent: 34 44 | }); 45 | }); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/devices/index.test.js: -------------------------------------------------------------------------------- 1 | const getDeviceForItem = require('../../functions/devices/index.js').getDeviceForItem; 2 | 3 | describe('Devices Index', () => { 4 | test('getDeviceForItem', () => { 5 | const device = getDeviceForItem({ type: 'Switch', metadata: { ga: { value: 'Switch' } } }); 6 | expect(device).not.toBeUndefined(); 7 | expect(device.name).toBe('Switch'); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /tests/devices/lock.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/lock.js'); 2 | 3 | describe('Lock Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'LOCK' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'Switch' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'Contact' })).toBe(true); 19 | expect(Device.matchesItemType({ type: 'String' })).toBe(false); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Switch' })).toBe(true); 21 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Contact' })).toBe(true); 22 | expect(Device.matchesItemType({ type: 'Group', groupType: 'String' })).toBe(false); 23 | }); 24 | 25 | describe('getState', () => { 26 | test('getState Switch', () => { 27 | expect(Device.getState({ state: 'ON' })).toStrictEqual({ 28 | isLocked: true 29 | }); 30 | expect(Device.getState({ state: 'OFF' })).toStrictEqual({ 31 | isLocked: false 32 | }); 33 | }); 34 | 35 | test('getState Contact', () => { 36 | expect(Device.getState({ state: 'CLOSED' })).toStrictEqual({ 37 | isLocked: true 38 | }); 39 | expect(Device.getState({ state: 'OPEN' })).toStrictEqual({ 40 | isLocked: false 41 | }); 42 | }); 43 | 44 | test('getState inverted Swtich', () => { 45 | const item1 = { 46 | state: 'ON', 47 | metadata: { 48 | ga: { 49 | config: { 50 | inverted: true 51 | } 52 | } 53 | } 54 | }; 55 | expect(Device.getState(item1)).toStrictEqual({ 56 | isLocked: false 57 | }); 58 | }); 59 | 60 | test('getState inverted Contact', () => { 61 | const item2 = { 62 | state: 'OPEN', 63 | metadata: { 64 | ga: { 65 | config: { 66 | inverted: true 67 | } 68 | } 69 | } 70 | }; 71 | expect(Device.getState(item2)).toStrictEqual({ 72 | isLocked: true 73 | }); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /tests/devices/modesdevice.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/modesdevice.js'); 2 | 3 | describe('ModesDevice Device', () => { 4 | test('matchesItemType', () => { 5 | expect(Device.matchesItemType({ type: 'Group' })).toBe(false); 6 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Number' })).toBe(true); 7 | expect(Device.matchesItemType({ type: 'String' })).toBe(true); 8 | }); 9 | 10 | describe('getAttributes', () => { 11 | test('getAttributes no config', () => { 12 | const item = { 13 | metadata: { 14 | ga: { 15 | config: {} 16 | } 17 | } 18 | }; 19 | expect(Device.getAttributes(item)).toStrictEqual({}); 20 | }); 21 | 22 | test('getAttributes mode', () => { 23 | const item = { 24 | metadata: { 25 | ga: { 26 | config: { 27 | mode: 'mode_name,alternate_mode_name', 28 | settings: 'setting1=mode_value:alternate_mode_value,setting2=mode_value2', 29 | ordered: true 30 | } 31 | } 32 | } 33 | }; 34 | expect(Device.getAttributes(item)).toStrictEqual({ 35 | availableModes: [ 36 | { 37 | name: 'mode_name', 38 | name_values: [ 39 | { 40 | lang: 'en', 41 | name_synonym: ['mode_name', 'alternate_mode_name'] 42 | } 43 | ], 44 | ordered: true, 45 | settings: [ 46 | { 47 | setting_name: 'setting1', 48 | setting_values: [ 49 | { 50 | lang: 'en', 51 | setting_synonym: ['setting1', 'mode_value', 'alternate_mode_value'] 52 | } 53 | ] 54 | }, 55 | { 56 | setting_name: 'setting2', 57 | setting_values: [ 58 | { 59 | lang: 'en', 60 | setting_synonym: ['setting2', 'mode_value2'] 61 | } 62 | ] 63 | } 64 | ] 65 | } 66 | ] 67 | }); 68 | }); 69 | }); 70 | 71 | test('getState', () => { 72 | expect(Device.getState({ state: 'mode_value' })).toStrictEqual({}); 73 | expect( 74 | Device.getState({ 75 | state: 'mode_value', 76 | metadata: { 77 | ga: { 78 | config: { 79 | mode: 'mode_name,alternate_mode_name', 80 | settings: 'setting1=mode_value:alternate_mode_value,setting2=mode_value2' 81 | } 82 | } 83 | } 84 | }) 85 | ).toStrictEqual({ 86 | currentModeSettings: { 87 | mode_name: 'mode_value' 88 | } 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /tests/devices/modeslight.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/modeslight.js'); 2 | 3 | describe('ModesLight Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect(Device.matchesDeviceType({ type: 'Group' })).toBe(false); 6 | expect( 7 | Device.matchesDeviceType({ 8 | type: 'Switch', 9 | metadata: { 10 | ga: { 11 | value: 'light', 12 | config: {} 13 | } 14 | } 15 | }) 16 | ).toBe(false); 17 | expect( 18 | Device.matchesDeviceType({ 19 | type: 'Switch', 20 | metadata: { 21 | ga: { 22 | value: 'light', 23 | config: { 24 | mode: 'testMode', 25 | settings: '1=test' 26 | } 27 | } 28 | } 29 | }) 30 | ).toBe(true); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/devices/openclosedevice.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/openclosedevice.js'); 2 | 3 | describe('OpenCloseDevice Device', () => { 4 | test('getAttributes', () => { 5 | expect(Device.getAttributes({ type: 'Rollershutter' })).toStrictEqual({ 6 | pausable: false, 7 | discreteOnlyOpenClose: false, 8 | queryOnlyOpenClose: false 9 | }); 10 | expect( 11 | Device.getAttributes({ 12 | type: 'Rollershutter', 13 | metadata: { 14 | ga: { 15 | config: { 16 | discreteOnly: true, 17 | queryOnly: true 18 | } 19 | } 20 | } 21 | }) 22 | ).toStrictEqual({ 23 | pausable: false, 24 | discreteOnlyOpenClose: true, 25 | queryOnlyOpenClose: true 26 | }); 27 | expect(Device.getAttributes({ type: 'Switch' })).toStrictEqual({ 28 | pausable: false, 29 | discreteOnlyOpenClose: true, 30 | queryOnlyOpenClose: false 31 | }); 32 | expect(Device.getAttributes({ type: 'Contact' })).toStrictEqual({ 33 | pausable: false, 34 | discreteOnlyOpenClose: true, 35 | queryOnlyOpenClose: true 36 | }); 37 | 38 | expect(Device.getAttributes({ type: 'Group' })).toStrictEqual({ 39 | pausable: false, 40 | discreteOnlyOpenClose: false, 41 | queryOnlyOpenClose: false 42 | }); 43 | expect(Device.getAttributes({ type: 'Group', groupType: 'Switch' })).toStrictEqual({ 44 | pausable: false, 45 | discreteOnlyOpenClose: true, 46 | queryOnlyOpenClose: false 47 | }); 48 | expect(Device.getAttributes({ type: 'Group', groupType: 'Contact' })).toStrictEqual({ 49 | pausable: false, 50 | discreteOnlyOpenClose: true, 51 | queryOnlyOpenClose: true 52 | }); 53 | }); 54 | 55 | describe('getState', () => { 56 | test('getState Contact', () => { 57 | const item = { 58 | type: 'Contact', 59 | state: 'OPEN' 60 | }; 61 | expect(Device.getState(item)).toStrictEqual({ 62 | openPercent: 100 63 | }); 64 | item.state = 'CLOSED'; 65 | expect(Device.getState(item)).toStrictEqual({ 66 | openPercent: 0 67 | }); 68 | }); 69 | 70 | test('getState Switch', () => { 71 | const item = { 72 | type: 'Switch', 73 | state: 'ON' 74 | }; 75 | expect(Device.getState(item)).toStrictEqual({ 76 | openPercent: 100 77 | }); 78 | item.state = 'OFF'; 79 | expect(Device.getState(item)).toStrictEqual({ 80 | openPercent: 0 81 | }); 82 | }); 83 | 84 | test('getState Rollershutter', () => { 85 | const item = { 86 | type: 'Rollershutter', 87 | state: '25 %' 88 | }; 89 | expect(Device.getState(item)).toStrictEqual({ 90 | openPercent: 75 91 | }); 92 | }); 93 | 94 | test('getState Group Rollershutter', () => { 95 | const item = { 96 | type: 'Group', 97 | groupType: 'Rollershutter', 98 | state: '25' 99 | }; 100 | expect(Device.getState(item)).toStrictEqual({ 101 | openPercent: 75 102 | }); 103 | }); 104 | 105 | test('getState inverted Contact', () => { 106 | const item = { 107 | type: 'Contact', 108 | state: 'CLOSED', 109 | metadata: { 110 | ga: { 111 | config: { 112 | inverted: true 113 | } 114 | } 115 | } 116 | }; 117 | expect(Device.getState(item)).toStrictEqual({ 118 | openPercent: 100 119 | }); 120 | }); 121 | 122 | test('getState inverted Switch', () => { 123 | const item = { 124 | type: 'Switch', 125 | state: 'ON', 126 | metadata: { 127 | ga: { 128 | config: { 129 | inverted: true 130 | } 131 | } 132 | } 133 | }; 134 | expect(Device.getState(item)).toStrictEqual({ 135 | openPercent: 0 136 | }); 137 | }); 138 | 139 | test('getState inverted Rollershutter', () => { 140 | const item = { 141 | type: 'Rollershutter', 142 | state: '25', 143 | metadata: { 144 | ga: { 145 | config: { 146 | inverted: true 147 | } 148 | } 149 | } 150 | }; 151 | expect(Device.getState(item)).toStrictEqual({ 152 | openPercent: 25 153 | }); 154 | }); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /tests/devices/scene.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/scene.js'); 2 | 3 | describe('Scene Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'SCENE' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'Switch' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'String' })).toBe(false); 19 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Switch' })).toBe(true); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'String' })).toBe(false); 21 | }); 22 | 23 | describe('getAttributes', () => { 24 | test('getAttributes no config', () => { 25 | expect(Device.getAttributes()).toStrictEqual({ 26 | sceneReversible: true 27 | }); 28 | }); 29 | 30 | test('getAttributes with sceneReversible = true', () => { 31 | const item = { 32 | metadata: { 33 | ga: { 34 | config: { 35 | sceneReversible: true 36 | } 37 | } 38 | } 39 | }; 40 | expect(Device.getAttributes(item)).toStrictEqual({ 41 | sceneReversible: true 42 | }); 43 | }); 44 | 45 | test('getAttributes with sceneReversible = false', () => { 46 | const item = { 47 | metadata: { 48 | ga: { 49 | config: { 50 | sceneReversible: false 51 | } 52 | } 53 | } 54 | }; 55 | expect(Device.getAttributes(item)).toStrictEqual({ 56 | sceneReversible: false 57 | }); 58 | }); 59 | }); 60 | 61 | test('getState', () => { 62 | expect(Device.getState({})).toStrictEqual({}); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /tests/devices/sensor.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/sensor.js'); 2 | 3 | describe('Sensor Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'SENSOR' 10 | } 11 | } 12 | }) 13 | ).toBe(false); 14 | expect( 15 | Device.matchesDeviceType({ 16 | metadata: { 17 | ga: { 18 | value: 'SENSOR', 19 | config: { 20 | sensorName: 'Sensor', 21 | valueUnit: 'PERCENT' 22 | } 23 | } 24 | } 25 | }) 26 | ).toBe(true); 27 | expect( 28 | Device.matchesDeviceType({ 29 | metadata: { 30 | ga: { 31 | value: 'SENSOR', 32 | config: { 33 | sensorName: 'Sensor', 34 | states: '50=good,100=bad' 35 | } 36 | } 37 | } 38 | }) 39 | ).toBe(true); 40 | }); 41 | 42 | test('matchesItemType', () => { 43 | expect(Device.matchesItemType({ type: 'Group' })).toBe(false); 44 | expect(Device.matchesItemType({ type: 'Group', groupType: 'String' })).toBe(true); 45 | expect(Device.matchesItemType({ type: 'Number' })).toBe(true); 46 | }); 47 | 48 | describe('getAttributes', () => { 49 | test('getAttributes no config', () => { 50 | const item = { 51 | metadata: { 52 | ga: { 53 | config: {} 54 | } 55 | } 56 | }; 57 | expect(Device.getAttributes(item)).toStrictEqual({}); 58 | }); 59 | 60 | test('getAttributes states', () => { 61 | const item = { 62 | metadata: { 63 | ga: { 64 | config: { 65 | sensorName: 'Sensor', 66 | valueUnit: 'AQI', 67 | states: 'good=10,moderate=50,poor=90' 68 | } 69 | } 70 | } 71 | }; 72 | expect(Device.getAttributes(item)).toStrictEqual({ 73 | sensorStatesSupported: [ 74 | { 75 | descriptiveCapabilities: { 76 | availableStates: ['good', 'moderate', 'poor'] 77 | }, 78 | name: 'Sensor', 79 | numericCapabilities: { 80 | rawValueUnit: 'AQI' 81 | } 82 | } 83 | ] 84 | }); 85 | }); 86 | }); 87 | 88 | describe('getState', () => { 89 | test('getState', () => { 90 | const item = { 91 | metadata: { 92 | ga: { 93 | config: { 94 | sensorName: 'Sensor', 95 | valueUnit: 'AQI', 96 | states: 'good=10,moderate=50,poor=90' 97 | } 98 | } 99 | }, 100 | state: '10 ppm' 101 | }; 102 | expect(Device.getState(item)).toStrictEqual({ 103 | currentSensorStateData: [ 104 | { 105 | currentSensorState: 'good', 106 | name: 'Sensor', 107 | rawValue: 10 108 | } 109 | ] 110 | }); 111 | }); 112 | 113 | test('getState with string state', () => { 114 | const item = { 115 | metadata: { 116 | ga: { 117 | config: { 118 | sensorName: 'Sensor', 119 | valueUnit: 'AQI', 120 | states: 'good=good,moderate=moderate,poor=poor' 121 | } 122 | } 123 | }, 124 | state: 'moderate' 125 | }; 126 | expect(Device.getState(item)).toStrictEqual({ 127 | currentSensorStateData: [ 128 | { 129 | currentSensorState: 'moderate', 130 | name: 'Sensor' 131 | } 132 | ] 133 | }); 134 | }); 135 | 136 | test('getState no matching state', () => { 137 | const item = { 138 | metadata: { 139 | ga: { 140 | config: { 141 | sensorName: 'Sensor', 142 | valueUnit: 'AQI', 143 | states: 'good=10,moderate=50,poor=90' 144 | } 145 | } 146 | }, 147 | state: '20' 148 | }; 149 | expect(Device.getState(item)).toStrictEqual({ 150 | currentSensorStateData: [ 151 | { 152 | currentSensorState: '', 153 | name: 'Sensor', 154 | rawValue: 20 155 | } 156 | ] 157 | }); 158 | }); 159 | }); 160 | }); 161 | -------------------------------------------------------------------------------- /tests/devices/simplelight.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/simplelight.js'); 2 | 3 | describe('SimpleLight Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'LIGHT' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/devices/simplesecuritysystem.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/simplesecuritysystem.js'); 2 | 3 | describe('SimpleSecuritySystem Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'SECURITYSYSTEM' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'Switch' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'String' })).toBe(false); 19 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Switch' })).toBe(true); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'String' })).toBe(false); 21 | }); 22 | 23 | describe('getState', () => { 24 | test('getState', () => { 25 | expect(Device.getState({ state: 'ON' })).toStrictEqual({ 26 | isArmed: true 27 | }); 28 | expect(Device.getState({ state: 'OFF' })).toStrictEqual({ 29 | isArmed: false 30 | }); 31 | }); 32 | 33 | test('getState inverted', () => { 34 | const item = { 35 | state: 'ON', 36 | metadata: { 37 | ga: { 38 | config: { 39 | inverted: true 40 | } 41 | } 42 | } 43 | }; 44 | expect(Device.getState(item)).toStrictEqual({ 45 | isArmed: false 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /tests/devices/speaker.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/speaker.js'); 2 | 3 | describe('Speaker Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'SPEAKER' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'Dimmer' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'Number' })).toBe(false); 19 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Dimmer' })).toBe(true); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Number' })).toBe(false); 21 | }); 22 | 23 | describe('getAttributes', () => { 24 | test('getAttributes no config', () => { 25 | const item = { 26 | metadata: { 27 | ga: { 28 | config: {} 29 | } 30 | } 31 | }; 32 | expect(Device.getAttributes(item)).toStrictEqual({ 33 | volumeCanMuteAndUnmute: false, 34 | volumeMaxLevel: 100 35 | }); 36 | }); 37 | 38 | test('getAttributes volumeDefaultPercentage, volumeMaxLevel, levelStepSize', () => { 39 | const item = { 40 | metadata: { 41 | ga: { 42 | config: { 43 | volumeDefaultPercentage: '20', 44 | volumeMaxLevel: '90', 45 | levelStepSize: '10' 46 | } 47 | } 48 | } 49 | }; 50 | expect(Device.getAttributes(item)).toStrictEqual({ 51 | volumeCanMuteAndUnmute: false, 52 | volumeMaxLevel: 90, 53 | volumeDefaultPercentage: 20, 54 | levelStepSize: 10 55 | }); 56 | }); 57 | }); 58 | 59 | test('getState', () => { 60 | expect(Device.getState({ state: '10 %' })).toStrictEqual({ 61 | currentVolume: 10 62 | }); 63 | expect(Device.getState({ state: '90' })).toStrictEqual({ 64 | currentVolume: 90 65 | }); 66 | expect(Device.getState({ state: '23.1234' })).toStrictEqual({ 67 | currentVolume: 23 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /tests/devices/startstopswitch.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/startstopswitch.js'); 2 | 3 | describe('StartStopSwitch Device', () => { 4 | test('matchesItemType', () => { 5 | expect(Device.matchesItemType({ type: 'Switch' })).toBe(true); 6 | expect(Device.matchesItemType({ type: 'Dimmer' })).toBe(false); 7 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Switch' })).toBe(true); 8 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Dimmer' })).toBe(false); 9 | }); 10 | 11 | test('getAttributes', () => { 12 | expect(Device.getAttributes()).toStrictEqual({ pausable: false }); 13 | }); 14 | 15 | describe('getState', () => { 16 | test('getState', () => { 17 | expect(Device.getState({ state: 'ON' })).toStrictEqual({ 18 | isRunning: true 19 | }); 20 | expect(Device.getState({ state: 'OFF' })).toStrictEqual({ 21 | isRunning: false 22 | }); 23 | }); 24 | 25 | test('getState inverted', () => { 26 | const item = { 27 | state: 'ON', 28 | metadata: { 29 | ga: { 30 | config: { 31 | inverted: true 32 | } 33 | } 34 | } 35 | }; 36 | expect(Device.getState(item)).toStrictEqual({ 37 | isRunning: false 38 | }); 39 | item.state = 'OFF'; 40 | expect(Device.getState(item)).toStrictEqual({ 41 | isRunning: true 42 | }); 43 | }); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /tests/devices/switch.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/switch.js'); 2 | 3 | describe('Switch Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'SWITCH' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'Switch' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'String' })).toBe(false); 19 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Switch' })).toBe(true); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'String' })).toBe(false); 21 | }); 22 | 23 | test('getState', () => { 24 | expect(Device.getState({ state: 'ON' })).toStrictEqual({ 25 | on: true 26 | }); 27 | expect(Device.getState({ state: 'OFF' })).toStrictEqual({ 28 | on: false 29 | }); 30 | }); 31 | 32 | test('getState inverted', () => { 33 | const item = { 34 | state: 'ON', 35 | metadata: { 36 | ga: { 37 | config: { 38 | inverted: true 39 | } 40 | } 41 | } 42 | }; 43 | expect(Device.getState(item)).toStrictEqual({ 44 | on: false 45 | }); 46 | item.state = 'OFF'; 47 | expect(Device.getState(item)).toStrictEqual({ 48 | on: true 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /tests/devices/temperaturesensor.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/temperaturesensor.js'); 2 | 3 | describe('TemperatureSensor Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'temperaturesensor' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'Number' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'Number:Temperature' })).toBe(true); 19 | expect(Device.matchesItemType({ type: 'Dimmer' })).toBe(false); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Dimmer' })).toBe(false); 21 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Number' })).toBe(true); 22 | }); 23 | 24 | describe('getAttributes', () => { 25 | test('getAttributes no config', () => { 26 | const item = { 27 | metadata: { 28 | ga: { 29 | config: {} 30 | } 31 | } 32 | }; 33 | expect(Device.getAttributes(item)).toStrictEqual({ 34 | queryOnlyTemperatureControl: true, 35 | queryOnlyTemperatureSetting: true, 36 | temperatureUnitForUX: 'C', 37 | thermostatTemperatureUnit: 'C', 38 | temperatureRange: { 39 | maxThresholdCelsius: 100, 40 | minThresholdCelsius: -100 41 | } 42 | }); 43 | }); 44 | 45 | test('getAttributes useFahrenheit', () => { 46 | const item = { 47 | metadata: { 48 | ga: { 49 | config: { 50 | useFahrenheit: true 51 | } 52 | } 53 | } 54 | }; 55 | expect(Device.getAttributes(item)).toStrictEqual({ 56 | queryOnlyTemperatureControl: true, 57 | queryOnlyTemperatureSetting: true, 58 | temperatureUnitForUX: 'F', 59 | thermostatTemperatureUnit: 'F', 60 | temperatureRange: { 61 | maxThresholdCelsius: 100, 62 | minThresholdCelsius: -100 63 | } 64 | }); 65 | }); 66 | 67 | test('getAttributes temperatureRange', () => { 68 | const item1 = { 69 | metadata: { 70 | ga: { 71 | config: { 72 | temperatureRange: '-20,40' 73 | } 74 | } 75 | } 76 | }; 77 | expect(Device.getAttributes(item1)).toStrictEqual({ 78 | queryOnlyTemperatureControl: true, 79 | queryOnlyTemperatureSetting: true, 80 | temperatureUnitForUX: 'C', 81 | thermostatTemperatureUnit: 'C', 82 | temperatureRange: { 83 | maxThresholdCelsius: 40, 84 | minThresholdCelsius: -20 85 | } 86 | }); 87 | }); 88 | }); 89 | 90 | test('getState', () => { 91 | expect(Device.getState({ state: '10' })).toStrictEqual({ 92 | thermostatTemperatureAmbient: 10, 93 | temperatureAmbientCelsius: 10, 94 | temperatureSetpointCelsius: 10 95 | }); 96 | const item = { 97 | state: '10', 98 | metadata: { 99 | ga: { 100 | config: { 101 | useFahrenheit: true 102 | } 103 | } 104 | } 105 | }; 106 | expect(Device.getState(item)).toStrictEqual({ 107 | thermostatTemperatureAmbient: -12.2, 108 | temperatureAmbientCelsius: -12.2, 109 | temperatureSetpointCelsius: -12.2 110 | }); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /tests/devices/valve.test.js: -------------------------------------------------------------------------------- 1 | const Device = require('../../functions/devices/valve.js'); 2 | 3 | describe('Valve Device', () => { 4 | test('matchesDeviceType', () => { 5 | expect( 6 | Device.matchesDeviceType({ 7 | metadata: { 8 | ga: { 9 | value: 'VALVE' 10 | } 11 | } 12 | }) 13 | ).toBe(true); 14 | }); 15 | 16 | test('matchesItemType', () => { 17 | expect(Device.matchesItemType({ type: 'Switch' })).toBe(true); 18 | expect(Device.matchesItemType({ type: 'String' })).toBe(false); 19 | expect(Device.matchesItemType({ type: 'Group', groupType: 'Switch' })).toBe(true); 20 | expect(Device.matchesItemType({ type: 'Group', groupType: 'String' })).toBe(false); 21 | }); 22 | 23 | describe('getState', () => { 24 | test('getState', () => { 25 | expect(Device.getState({ state: 'ON' })).toStrictEqual({ 26 | openPercent: 100 27 | }); 28 | expect(Device.getState({ state: 'OFF' })).toStrictEqual({ 29 | openPercent: 0 30 | }); 31 | }); 32 | 33 | test('getState inverted', () => { 34 | const item = { 35 | state: 'ON', 36 | metadata: { 37 | ga: { 38 | config: { 39 | inverted: true 40 | } 41 | } 42 | } 43 | }; 44 | expect(Device.getState(item)).toStrictEqual({ 45 | openPercent: 0 46 | }); 47 | item.state = 'OFF'; 48 | expect(Device.getState(item)).toStrictEqual({ 49 | openPercent: 100 50 | }); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /tests/setenv.js: -------------------------------------------------------------------------------- 1 | process.env.OH_HOST = 'test.host'; 2 | process.env.OH_PORT = '1234'; 3 | process.env.OH_PATH = '/test/items'; 4 | -------------------------------------------------------------------------------- /tests/utilities.test.js: -------------------------------------------------------------------------------- 1 | const Utilities = require('../functions/utilities.js'); 2 | 3 | describe('Utilities', () => { 4 | test('convertFahrenheitToCelsius', () => { 5 | expect(Utilities.convertFahrenheitToCelsius(10.0)).toEqual(-12.2); 6 | expect(Utilities.convertFahrenheitToCelsius(0.0)).toEqual(-17.8); 7 | }); 8 | 9 | test('convertCelsiusToFahrenheit', () => { 10 | expect(Utilities.convertCelsiusToFahrenheit(10.0)).toEqual(50); 11 | expect(Utilities.convertCelsiusToFahrenheit(0.0)).toEqual(32); 12 | }); 13 | 14 | test('convertKelvinToRgb', () => { 15 | expect(Utilities.convertKelvinToRgb(2000)).toStrictEqual({ b: 14, g: 137, r: 255 }); 16 | expect(Utilities.convertKelvinToRgb(5900)).toEqual({ b: 234, g: 244, r: 255 }); 17 | expect(Utilities.convertKelvinToRgb(9000)).toEqual({ b: 255, g: 223, r: 210 }); 18 | }); 19 | 20 | test('convertKelvinToMired', () => { 21 | expect(Utilities.convertMired(3400)).toBe(294); 22 | expect(Utilities.convertMired(294)).toBe(3401); 23 | }); 24 | 25 | test('convertRgbToHsv', () => { 26 | expect(Utilities.convertRgbToHsv({ r: 50, g: 10, b: 100 })).toStrictEqual({ 27 | hue: 266.67, 28 | saturation: 0.9, 29 | value: 0.39 30 | }); 31 | expect(Utilities.convertRgbToHsv({ r: 0, g: 0, b: 0 })).toStrictEqual({ hue: 0, saturation: 0, value: 0 }); 32 | expect(Utilities.convertRgbToHsv({ r: 255, g: 0, b: 0 })).toStrictEqual({ hue: 0, saturation: 1, value: 1 }); 33 | expect(Utilities.convertRgbToHsv({ r: 0, g: 255, b: 0 })).toStrictEqual({ hue: 120, saturation: 1, value: 1 }); 34 | expect(Utilities.convertRgbToHsv({ r: 0, g: 0, b: 255 })).toStrictEqual({ hue: 240, saturation: 1, value: 1 }); 35 | }); 36 | }); 37 | --------------------------------------------------------------------------------