├── .gitattributes ├── run.sh ├── run2.sh ├── .github └── workflows │ └── npmpublish.yml ├── .gitignore ├── .vscode └── launch.json ├── test2 ├── toggle.js └── config.json ├── test ├── keep-alive-codec.js ├── empty-codec.js ├── test-codec.js └── config.json ├── package.json ├── wip ├── notes.txt └── accessories.schema.json ├── README.md ├── codecs └── json.js ├── .eslintrc.js ├── LICENSE ├── docs ├── ReleaseNotes.md ├── Configuration.md └── Codecs.md └── libs └── mqttlib.js /.gitattributes: -------------------------------------------------------------------------------- 1 | * text eol=lf 2 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #export DEBUG=* 2 | homebridge -U test -P . 3 | -------------------------------------------------------------------------------- /run2.sh: -------------------------------------------------------------------------------- 1 | #export DEBUG=* 2 | homebridge -U test2 -P . 3 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | registry-url: https://registry.npmjs.org/ 16 | - run: npm ci 17 | - run: npm publish --access public 18 | env: 19 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | npm-debug.log 3 | test/*_persist.json 4 | test/accessories/cachedAccessories 5 | test/persist/AccessoryInfo.*.json 6 | test/persist/IdentifierCache.*.json 7 | test/.uix-dashboard.json 8 | test/.uix-secrets 9 | test/auth.json 10 | test/config.json.* 11 | test2/*_persist.json 12 | test2/accessories/cachedAccessories 13 | test2/persist/AccessoryInfo.*.json 14 | test2/persist/IdentifierCache.*.json 15 | test2/.uix-dashboard.json 16 | test2/.uix-secrets 17 | test2/auth.json 18 | test2/config.json.* 19 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "/usr/bin/homebridge", 15 | "args": [ "-U", "test2", "-P", "." ], 16 | "cwd": "${workspaceFolder}" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /test2/toggle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test 'toggle' codec - toggles switch on receipt of any message 3 | * toggle.js 4 | */ 5 | 6 | 'use strict' 7 | 8 | module.exports = { 9 | init: function() { 10 | let state = false; 11 | return { 12 | properties: { 13 | on: { 14 | decode: function() { 15 | state = ! state; 16 | return state; 17 | }, 18 | encode: function( msg ) { 19 | state = msg; 20 | return msg; 21 | } 22 | } 23 | } 24 | }; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /test/keep-alive-codec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test/Demo Homebridge-MQTTThing Codec (encoder/decoder) 3 | * Codecs allow custom logic to be applied to accessories in mqttthing, rather like apply() functions, 4 | * but in the convenience of a stand-alone JavaScript file. 5 | * 6 | * keep-alive-codec.js - sends keep-alive message at configurable interval 7 | */ 8 | 9 | 'use strict'; 10 | 11 | module.exports = { 12 | init: function( params ) { 13 | let { config, publish } = params; 14 | 15 | // publish keep-alive topic at regular interval 16 | if( config.keepAliveTopic ) { 17 | let keepAlivePeriod = config.keepAlivePeriod || 60; 18 | let keepAliveMessage = config.keepAliveMessage || ''; 19 | 20 | setInterval( () => { 21 | publish( config.keepAliveTopic, keepAliveMessage ); 22 | }, keepAlivePeriod * 1000 ); 23 | } 24 | 25 | // no encode/decode in this codec 26 | return {}; 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "homebridge-mqttthing", 3 | "version": "1.1.22", 4 | "description": "Homebridge plugin supporting various services over MQTT", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "homebridge -P . -U test" 9 | }, 10 | "engines": { 11 | "node": ">4.0.0", 12 | "homebridge": ">=0.2.0" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/arachnetech/homebridge-mqttthing.git" 17 | }, 18 | "keywords": [ 19 | "homebridge-plugin", 20 | "MQTT", 21 | "homekit", 22 | "Siri", 23 | "IoT", 24 | "ESPurna", 25 | "tasmota", 26 | "light", 27 | "light bulb", 28 | "switch", 29 | "outlet", 30 | "motion sensor", 31 | "occupancy sensor", 32 | "light sensor", 33 | "contact sensor", 34 | "doorbell", 35 | "smoke sensor", 36 | "security system", 37 | "humidity sensor", 38 | "temperature sensor", 39 | "garage door", 40 | "stateless programmable switch", 41 | "fan", 42 | "microphone", 43 | "speaker", 44 | "window covering", 45 | "blind", 46 | "leak sensor", 47 | "lock mechanism", 48 | "lock", 49 | "window", 50 | "air quality sensor", 51 | "carbondioxide sensor", 52 | "air pressure sensor", 53 | "weather station", 54 | "valve", 55 | "sprinkler", 56 | "shower", 57 | "faucet", 58 | "thermostat", 59 | "heater cooler", 60 | "irrigation system", 61 | "air purifier" 62 | ], 63 | "author": "David Miller", 64 | "license": "Apache-2.0", 65 | "bugs": { 66 | "url": "https://github.com/arachnetech/homebridge-mqttthing/issues" 67 | }, 68 | "homepage": "https://github.com/arachnetech/homebridge-mqttthing#readme", 69 | "dependencies": { 70 | "fakegato-history": "^0.6.1", 71 | "homebridge-lib": "~4.7.7", 72 | "mqtt": "^2.18.8" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /wip/notes.txt: -------------------------------------------------------------------------------- 1 | Configuration GUI Support 2 | ------------------------- 3 | 4 | config-ui-x and HOOBS use different configuration schema files to drive their 5 | configuration GUIs. HOOBS plan to add some of the more advanced features from 6 | config-ui-x in the near future, so for now I am focussing on config-ui-x. 7 | 8 | The current config.schema.json file from config-ui-x author, @oznu, has two main 9 | limitations: 10 | 11 | 1. The UI doesn't like properties with multiple types, so can't cope with the 12 | 'apply' format of topics (where a { "topic": ..., "apply": ... } object is 13 | provided as an alternative to just the topic string). 14 | 15 | "setBrightness": { 16 | "topic": "test/lightbulb/setBrightness", 17 | "apply": "return Math.round( message * 2.55 );" 18 | }, 19 | 20 | 2. The UI can't handle "startPub" with its user-defined keys. 21 | 22 | "startPub": { 23 | "test/lightbulb/setOn": "1", 24 | "test/lightbulb/getOn": "1", 25 | "test/lightbulb/setBrightness": "200", 26 | "test/lightbulb/getBrightness": "200", 27 | "test/lightbulb": "Hello world" 28 | } 29 | 30 | 31 | I can't immediately see a solution to issue 1 which wouldn't expose apply() 32 | functions for every topic in the GUI - which I would prefer to avoid as in apply 33 | functions are intended to be an advanced feature for solving specific issues. 34 | Having an 'apply' field below every 'topic' field would be cumbersome. Putting 35 | apply fields in a separate section is possible, but not ideal from a GUI 36 | perspective. 37 | 38 | However, I'm considering a new configuration mechanism based on device profiles, 39 | where common devices which currently require apply() functions in order to be 40 | configured would have the 'topics' part of their configuration generated from a 41 | simpler set of configuration properties. This feels like a better solution to 42 | me. For now, custom apply() functions defined directly in the configuration will 43 | require direct editing in JSON. 44 | 45 | Issue 2 is easier to solve. I will add an alternative startPub format like: 46 | 47 | "startPub": [ 48 | { "topic": "test/lightbulb/setOn", "message": "1" }, 49 | { "topic": "test/lightbulb/getOn", "message": "1" } 50 | ] 51 | 52 | Some accessories (particularly lightbulb) operate in different 'modes' depending 53 | on the topics configured. There are different types of light (on/off, dimmable, 54 | coloured), and come types of light can operate using different sets of topics 55 | (e.g. separate hue, saturation, brightness, RGB, HSV). Creating simplified 56 | configurations for specific light types could be very helpful here. 57 | 58 | 59 | So, the plan is: 60 | 61 | 1. Add alternative startPub format and update config.schema.json to support it. [Done] 62 | 2. Review field names and descriptions in config.schema.json. [Done] 63 | 3. Release initial config-ui-x support. [Done - Version 1.1.2] 64 | 4. Add lightbulb sub-types for easier configuration. [Done] 65 | 5. Add configuration profiles. 66 | -------------------------------------------------------------------------------- /wip/accessories.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugin_alias": "homebridge-mqttthing", 3 | "schemas": [{ 4 | "title": "Light bulb (on/off)", 5 | "type": "object", 6 | "properties": { 7 | "accessory": { 8 | "title": "Accessory", 9 | "type": "string", 10 | "const": "mqttthing", 11 | "readOnly": true 12 | }, 13 | "type": { 14 | "title": "Accessory", 15 | "type": "string", 16 | "const": "lightbulb", 17 | "readOnly": true 18 | }, 19 | "name": { 20 | "title": "Name", 21 | "type": "string", 22 | "default": "My light bulb", 23 | "required": true 24 | }, 25 | "url": { 26 | "title": "MQTT server URL", 27 | "type": "string", 28 | "required": false 29 | }, 30 | "username": { 31 | "title": "MQTT server username", 32 | "type": "string", 33 | "required": false 34 | }, 35 | "password": { 36 | "title": "MQTT server password", 37 | "type": "string", 38 | "required": false 39 | }, 40 | "topics": { 41 | "title": "MQTT topics", 42 | "type": "object", 43 | "properties": { 44 | "getOn": { 45 | "title": "Status topic on which Homebridge receives on/off state", 46 | "type": "string" 47 | }, 48 | "setOn": { 49 | "title": "Command topic on which Homebridge sends on/off state", 50 | "type": "string" 51 | } 52 | } 53 | }, 54 | "onValue": { 55 | "title": "Value used to represent 'on'", 56 | "type": "string", 57 | "default": "true", 58 | "required": true 59 | }, 60 | "offValue": { 61 | "title": "Value used to represent 'off'", 62 | "type": "string", 63 | "default": "false", 64 | "required": true 65 | } 66 | } 67 | }, { 68 | "title": "Accessory 2", 69 | "type": "object", 70 | "properties": { 71 | "accessory": { 72 | "title": "Accessory", 73 | "type": "string", 74 | "const": "mqttthing", 75 | "readOnly": true 76 | }, 77 | "name": { 78 | "title": "Name", 79 | "type": "string", 80 | "default": "Roborock Vacuum 2", 81 | "required": true 82 | } 83 | } 84 | }, { 85 | "title": "Accessory 3", 86 | "type": "object", 87 | "properties": { 88 | "accessory": { 89 | "title": "Accessory", 90 | "type": "string", 91 | "const": "mqttthing", 92 | "readOnly": true 93 | }, 94 | "name": { 95 | "title": "Name", 96 | "type": "string", 97 | "default": "Roborock Vacuum 3", 98 | "required": true 99 | } 100 | } 101 | }] 102 | } 103 | -------------------------------------------------------------------------------- /test/empty-codec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test/Demo Homebridge-MQTTThing Codec (encoder/decoder) 3 | * Codecs allow custom logic to be applied to accessories in mqttthing, rather like apply() functions, 4 | * but in the convenience of a stand-alone JavaScript file. 5 | * 6 | * empty-codec.js - shows the structure of a codec, but does nothing 7 | */ 8 | 9 | 'use strict'; 10 | 11 | // todo: declare any global state 12 | 13 | /** 14 | * Initialise codec for accessory 15 | * @param {object} params Initialisation parameters object 16 | * @param {function} params.log Logging function 17 | * @param {object} params.config Configuration 18 | * @param {function} params.publish Function to publish a message directly to MQTT 19 | * @param {function} params.notify Function to send MQTT-Thing a property notification 20 | * @return {object} Encode and/or decode functions 21 | */ 22 | function init( params ) { 23 | // extract parameters for convenience 24 | let { log } = params; 25 | 26 | // todo: declare state for accessory instance (available from encode/decode function definitions below) 27 | 28 | /** 29 | * Encode message before sending. 30 | * The output function may be called to deliver an encoded value for the property later. 31 | * @param {string} message Message from mqttthing to be published to MQTT 32 | * @param {object} info Object giving contextual information 33 | * @param {string} info.topic MQTT topic to be published 34 | * @param {string} info.property Property associated with publishing operation 35 | * @param {function} output Function which may be called to deliver the encoded value asynchronously 36 | * @returns {string} Processed message (optionally) 37 | */ 38 | function encode( message, info, output ) { // eslint-disable-line no-unused-vars 39 | log( `empty-codec: encode() called for topic [${info.topic}], property [${info.property}] with message [${message}]` ); 40 | // todo: manipulate message 41 | return message; 42 | } 43 | 44 | /** 45 | * Decode received message, and optionally return decoded value. 46 | * The output function may be called to deliver a decoded value for the property later. 47 | * @param {string} message Message received from MQTT 48 | * @param {object} info Object giving contextual information 49 | * @param {string} info.topic MQTT topic received 50 | * @param {string} info.property Property associated with subscription 51 | * @param {function} output Function which may be called to deliver the decoded value asynchronously 52 | * @returns {string} Processed message (optionally) 53 | */ 54 | function decode( message, info, output ) { // eslint-disable-line no-unused-vars 55 | log( `empty-codec: decode() called for topic [${info.topic}], property [${info.property}] with message [${message}]` ); 56 | // todo: manipulate message 57 | return message; 58 | } 59 | 60 | // todo: define separate encode/decode functions for specific properties 61 | 62 | /** 63 | * The init() function must return an object containing encode and/or decode functions as defined above. 64 | * To define property-specific encode/decode functions, the following syntax may be used: 65 | * { 66 | * properties: { 67 | * targetProp1: { 68 | * encode: encodeFunction1, 69 | * decode: decodeFunction1 70 | * }, 71 | * targetProp2: { 72 | * encode: encodeFunction2 73 | * }, 74 | * }, 75 | * encode: defaultEncodeFunction, 76 | * decode: defaultDecodeFunction 77 | * } 78 | * 79 | * The default encode/decode functions are called for properties for which no property-specific 80 | * entry is specified. 81 | */ 82 | 83 | // return encode and decode functions 84 | return { 85 | encode, 86 | decode 87 | }; 88 | } 89 | 90 | // export initialisation function 91 | module.exports = { 92 | init 93 | }; 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![npm](https://badgen.net/npm/v/homebridge-mqttthing/latest)](https://www.npmjs.com/package/homebridge-mqttthing) 2 | [![npm](https://badgen.net/npm/dt/homebridge-mqttthing)](https://www.npmjs.com/package/homebridge-mqttthing) 3 | [![Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=discord)](https://discord.gg/MTpeMC) 4 | [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 5 | 6 | # Homebridge MQTT-Thing 7 | [Homebridge MQTT-Thing](https://www.npmjs.com/package/homebridge-mqttthing) is a plugin for [Homebridge](https://github.com/homebridge/homebridge) allowing the integration of [many different accessory types](#supported-accessories) using MQTT. 8 | 9 | * [Installation](#installation) 10 | * [Configuration](#configuration) 11 | * [Supported Accessories](#supported-accessories) 12 | * [Release notes](docs/ReleaseNotes.md) 13 | 14 | ## Compatibility with previous versions 15 | 16 | **From version 1.1.x, raw JavaScript values for Boolean properties are passed to MQTT apply functions.** This may change published message formats, e.g. when apply functions are used to build JSON strings. 17 | 18 | For full details of changes please see the [Release notes](docs/ReleaseNotes.md). 19 | 20 | ## Installation 21 | Follow the instructions in [homebridge](https://www.npmjs.com/package/homebridge) for the homebridge server installation. 22 | This plugin is published through [NPM](https://www.npmjs.com/package/homebridge-mqttthing) and should be installed "globally" by typing: 23 | 24 | npm install -g homebridge-mqttthing 25 | 26 | Installation through 27 | [Homebridge Config UI X](https://www.npmjs.com/package/homebridge-config-ui-x) is also supported (and recommended). 28 | 29 | ## Configuration 30 | Configure the plugin in your homebridge `config.json` file. Most configuration settings can now also be entered using 31 | [Homebridge Config UI X](https://www.npmjs.com/package/homebridge-config-ui-x). 32 | 33 | MQTT topics used fall into two categories: 34 | 35 | * Control topics, of the form `setXXX`, are published by MQTT-Thing in order to control device state (e.g. to turn on a light). 36 | * Status/notification topics, of the form `getXXX`, are published by the device to notify MQTT-Thing that something has occurred (e.g. that a sensor has detected something or a control topic action has been performed). 37 | 38 | For further details, see [docs/Configuration.md](docs/Configuration.md) and [docs/Codecs.md](docs/Codecs.md). 39 | 40 | ## Supported Accessories 41 | 42 | The following Homekit accessory types are supported by MQTT-Thing: 43 | 44 | * [Air Pressure Sensor](docs/Accessories.md#air-pressure-sensor) 45 | * [Air Purifier](docs/Accessories.md#air-purifier) 46 | * [Air Quality Sensor](docs/Accessories.md#air-quality-sensor) 47 | * [Carbon Dioxide Sensor](docs/Accessories.md#carbon-dioxide-sensor) 48 | * [Contact Sensor](docs/Accessories.md#contact-sensor) 49 | * [Doorbell](docs/Accessories.md#doorbell) 50 | * [Fan](docs/Accessories.md#fan) 51 | * [Garage door opener](docs/Accessories.md#garage-door-opener) 52 | * [Heater Cooler](docs/Accessories.md#heater-cooler) 53 | * [Humidity Sensor](docs/Accessories.md#humidity-sensor) 54 | * [Irrigation System](docs/Accessories.md#irrigation-system) 55 | * [Leak Sensor](docs/Accessories.md#leak-sensor) 56 | * [Light bulb](docs/Accessories.md#light-bulb) 57 | * [Light Sensor](docs/Accessories.md#light-sensor) 58 | * [Lock Mechanism](docs/Accessories.md#lock-mechanism) 59 | * [Microphone](docs/Accessories.md#microphone) 60 | * [Motion Sensor](docs/Accessories.md#motion-sensor) 61 | * [Occupancy Sensor](docs/Accessories.md#occupancy-sensor) 62 | * [Outlet](docs/Accessories.md#outlet) 63 | * [Security System](docs/Accessories.md#security-system) 64 | * [Speaker](docs/Accessories.md#speaker) 65 | * [StatelessProgrammableSwitch](docs/Accessories.md#statelessprogrammableswitch) 66 | * [Switch](docs/Accessories.md#switch) 67 | * [Television](docs/Accessories.md#television) 68 | * [Temperature Sensor](docs/Accessories.md#temperature-sensor) 69 | * [Thermostat](docs/Accessories.md#thermostat) 70 | * [Valve (Sprinkler, Shower, Faucet)](docs/Accessories.md#valve) 71 | * [Weather Station](docs/Accessories.md#weather-station) 72 | * [Window](docs/Accessories.md#window) 73 | * [Window Covering (Blinds)](docs/Accessories.md#window-covering) 74 | 75 | ## Tested Configurations 76 | 77 | Tested and working configurations for devices are available on the [Wiki](https://github.com/arachnetech/homebridge-mqttthing/wiki/Tested-Configurations). Please add your working configurations for others. 78 | -------------------------------------------------------------------------------- /test/test-codec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test/Demo Homebridge-MQTTThing Codec (encoder/decoder) 3 | * Codecs allow custom logic to be applied to accessories in mqttthing, rather like apply() functions, 4 | * but in the convenience of a stand-alone JavaScript file. 5 | */ 6 | 7 | 'use strict'; 8 | 9 | /** 10 | * Initialise codec for accessory 11 | * @param {object} params Initialisation parameters object 12 | * @param {function} params.log Logging function 13 | * @param {object} params.config Configuration 14 | * @param {function} params.publish Function to publish a message directly to MQTT 15 | * @param {function} params.notify Function to send MQTT-Thing a property notification 16 | * @return {object} Encode and/or decode functions 17 | */ 18 | function init( params ) { 19 | // extract parameters for convenience 20 | let { log, config, publish, notify } = params; 21 | 22 | setTimeout( () => { 23 | let msg = `Hello from test-codec.js. This is ${config.name}.`; 24 | log( msg ); 25 | 26 | // publish a test message 27 | publish( 'hello/mqtt', msg ); 28 | 29 | // update state 30 | notify( 'on', config.onValue || 1 ); 31 | notify( 'brightness', 50 ); 32 | notify( 'HSV', '0,100,100' ); 33 | }, 3000 ); 34 | 35 | /** 36 | * Encode message before sending. 37 | * The output function may be called to deliver an encoded value for the property later. 38 | * @param {string} message Message from mqttthing to be published to MQTT 39 | * @param {object} info Object giving contextual information 40 | * @param {string} info.topic MQTT topic to be published 41 | * @param {string} info.property Property associated with publishing operation 42 | * @param {function} output Function which may be called to deliver the encoded value asynchronously 43 | * @returns {string} Processed message (optionally) 44 | */ 45 | function encode( message, info, output ) { 46 | log( `encode() called for topic [${info.topic}], property [${info.property}] with message [${message}]` ); 47 | 48 | // in this example we just delay publishing 49 | setTimeout( () => { 50 | output( message ); 51 | }, 1000 ); 52 | } 53 | 54 | /** 55 | * Decode received message, and optionally return decoded value. 56 | * The output function may be called to deliver a decoded value for the property later. 57 | * @param {string} message Message received from MQTT 58 | * @param {object} info Object giving contextual information 59 | * @param {string} info.topic MQTT topic received 60 | * @param {string} info.property Property associated with subscription 61 | * @param {function} output Function which may be called to deliver the decoded value asynchronously 62 | * @returns {string} Processed message (optionally) 63 | */ 64 | function decode( message, info, output ) { // eslint-disable-line no-unused-vars 65 | log( `decode() called for topic [${info.topic}], property [${info.property}] with message [${message}]` ); 66 | 67 | // in this example we just delay passing the received mesage on to homebridge 68 | setTimeout( () => { 69 | output( message ); 70 | }, 500 ); 71 | } 72 | 73 | function encode_brightness( message ) { 74 | // scale up to 0-255 range 75 | log( "brightness out: " + message ); 76 | return Math.floor( message * 2.55 ); 77 | } 78 | 79 | function decode_brightness( message ) { 80 | // scale down to 0-100 range 81 | log( "brightness in: " + message ); 82 | return Math.floor( message / 2.55 ); 83 | } 84 | 85 | /** 86 | * The init() function must return an object containing encode and/or decode functions as defined above. 87 | * To define property-specific encode/decode functions, the following syntax may be used: 88 | * { 89 | * properties: { 90 | * targetProp1: { 91 | * encode: encodeFunction1, 92 | * decode: decodeFunction2 93 | * }, 94 | * targetProp2: { 95 | * encode: encodeFunction2 96 | * }, 97 | * }, 98 | * encode: defaultEncodeFunction, 99 | * decode: defaultDecodeFunction 100 | * } 101 | * 102 | * The default encode/decode functions are called for properties for which no property-specific 103 | * entry is specified. 104 | */ 105 | 106 | // return encode and decode functions 107 | return { 108 | encode, decode, // default encode/decode functions 109 | properties: { 110 | brightness: { // encode/decode functions for brightness property 111 | encode: encode_brightness, 112 | decode: decode_brightness 113 | } 114 | } 115 | }; 116 | } 117 | 118 | // export initialisation function 119 | module.exports = { 120 | init 121 | }; 122 | -------------------------------------------------------------------------------- /codecs/json.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Homebridge-MQTTThing JSON Codec (encoder/decoder) 3 | * codecs/json.js 4 | * 5 | * Add configuration giving JSON 'path' for each property used by the accessory. For example: 6 | * "jsonCodec": { 7 | * "properties": { 8 | * "on": "state.power", 9 | * "RGB": "state.rgb" 10 | * }, 11 | * "fixed": { fixed properties object (global/default) }, 12 | * "fixedByTopic": { 13 | * "topic1": { fixed properties object for topic1 }, 14 | * "topic2": { fixed properties object for topic2 } 15 | * }, 16 | * "retain": true|false 17 | * } 18 | * 19 | * Set retain: true in order to retain the object published for each topic, so that unchanged properties are published. 20 | * (Default is retain: false, recreating object from fixed properties on every publish.) 21 | */ 22 | 23 | 'use strict'; 24 | 25 | function splitJPath( jpath ) { 26 | return jpath.split( '.' ); 27 | } 28 | 29 | function setJson( msg, jpath, val ) { 30 | let obj = msg; 31 | let apath = splitJPath( jpath ); 32 | for( let i = 0; i < apath.length - 1; i++ ) { 33 | let item = apath[ i ]; 34 | if( ! obj.hasOwnProperty( item ) ) { 35 | obj[ item ] = {}; 36 | } 37 | obj = obj[ item ]; 38 | } 39 | obj[ apath[ apath.length - 1 ] ] = val; 40 | } 41 | 42 | function getJson( msg, jpath ) { 43 | let val = msg; 44 | for( let pi of splitJPath( jpath ) ) { 45 | if( val.hasOwnProperty( pi ) ) { 46 | val = val[ pi ]; 47 | } else { 48 | return; 49 | } 50 | } 51 | return val; 52 | } 53 | 54 | /** 55 | * Initialise codec for accessory 56 | * @param {object} params Initialisation parameters object 57 | * @param {function} params.log Logging function 58 | * @param {object} params.config Configuration 59 | * @param {function} params.publish Function to publish a message directly to MQTT 60 | * @param {function} params.notify Function to send MQTT-Thing a property notification 61 | * @return {object} Encode and/or decode functions 62 | */ 63 | function init( params ) { 64 | // extract parameters for convenience 65 | let { log, config } = params; 66 | let jsonConfig = config.jsonCodec; 67 | if( ! jsonConfig ) { 68 | log.warn( 'Add jsonCodec object to configuration' ); 69 | } 70 | 71 | let readJPath = function( prop ) { 72 | if( jsonConfig && jsonConfig.properties ) { 73 | return jsonConfig.properties[ prop ]; 74 | } 75 | }; 76 | 77 | let emptyMessage = function( topic ) { 78 | if( jsonConfig ) { 79 | if( jsonConfig.fixedByTopic && jsonConfig.fixedByTopic[ topic ] ) { 80 | return JSON.parse( JSON.stringify( jsonConfig.fixedByTopic[ topic ] ) ); 81 | } else if( jsonConfig.fixed ) { 82 | return JSON.parse( JSON.stringify( jsonConfig.fixed ) ); 83 | } 84 | } 85 | return {}; 86 | }; 87 | 88 | // pending messages/timers by MQTT topic 89 | let pending = {}; 90 | 91 | // get message object which will be published (automatically) 92 | let publishMessage = function( topic, publish ) { 93 | let entry = pending[ topic ]; 94 | if( entry ) { 95 | // existing entry - clear any timer 96 | if( entry.tmr ) { 97 | clearTimeout( entry.tmr ); 98 | } 99 | } else { 100 | // new entry 101 | entry = pending[ topic ] = { msg: emptyMessage( topic ) }; 102 | } 103 | 104 | // publish later 105 | entry.tmr = setTimeout( () => { 106 | if( jsonConfig && jsonConfig.retain ) { 107 | // retain: just clear timer - keep the message 108 | entry.tmr = null; 109 | } else { 110 | // no retain: remove entry 111 | pending[ topic ] = null; 112 | } 113 | publish( JSON.stringify( entry.msg ) ); 114 | }, 50 ); 115 | 116 | return entry.msg; 117 | } 118 | 119 | /** 120 | * Encode message before sending. 121 | * The output function may be called to deliver an encoded value for the property later. 122 | * @param {string} message Message from mqttthing to be published to MQTT 123 | * @param {object} info Object giving contextual information 124 | * @param {string} info.topic MQTT topic to be published 125 | * @param {string} info.property Property associated with publishing operation 126 | * @param {function} output Function which may be called to deliver the encoded value asynchronously 127 | * @returns {string} Processed message (optionally) 128 | */ 129 | function encode( message, info, output ) { // eslint-disable-line no-unused-vars 130 | let diag = ! jsonConfig || jsonConfig.diag; 131 | let jpath = readJPath( info.property ); 132 | if( jpath ) { 133 | let msg = publishMessage( info.topic, output ); 134 | setJson( msg, jpath, message ); 135 | } else { 136 | diag = true; 137 | } 138 | if( diag ) { 139 | log( `json-codec: encode() called for topic [${info.topic}], property [${info.property}] with message [${message}]` ); 140 | } 141 | } 142 | 143 | /** 144 | * Decode received message, and optionally return decoded value. 145 | * The output function may be called to deliver a decoded value for the property later. 146 | * @param {string} message Message received from MQTT 147 | * @param {object} info Object giving contextual information 148 | * @param {string} info.topic MQTT topic received 149 | * @param {string} info.property Property associated with subscription 150 | * @param {function} output Function which may be called to deliver the decoded value asynchronously 151 | * @returns {string} Processed message (optionally) 152 | */ 153 | function decode( message, info, output ) { // eslint-disable-line no-unused-vars 154 | let diag = ! jsonConfig || jsonConfig.diag; 155 | let jpath = readJPath( info.property ); 156 | let decoded; 157 | if( jpath ) { 158 | let msg = JSON.parse( message ); 159 | decoded = getJson( msg, jpath ); 160 | } else { 161 | diag = true; 162 | } 163 | if( diag ) { 164 | log( `json-codec: decode() called for topic [${info.topic}], property [${info.property}] with message [${message}]` ); 165 | } 166 | return decoded; 167 | } 168 | 169 | // return encode and decode functions 170 | return { 171 | encode, 172 | decode 173 | }; 174 | } 175 | 176 | // export initialisation function 177 | module.exports = { 178 | init 179 | }; 180 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "ecmaVersion": 9 /* otherwise object spread causes 'Parsing error: Unexpected token...' */ 9 | }, 10 | "rules": { 11 | "accessor-pairs": "error", 12 | "array-bracket-spacing": "off", 13 | "array-callback-return": "error", 14 | "array-element-newline": "off", 15 | "arrow-body-style": "error", 16 | "arrow-parens": "error", 17 | "arrow-spacing": "error", 18 | "block-scoped-var": "error", 19 | "block-spacing": "error", 20 | "brace-style": [ 21 | "error", 22 | "1tbs" 23 | ], 24 | "callback-return": "off", 25 | "camelcase": "off", 26 | "capitalized-comments": "off", 27 | "class-methods-use-this": "error", 28 | "comma-dangle": "error", 29 | "comma-spacing": [ 30 | "error", 31 | { 32 | "after": true, 33 | "before": false 34 | } 35 | ], 36 | "comma-style": [ 37 | "error", 38 | "last" 39 | ], 40 | "complexity": "off", 41 | "computed-property-spacing": "off", 42 | "consistent-return": "off", 43 | "consistent-this": "error", 44 | "curly": "error", 45 | "default-case": "off", 46 | "dot-location": "error", 47 | "dot-notation": "error", 48 | "eol-last": "error", 49 | "eqeqeq": "off", 50 | "func-call-spacing": "error", 51 | "func-name-matching": "error", 52 | "func-names": [ 53 | "error", 54 | "never" 55 | ], 56 | "func-style": [ 57 | "off", 58 | "declaration" 59 | ], 60 | "function-paren-newline": "off", 61 | "generator-star-spacing": "error", 62 | "global-require": "off", 63 | "guard-for-in": "error", 64 | "handle-callback-err": "error", 65 | "id-blacklist": "error", 66 | "id-length": "off", 67 | "id-match": "error", 68 | "implicit-arrow-linebreak": "error", 69 | "indent": "off", 70 | "indent-legacy": "off", 71 | "init-declarations": "off", 72 | "jsx-quotes": "error", 73 | "key-spacing": "error", 74 | "keyword-spacing": "off", 75 | "line-comment-position": "off", 76 | "lines-around-comment": "off", 77 | "lines-around-directive": "error", 78 | "lines-between-class-members": "error", 79 | "max-classes-per-file": "error", 80 | "max-depth": "off", 81 | "max-len": "off", 82 | "max-lines": "off", 83 | "max-lines-per-function": "off", 84 | "max-nested-callbacks": "error", 85 | "max-params": "off", 86 | "max-statements": "off", 87 | "max-statements-per-line": "off", 88 | "multiline-comment-style": "off", 89 | //"new-parens": "error", 90 | "newline-after-var": "off", 91 | "newline-before-return": "off", 92 | "newline-per-chained-call": "off", 93 | "no-alert": "error", 94 | "no-array-constructor": "error", 95 | "no-await-in-loop": "error", 96 | "no-bitwise": "error", 97 | "no-buffer-constructor": "error", 98 | "no-caller": "error", 99 | "no-catch-shadow": "error", 100 | "no-confusing-arrow": "error", 101 | "no-continue": "error", 102 | "no-div-regex": "error", 103 | "no-duplicate-imports": "error", 104 | "no-else-return": "off", 105 | "no-empty-function": "error", 106 | "no-eq-null": "off", 107 | "no-eval": "error", 108 | "no-extend-native": "error", 109 | "no-extra-bind": "error", 110 | "no-extra-label": "error", 111 | "no-extra-parens": "off", 112 | "no-floating-decimal": "error", 113 | "no-implicit-globals": "error", 114 | "no-implied-eval": "error", 115 | "no-inline-comments": "off", 116 | "no-inner-declarations": [ 117 | "error", 118 | "functions" 119 | ], 120 | //"no-invalid-this": "error", 121 | "no-iterator": "error", 122 | "no-label-var": "error", 123 | "no-labels": "error", 124 | "no-lone-blocks": "error", 125 | "no-lonely-if": "off", 126 | "no-loop-func": "error", 127 | "no-magic-numbers": "off", 128 | "no-mixed-operators": "off", 129 | "no-mixed-requires": "error", 130 | //"no-multi-assign": "error", 131 | "no-multi-spaces": "off", 132 | "no-multi-str": "error", 133 | "no-multiple-empty-lines": "error", 134 | "no-native-reassign": "error", 135 | "no-negated-in-lhs": "error", 136 | "no-nested-ternary": "error", 137 | "no-new": "error", 138 | "no-new-func": "error", 139 | "no-new-object": "error", 140 | "no-new-require": "error", 141 | "no-new-wrappers": "error", 142 | "no-octal-escape": "error", 143 | "no-param-reassign": "off", 144 | "no-path-concat": "error", 145 | /*"no-plusplus": [ 146 | "error", 147 | { 148 | "allowForLoopAfterthoughts": true 149 | } 150 | ],*/ 151 | "no-process-env": "error", 152 | "no-process-exit": "error", 153 | "no-proto": "error", 154 | "no-prototype-builtins": "off", 155 | "no-restricted-globals": "error", 156 | "no-restricted-imports": "error", 157 | "no-restricted-modules": "error", 158 | "no-restricted-properties": "error", 159 | "no-restricted-syntax": "error", 160 | "no-return-assign": "error", 161 | "no-return-await": "error", 162 | "no-script-url": "error", 163 | "no-self-compare": "error", 164 | "no-sequences": "off", 165 | "no-shadow": "error", 166 | "no-shadow-restricted-names": "error", 167 | "no-spaced-func": "error", 168 | "no-sync": "off", 169 | "no-tabs": "error", 170 | "no-template-curly-in-string": "error", 171 | "no-ternary": "off", 172 | "no-throw-literal": "error", 173 | "no-trailing-spaces": "off", 174 | "no-undef-init": "error", 175 | "no-undefined": "off", 176 | "no-underscore-dangle": "error", 177 | "no-unmodified-loop-condition": "error", 178 | "no-unneeded-ternary": "off", 179 | "no-unused-expressions": "off", 180 | "no-use-before-define": "off", 181 | "no-useless-call": "error", 182 | "no-useless-computed-key": "error", 183 | "no-useless-concat": "error", 184 | "no-useless-constructor": "error", 185 | "no-useless-rename": "error", 186 | "no-useless-return": "error", 187 | "no-var": "off", 188 | "no-void": "error", 189 | "no-warning-comments": "off", 190 | "no-whitespace-before-property": "error", 191 | "no-with": "error", 192 | "nonblock-statement-body-position": "error", 193 | //"object-curly-spacing": "error", 194 | "object-property-newline": "off", 195 | "object-shorthand": "off", 196 | "one-var": "off", 197 | "one-var-declaration-per-line": "off", 198 | "operator-linebreak": "error", 199 | "padded-blocks": "off", 200 | "padding-line-between-statements": "error", 201 | "prefer-arrow-callback": "off", 202 | "prefer-const": "off", 203 | "prefer-destructuring": "off", 204 | "prefer-numeric-literals": "error", 205 | "prefer-object-spread": "error", 206 | "prefer-promise-reject-errors": "error", 207 | "prefer-reflect": "error", 208 | "prefer-rest-params": "error", 209 | "prefer-spread": "error", 210 | "prefer-template": "off", 211 | "quote-props": "off", 212 | "quotes": "off", 213 | "radix": [ 214 | "error", 215 | "as-needed" 216 | ], 217 | "require-await": "error", 218 | "require-jsdoc": "off", 219 | "rest-spread-spacing": "error", 220 | "semi": "off", 221 | "semi-spacing": [ 222 | "error", 223 | { 224 | "after": true, 225 | "before": false 226 | } 227 | ], 228 | "semi-style": [ 229 | "error", 230 | "last" 231 | ], 232 | "sort-imports": "error", 233 | "sort-keys": "off", 234 | "sort-vars": "off", 235 | "space-before-blocks": "error", 236 | "space-before-function-paren": "off", 237 | "space-in-parens": "off", 238 | "space-infix-ops": "off", 239 | "space-unary-ops": "off", 240 | "spaced-comment": "off", 241 | "strict": "error", 242 | "switch-colon-spacing": [ 243 | "error", 244 | { 245 | "after": true, 246 | "before": false 247 | } 248 | ], 249 | "symbol-description": "error", 250 | "template-curly-spacing": "error", 251 | "template-tag-spacing": "error", 252 | "unicode-bom": [ 253 | "error", 254 | "never" 255 | ], 256 | "valid-jsdoc": "error", 257 | "vars-on-top": "off", 258 | "wrap-iife": "error", 259 | "wrap-regex": "error", 260 | "yield-star-spacing": "error", 261 | "yoda": [ 262 | "error", 263 | "never" 264 | ] 265 | } 266 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /docs/ReleaseNotes.md: -------------------------------------------------------------------------------- 1 | [![npm](https://badgen.net/npm/v/homebridge-mqttthing/latest)](https://www.npmjs.com/package/homebridge-mqttthing) 2 | [![npm](https://badgen.net/npm/dt/homebridge-mqttthing)](https://www.npmjs.com/package/homebridge-mqttthing) 3 | [![Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=discord)](https://discord.gg/MTpeMC) 4 | [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 5 | 6 | # Homebridge MQTT-Thing: Release Notes 7 | 8 | ### Version 1.1.22 9 | + Light: Added redThreshold, greenThreshold, blueThreshold for when whiteMix is false in an RGBWW light 10 | + Light: Added minColorTemperature and maxColorTemperature configuration settings 11 | + Light: Added switchWhites setting for RGBWW light 12 | 13 | ### Version 1.1.21 14 | + Updated fakegato-history dependency version 15 | + Added whiteMix option to configuration schema 16 | + Fixed publishing confirmation with Boolean values (https://github.com/arachnetech/homebridge-mqttthing/issues/363) 17 | 18 | ### Version 1.1.20 19 | + Added insecure option for TLS, disabling checking of certificate and server identity 20 | 21 | ### Version 1.1.19 22 | + Allowed TLS certificate, key and ca pem files to be loaded 23 | 24 | ### Version 1.1.18 25 | + Added whiteMix option for RGBWW lights. Set whiteMix to false to disable extraction of white components from colours - i.e. powering only RGB channels or WW,CW channels. (https://github.com/arachnetech/homebridge-mqttthing/issues/300) 26 | 27 | ### Version 1.1.17 (TEST BUILD) 28 | + Added support for grouped (custom) accessories (https://github.com/arachnetech/homebridge-mqttthing/issues/201) 29 | 30 | ### Version 1.1.16 31 | + Changed order of Codec and apply() used together so that on publishing values pass through apply function before codec, and on subscription values pass through codec before apply function. This allows manipulation of values like the red,green,blue string from the RGB light before codec encoding and after codec decoding. This makes the JSON codec more flexible. 32 | 33 | ### Version 1.1.15 34 | + Fixed weather station with homebridge-lib 4.7.7 35 | 36 | ### Version 1.1.14 37 | + Added air purifier (implemented by @tobekas) 38 | + Added irrigation system (implemented by @tobekas) 39 | + JSON codec: added per-topic fixed values and retain option 40 | 41 | ### Version 1.1.13 42 | + When using missing confirmation to set offline state, any message received after timeout sets state back to online 43 | + Added internal codec concept: specifying a codec with no .js suffix will load it from the mqttthing 'codecs' directory 44 | + Added experimental JSON codec (json) 45 | + RGB and HSV lights: wait for multiple property changes before publishing 46 | 47 | ### Version 1.1.12 48 | + Extended codecs to support ad hoc property changes and MQTT publishing 49 | + Codec defaults changed to apply per-function 50 | 51 | ### Version 1.1.11 52 | + Fixed publishing of empty messages configured through config-ui-x in startPub (#253) 53 | 54 | ### Version 1.1.10 55 | + Fixed crash (introduced in version 1.1.9) with confirmed publisher on/off acknowledgement (#252) 56 | 57 | ### Version 1.1.9 58 | + Added persistencePath to historyOptions 59 | + Added support for codecs 60 | + Added state to apply functions 61 | 62 | ### Version 1.1.8 63 | + Garage door add getDoorMoving option as simpler alternative to getCurrentDoorState 64 | + Changed default garage door state (after restart) to closed 65 | 66 | ### Version 1.1.7 67 | + Allow temperature sensor current temperature range to be overriden (using minTemperature and maxTemperature) 68 | + Added confirmationIndicateOffline option 69 | + Moved history options from history to historyOptions object 70 | + Added config-ui-x support for historyOptions 71 | 72 | ### Version 1.1.6 73 | + Added history support for switch (implemented by @tobekas) 74 | + Fixed #223 and #207 - history not working if getTotalConsumption used (implemented by @tobekas) 75 | + Fixed history last activation in motion and contact sensor (implemented by @tobekas) 76 | + Allowed config.url string without protocol, now defaulting to mqtt:// (implemented by @tobekas) 77 | 78 | ### Version 1.1.5 79 | + Don't throw an exception at start-up if the configuration is invalid (as this stops Homebridge unnecessarily) 80 | 81 | ### Version 1.1.4 82 | + Fixed excessive MQTT logging (introduced in 1.1.2). Thanks, @nzbullet. 83 | 84 | ### Version 1.1.3 85 | + Added lightbulb sub-types to configuration schema, allowing easier configuration of different lightbulb types. 86 | + Added missing otherValueOff to configuration schema. 87 | 88 | ### Version 1.1.2 89 | + Added configuration schema, supporting configuration of most settings through config-ui-x (thanks, @oznu). Note that 'apply' functions are not supported. 90 | + Added new 'startPub' format, allowing configuration through config-ui-x. 91 | 92 | ### Version 1.1.1 93 | + Changed Boolean value handling to support bare JavaScript Booleans returned from incoming MQTT apply() functions (`"true" != true` but both are now accepted). 94 | + Boolean property values passed to outgoing MQTT apply() functions are no-longer converted to strings first (for consistency with the change above). This allows easier publishing of JSON containing correctly typed values, but **may change outgoing message format with existing configurations in some situations**. 95 | + Added option to configure garage door target values independently of current values - thanks, Charles Powell 96 | 97 | ### Version 1.0.50 98 | + Stateless Programmable Switch: allow multiple buttons under a single switch - thanks, Jacob Nite 99 | 100 | ### Version 1.0.49 101 | + Stateless Programmable Switch: added option to restrict available switch values - thanks, Jacob Nite 102 | 103 | ### Version 1.0.48 104 | + Upgrade to latest homebridge-lib (4.4.10 or later) 105 | 106 | ### Version 1.0.47 107 | + Fix: latest homebridge-lib (introduced in last build) appears incompatible with our references to Eve.Characteristic (e.g. Eve.Characteristic.CurrentConsumption) 108 | 109 | ### Version 1.0.46 110 | + Suppress logging of MQTT password (issue #150) 111 | 112 | ### Version 1.0.45 113 | + Allow changing of default run time (duration) range for valve 114 | 115 | ### Version 1.0.44 116 | + Added HeaterCooler 117 | 118 | ### Version 1.0.43 119 | + Added option to treat unrecognized received on/off values as off when an explicit off value is configured 120 | + Security System: Allow target states to be restricted 121 | 122 | ### Version 1.0.42 123 | + Added publishing confirmation (`setOn` message must be echoed to `getOn` topic to confirm that it has been processed by the accessory), with automatic republishing 124 | + Added Television (implemented by tobekas) 125 | + Fix to characteristics with multiple states (thanks, tobekas) 126 | 127 | ### Version 1.0.41 128 | + Light: Add option to control dimmable white light through integer in range 0-255, so that one channel of an RGB or RGBW controller can be used more easily for a white light 129 | 130 | ### Version 1.0.40 131 | + Thermostat: Allow target heating/cooling states to be restricted 132 | 133 | ### Version 1.0.39 134 | + Valve: Added duration timer (implemented by tobekas) 135 | 136 | ### Version 1.0.38 137 | + Thermostat: Allow minimum and maximum target temperature to be configured 138 | 139 | ### Version 1.0.37 140 | + Added Thermostat 141 | 142 | ### Version 1.0.36 143 | + Fix to Valve remaining duration 144 | + Added experimental support for RGBWWCW lights (red, green, blue, warm_white and cold_white channels) 145 | 146 | ### Version 1.0.35 147 | + Added Valve (for Sprinkler, Shower and Faucet) - implemented by tobekas 148 | 149 | ### Version 1.0.34 150 | + Added Air Pressure Sensor (implemented by tobekas) 151 | + Added Weather Station with custom Eve characteristics (implemented by tobekas) 152 | + Fakegato-History fix 153 | 154 | ### Version 1.0.33 155 | + Added optional air quality sensor characteristics 156 | 157 | ### Version 1.0.32 158 | + Added resetStateAfterms option for contact sensor, leak sensor and smoke sensor 159 | 160 | ### Version 1.0.31 161 | + Added Eve history support for outlet power consumption (implemented by tobekas) 162 | + Wrap exception handling around 'apply' functions (used for encoding/decoding MQTT messages), so that errors don't crash Homebridge and messages that can't be decoded are skipped 163 | 164 | ### Version 1.0.30 165 | + Added Elgato history support for AirQuality (thanks, sieren) 166 | + Extended Eve history support (implemented by tobekas) 167 | 168 | ### Version 1.0.29 169 | + Added history support for Eve App (only) using fakegato-history (implemented by tobekas) 170 | 171 | ### Version 1.0.28 172 | + Improve behaviour of RGB, RGBW and HSV lightbulbs when not using a separate on/off topic 173 | 174 | ### Version 1.0.27 175 | + Added ColorTemperature to Light bulb 176 | 177 | ### Version 1.0.26 178 | + Added Window 179 | + Added Air Quality Sensor 180 | + Added Carbon Dioxide Sensor 181 | 182 | ### Version 1.0.25 183 | + Added Lock Mechanism 184 | 185 | ### Version 1.0.24 186 | + Added Speaker and Microphone 187 | + Added Window Covering (blind) 188 | 189 | ### Version 1.0.23 190 | + Add MQTT publishing options configuration setting (`mqttPubOptions`), to allow retain flag and QoS level to be set 191 | + If no offValue is specified, don't publish anything when a Boolean characteristic turns off 192 | + When receiving a Boolean value, require configured off value to turn it off 193 | 194 | ### Version 1.0.22 195 | + Added `startPub` configuration setting, allowing MQTT messages to be published on start-up 196 | 197 | ### Version 1.0.21 198 | + Added InformationService to populate manufacturer and other characteristics (thanks, NorthernMan54) 199 | + Added Leak Sensor 200 | 201 | ### Version 1.0.20 202 | + Added `onlineValue` configuration setting, allowing the use of a custom value to represent an online state (with `getOnline`) without the use of a custom payload decoding function. 203 | + Added `turnOffAfterms` support for motion sensor, allowing motion triggered by MQTT message to be self-resetting. 204 | 205 | ### Version 1.0.19 206 | + Changed minimum temperature for temperatureSensor to -100 degrees celsius 207 | + Added BatteryService supporting `getBatteryLevel`, `getChargingState` and `getStatusLowBattery` for all accessories. 208 | 209 | ### Version 1.0.18 210 | + Added `getOnline` topic to control whether an accessory should appear responsive or unresponsive 211 | 212 | ### Version 1.0.17 213 | + Added ability to encode/decode MQTT payload using custom JavaScript functions (implemented by Michael Stürmer) 214 | 215 | ### Version 1.0.16 216 | + Allow MQTT options to be passed directly, so that any options required can be set (not just those specifically supported by mqttthing) 217 | 218 | ### Version 1.0.15 219 | + Allowed Garage Door and Security System target states to be modified outside of HomeKit (thanks, brefra) 220 | 221 | ### Version 1.0.14 222 | + Added `turnOffAfterms` to items with an On characteristic like Switch, causing them to turn off automatically after a specified timeout (in milliseconds) 223 | 224 | ### Version 1.0.13 225 | + Remove non-ASCII characters from MQTT client ID (thanks, twinkelm) 226 | 227 | ### Version 1.0.12 228 | + Added Fan 229 | 230 | ### Version 1.0.11 231 | + Added Light bulb option to publish RGB and RGBW values as hex 232 | + Added Light bulb option to publish RGB white level separately 233 | 234 | ### Version 1.0.10 235 | + Allowed separate on/off topic when using combined "hue,saturation,value" topic with Light bulb 236 | + Added Light bulb combined "red,green,blue" topic support 237 | + Added Light bulb RGBW support through combined "red,green,blue,white" topic 238 | 239 | ### Version 1.0.9 240 | + Added option to combine Light bulb hue (0-360), saturation (0-100) and value/brightness (0-100) into a single topic containing "hue,saturation,value" 241 | 242 | ### Version 1.0.8 243 | + Added Stateless Programmable Switch 244 | + Added Garage Door Opener 245 | 246 | ### Version 1.0.7 247 | + Fixed Smoke Sensor 248 | 249 | ### Version 1.0.6 250 | + Added Temperature Sensor 251 | + Added Humidity Sensor 252 | 253 | ### Version 1.0.5 254 | + Added Security System 255 | + Added Smoke Sensor 256 | 257 | ### Version 1.0.4 258 | + Fixed Occupancy Sensor values 259 | + Added Doorbell 260 | 261 | ### Version 1.0.3 262 | + Added Contact Sensor 263 | 264 | ### Version 1.0.2 265 | + Added Light Sensor 266 | + Default sensors to 'active' state 267 | 268 | ### Version 1.0.1 269 | + Initial public version with Light bulb, Switch, Outlet, Motion Sensor, Occupancy Sensor 270 | -------------------------------------------------------------------------------- /test2/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Dev-2", 4 | "username": "CA:7C:A3:34:AA:6B", 5 | "port": 51826, 6 | "pin": "784-65-424" 7 | }, 8 | "platforms": [ 9 | { 10 | "platform": "config", 11 | "name": "Config", 12 | "port": 8080, 13 | "sudo": false 14 | } 15 | ], 16 | "accessories": [ 17 | { 18 | "type": "lightbulb-OnOff", 19 | "name": "Light", 20 | "url": "homebridge", 21 | "topics": { 22 | "getOn": "light/get", 23 | "setOn": "light/set" 24 | }, 25 | "whiteMix": true, 26 | "accessory": "mqttthing" 27 | }, 28 | { 29 | "accessory": "mqttthing", 30 | "type": "valve", 31 | "valveType": "sprinkler", 32 | "name": "Sprinkler", 33 | "url": "http://192.168.10.35:1883", 34 | "topics": { 35 | "getActive": "test/sprinkler/getActive", 36 | "setActive": "test/sprinkler/setActive", 37 | "getInUse": "test/sprinkler/getInUse" 38 | }, 39 | "integerValue": true, 40 | "durationTimer": true, 41 | "minDuration": 1, 42 | "maxDuration": 30 43 | }, 44 | { 45 | "type": "television", 46 | "name": "Tasmota-TV", 47 | "url": "homebridge", 48 | "logMqtt": true, 49 | "accessory": "mqttthing", 50 | "topics": { 51 | "setActive": "cmnd/Remote/irsend", 52 | "setActiveInput": "cmnd/Remote/irsend" 53 | }, 54 | "inputs": [ 55 | { 56 | "name": "Input 1", 57 | "value": "3249178998" 58 | }, 59 | { 60 | "name": "Input 2", 61 | "value": "input-2-rf-code" 62 | }, 63 | { 64 | "name": "Input 3", 65 | "value": "input-3-rf-code" 66 | }, 67 | { 68 | "name": "Input 4", 69 | "value": "input-4-rf-code" 70 | } 71 | ], 72 | "codec": "json", 73 | "jsonCodec": { 74 | "fixed": { 75 | "Protocol": "NEC", 76 | "Bits": 32 77 | }, 78 | "properties": { 79 | "activeIdentifier": "Data", 80 | "active": "Data" 81 | } 82 | }, 83 | "onValue": "power-on-rf-code", 84 | "offValue": "power-off-rf-code" 85 | }, 86 | { 87 | "accessory": "mqttthing", 88 | "type": "switch", 89 | "name": "Toggle Switch", 90 | "url": "homebridge", 91 | "logMqtt": true, 92 | "topics": { 93 | "getOn": "test/toggle/get", 94 | "setOn": "test/toggle/set" 95 | }, 96 | "codec": "toggle.js" 97 | }, 98 | { 99 | "accessory": "mqttthing", 100 | "type": "doorbell", 101 | "name": "Door Bell", 102 | "url": "homebridge", 103 | "caption": "Front Door Bell", 104 | "topics": { 105 | "getSwitch": { 106 | "topic": "tele/RF/RESULT", 107 | "apply": "return JSON.parse( message ).RfReceived.Data;" 108 | } 109 | }, 110 | "switchValues": [ 111 | [ 112 | "F1FB61" 113 | ] 114 | ], 115 | "restrictSwitchValues": [ 116 | 0 117 | ], 118 | "logMqtt": true 119 | }, 120 | { 121 | "accessory": "mqttthing", 122 | "type": "lightbulb", 123 | "name": "Test RGB Light", 124 | "url": "http://192.168.10.35:1883", 125 | "topics": { 126 | "getRGB": "test/rgblight/get", 127 | "setRGB": "test/rgblight/set", 128 | "getOn": "test/rgblight/get", 129 | "setOn": "test/rgblight/set" 130 | }, 131 | "logMqtt": true, 132 | "integerValue": false, 133 | "codec": "json", 134 | "jsonCodec": { 135 | "properties": { 136 | "on": "state.power", 137 | "RGB": "state.rgb" 138 | }, 139 | "fixed": { 140 | "version": 1, 141 | "sender": "MQTT-Thing" 142 | } 143 | } 144 | }, 145 | { 146 | "accessory": "mqttthing", 147 | "type": "lightbulb-RGB", 148 | "name": "Winter-Light", 149 | "url": "homebridge", 150 | "topics": { 151 | "getRGB": { 152 | "topic": "light/get", 153 | "apply": "return message.r + ',' + message.g + ',' + message.b;" 154 | }, 155 | "setRGB": { 156 | "topic": "light/set", 157 | "apply": "let rgb = message.split(',').map( v => parseInt(v) ); return { r: rgb[0], g: rgb[1], b: rgb[2] };" 158 | }, 159 | "getOn": "light/get", 160 | "setOn": "light/set" 161 | }, 162 | "logMqtt": true, 163 | "codec": "json", 164 | "jsonCodec": { 165 | "properties": { 166 | "on": "state", 167 | "RGB": "color" 168 | }, 169 | "retain": true 170 | }, 171 | "onValue": "ON", 172 | "offValue": "OFF" 173 | }, 174 | { 175 | "type": "statelessProgrammableSwitch", 176 | "name": "Hall Entry", 177 | "url": "homebridge", 178 | "topics": { 179 | "getSwitch": "zigbee2mqtt/0x0017880104e6c096", 180 | "getBatteryLevel": "zigbee2mqtt/0x0017880104e6c096", 181 | "getStatusLowBattery": { 182 | "topic": "zigbee2mqtt/0x0017880104e6c096", 183 | "apply": "return message < 20;" 184 | } 185 | }, 186 | "switchValues": [ 187 | "on-press", 188 | "up-press", 189 | "down-press" 190 | ], 191 | "codec": "json", 192 | "jsonCodec": { 193 | "properties": { 194 | "switch": "action", 195 | "batteryLevel": "battery", 196 | "statusLowBattery": "battery" 197 | } 198 | }, 199 | "accessory": "mqttthing", 200 | "logMqtt": true 201 | }, 202 | { 203 | "accessory": "mqttthing", 204 | "type": "custom", 205 | "name": "Composite", 206 | "url": "homebridge", 207 | "logMqtt": true, 208 | "services": [ 209 | { 210 | "type": "switch", 211 | "name": "Switch 1", 212 | "topics": { 213 | "getOn": "home/get/switch1/POWER", 214 | "setOn": "home/set/switch1/POWER" 215 | }, 216 | "integerValue": true 217 | }, 218 | { 219 | "type": "switch", 220 | "name": "Switch 2", 221 | "topics": { 222 | "getOn": "home/get/switch2/POWER", 223 | "setOn": "home/set/switch2/POWER" 224 | }, 225 | "integerValue": true 226 | }, 227 | { 228 | "type": "motionSensor", 229 | "name": "My PIR", 230 | "topics": { 231 | "getMotionDetected": "home/get/pir/STATUS", 232 | "getStatusActive": "home/get/pir/ACTIVE", 233 | "getStatusFault": "home/get/pir/FAULT", 234 | "getStatusLowBattery": "home/get/pir/BATLOW" 235 | }, 236 | "onValue": "MOTION", 237 | "otherValueOff": true 238 | } 239 | ] 240 | }, 241 | { 242 | "accessory": "mqttthing", 243 | "type": "custom", 244 | "name": "Flower Care", 245 | "url": "homebridge", 246 | "logMqtt": true, 247 | "codec": "json", 248 | "jsonCodec": { 249 | "properties": { 250 | "currentRelativeHumidity": "moisture", 251 | "currentTemperature": "temperature", 252 | "currentAmbientLightLevel": "light", 253 | "statusLowBattery": "battery" 254 | } 255 | }, 256 | "services": [ 257 | { 258 | "type": "humiditySensor", 259 | "topics": { 260 | "getCurrentRelativeHumidity": "miflora/Limone", 261 | "getStatusLowBattery": { 262 | "topic": "miflora/Limone", 263 | "apply": "return message < 20;" 264 | } 265 | } 266 | }, 267 | { 268 | "type": "temperatureSensor", 269 | "topics": { 270 | "getCurrentTemperature": "miflora/Limone", 271 | "getStatusLowBattery": { 272 | "topic": "miflora/Limone", 273 | "apply": "return message < 20;" 274 | } 275 | } 276 | }, 277 | { 278 | "type": "lightSensor", 279 | "topics": { 280 | "getCurrentAmbientLightLevel": "miflora/Limone", 281 | "getStatusLowBattery": { 282 | "topic": "miflora/Limone", 283 | "apply": "return message < 20;" 284 | } 285 | } 286 | } 287 | ] 288 | }, 289 | { 290 | "accessory": "mqttthing", 291 | "type": "statelessProgrammableSwitch", 292 | "name": "4 Way Switch", 293 | "url": "homebridge", 294 | "logMqtt": true, 295 | "topics": { 296 | "getOnline": "zigbee2mqtt/bridge/state", 297 | "getSwitch": "zigbee2mqtt/0x04cf8cdf3c791ad9" 298 | }, 299 | "switchValues": [ 300 | "button_1_single", 301 | "button_1_double", 302 | "button_1_hold" 303 | ], 304 | "codec": "json", 305 | "jsonCodec": { 306 | "properties": { 307 | "switch": "action" 308 | } 309 | } 310 | }, 311 | { 312 | "accessory": "mqttthing", 313 | "type": "lightbulb-RGBWW", 314 | "name": "Test RGBWW Light", 315 | "url": "http://192.168.10.35:1883", 316 | "topics": { 317 | "getRGBWW": "test/rgbwwlight/rgb", 318 | "setRGBWW": "test/rgbwwlight/rgb/set" 319 | }, 320 | "whiteMix": false, 321 | "logMqtt": true 322 | }, 323 | { 324 | "accessory": "mqttthing", 325 | "type": "fan", 326 | "name": "Test Fan", 327 | "url": "http://192.168.10.35:1883", 328 | "logMqtt": true, 329 | "topics": { 330 | "getRotationDirection": "test/fan/getRotationDirection", 331 | "setRotationDirection": "test/fan/setRotationDirection", 332 | "getRotationSpeed": "test/fan/getRotationSpeed", 333 | "setRotationSpeed": "test/fan/setRotationSpeed" 334 | }, 335 | "integerValue": true, 336 | "mqttOptions": { 337 | "insecure": true 338 | } 339 | }, 340 | { 341 | "accessory": "mqttthing", 342 | "type": "battery", 343 | "name": "Test Battery", 344 | "url": "http://192.168.10.35:1883", 345 | "logMqtt": true, 346 | "topics": { 347 | "getBatteryLevel": "battery/level", 348 | "getStatusLowBattery": "battery/low" 349 | } 350 | }, 351 | { 352 | "type": "lightbulb-RGB", 353 | "name": "Color Light", 354 | "url": "http://192.168.10.35:1883", 355 | "topics": { 356 | "getOnline": "getOnline", 357 | "getOn": "getState", 358 | "setOn": "setState" 359 | }, 360 | "accessory": "mqttthing", 361 | "logMqtt": "true", 362 | "confirmationPeriodms": 2000 363 | }, 364 | { 365 | "type": "lightbulb-RGBWW", 366 | "name": "Test RGBWW", 367 | "url": "192.168.10.35", 368 | "logMqtt": true, 369 | "topics": { 370 | "setRGBWW": "rgbww/rgbww", 371 | "setOn": "rgbww/on" 372 | }, 373 | "whiteMix": true, 374 | "switchWhites": true, 375 | "accessory": "mqttthing" 376 | }, 377 | { 378 | "type": "lightbulb-ColTemp", 379 | "name": "Light-Temp", 380 | "url": "192.168.10.35", 381 | "logMqtt": true, 382 | "topics": { 383 | "setOn": "testTempLight/setOn", 384 | "setBrightness": "testTempLight/setBrightness", 385 | "setColorTemperature": "testTempLight/setColorTemperature" 386 | }, 387 | "whiteMix": true, 388 | "accessory": "mqttthing", 389 | "minColorTemperature": 130, 390 | "maxColorTemperature": 450 391 | } 392 | ] 393 | } 394 | -------------------------------------------------------------------------------- /docs/Configuration.md: -------------------------------------------------------------------------------- 1 | [![npm](https://badgen.net/npm/v/homebridge-mqttthing/latest)](https://www.npmjs.com/package/homebridge-mqttthing) 2 | [![npm](https://badgen.net/npm/dt/homebridge-mqttthing)](https://www.npmjs.com/package/homebridge-mqttthing) 3 | [![Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=discord)](https://discord.gg/MTpeMC) 4 | [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 5 | 6 | # Homebridge MQTT-Thing: Configuration 7 | 8 | ## Introduction 9 | 10 | Configure the plugin in your homebridge `config.json` file. Most configuration settings can now also be entered using 11 | [Homebridge Config UI X](https://www.npmjs.com/package/homebridge-config-ui-x). 12 | 13 | MQTT topics used fall into two categories: 14 | 15 | * Control topics, of the form `setXXX`, are published by MQTT-Thing in order to control device state (e.g. to turn on a light). 16 | * Status/notification topics, of the form `getXXX`, are published by the device to notify MQTT-Thing that something has occurred (e.g. that a sensor has detected something or a control topic action has been performed). 17 | 18 | **All values shown below (often within <>) are comments/descriptions, and should not be copied into your configuration file. For an example of an actual configuration file, please see `test/config.json`.** 19 | 20 | * [General Settings](#general-settings) 21 | * [MQTT Settings](#mqtt-settings) 22 | * [MQTT Topics](#mqtt-topics) 23 | * [Topic Apply Functions](#apply-functions) 24 | * [Boolean Value Settings](#boolean-value-settings) 25 | * [Accessory Information](#accessory-information) 26 | * [Publishing Values on Start-up](#publishing-values-on-start-up) 27 | * [History Service](#history-service) 28 | * [Confirmation](#confirmation) 29 | * [Codecs](#codecs) 30 | * [Accessories](#accessories) 31 | * [Grouped Accessories](#grouped-accessories) 32 | 33 | ## Common Settings 34 | 35 | The following settings apply to all device types: 36 | 37 | ```javascript 38 | { 39 | "accessory": "mqttthing", 40 | "type": "lightbulb", 41 | "name": "My lightbulb", 42 | "url": "http://192.168.1.235:1883", 43 | "username": "MQTT_username", 44 | "password": "MQTT_password", 45 | "mqttOptions": { "keepalive": 30 }, 46 | "mqttPubOptions": { "retain": true }, 47 | "logMqtt": true, 48 | "topics": { 49 | "getName": "my/get/name/topic", 50 | "getOnline": "my/get/online/topic", 51 | "getBatteryLevel": "my/get/battery-level/topic", 52 | "getChargingState": "my/get/battery-charging-state/topic", 53 | "getStatusLowBattery": "my/get/status-low-battery/topic" 54 | }, 55 | "integerValue": true, 56 | "onlineValue": "Online", 57 | "offlineValue": "Offline", 58 | "chargingStateValues": [ "NotCharging", "Charging", "NotChargeable" ], 59 | "startPub": [ 60 | { "topic": "topic1", "message": "message1" }, 61 | { "topic": "topic2", "message": "message2" } 62 | ], 63 | "confirmationPeriodms": 1000, 64 | "retryLimit": 5 65 | } 66 | ``` 67 | 68 | ### General Settings 69 | 70 | `accessory` - must always be set to the value `mqttthing` in order to use **homebridge-mqttthing** 71 | 72 | `type` - one of the supported accessory types listed below 73 | 74 | `name` - name of accessory, as displayed in HomeKit 75 | 76 | `caption` - HomeKit caption/label (optional) 77 | 78 | ### MQTT Settings 79 | 80 | `url` - URL of MQTT server if not localhost port 1883 (optional) 81 | 82 | `username` - Username for MQTT server (optional) 83 | 84 | `password` - Password for MQTT server (optional) 85 | 86 | `mqttOptions` - Object containing all MQTT options passed to https://www.npmjs.com/package/mqtt#client, for MQTT configuration settings not supported above (optional). Any standard settings *not* specified in an **mqttOptions** option will be set by homebridge-mqttthing. Enable MQTT logging with **logMqtt** to check the options provided. 87 | 88 | When MQTTS (MQTT over TLS) is used, the `mqttOptions` object is passed through to tls.connect() so the options described at https://nodejs.org/api/tls.html#tls_tls_connect_options_callback may be used. `keyfile`, `certfile` and `cafile` may be used to read key, cert and ca pem files respectively. Set `insecure` to true to disable all server identity checking (e.g. when using a self-signed certificate). 89 | 90 | `mqttPubOptions` - Option containing any MQTT publishing options required. See https://www.npmjs.com/package/mqtt#publish for details. 91 | 92 | `logMqtt` - Set to true to enable MQTT logging for this accessory (optional, defaults to false) 93 | 94 | ### MQTT Topics 95 | 96 | MQTT Topics are configured within a `topics` object. Most topics are optional (including all of the topics described in this section). 97 | 98 | `getName` - Topic that may be published to send HomeKit the name of the accessory (optional). HomeKit doesn't show name changes dynamically, so it's generally simpler just to configure the name with `name`. 99 | 100 | `getOnline` - Topic that may be published to tell homebridge-mqttthing whether or not the accessory is online (optional). This is a Boolean value (see below) intended to be published as false by the MQTT Last Will and Testament (LWT) feature in order to notify homebridge-mqttthing that the accessory is offline. Accessories using this feature must also publish an online true status when available. The "not responding" state is not updated immediately. This feature is a little hack... but there is no other possibility to achieve anything similar. It will be visible after closing and reopening the App or sometimes by switching the room view. The trick is to respond to a get request from the App with an exception. But this is only possible when there is a get request. 101 | 102 | `getBatteryLevel` - Topic that may be published by an accessory to indicate its current battery level, from 0 to 100 (optional). 103 | 104 | `getChargingState` - Topic that may be published by an accessory to indicate its charging state (optional). Default values accepted are `[ "NOT_CHARGING", "CHARGING", "NOT_CHARGEABLE" ]`. These may be changed with the `chargingStateValues` setting. 105 | 106 | `getStatusLowBattery` - Topic that may be published by an accessory to indicate whether it has a low battery (optional). 107 | 108 | ### Apply Functions 109 | 110 | User functions may be applied to MQTT messages for custom payload encoding/decoding. Apply functions do this within the main configuration file, but are not supported by config-ui-x. Alternatively, an external codec may be used (see [Codecs](#codecs)). 111 | 112 | If an MQTT message is not a simple value or does not match the expected syntax, it is possible to specify a JavaScript function that is called for the message every time it is received/published. For this, the topic string in the configuration can be replaced with an object with these properties: 113 | 114 | `topic` - Topic string 115 | 116 | `apply` - Javascript function to apply (must be a complete function body that `return`s a value). The function is called with one arguments: `message`, holding the original message, and `state` (optional). 117 | 118 | e.g. Decoding a JSON payload: 119 | ```javascript 120 | "topics": { 121 | "getCurrentTemperature": { 122 | "topic": "outdoor", 123 | "apply": "return JSON.parse(message).temperature.toFixed(1);" 124 | } 125 | } 126 | ``` 127 | 128 | e.g. Scaling brightness from its internal 0-100 range to an external 0-255 range: 129 | ```javascript 130 | "getBrightness": { 131 | "topic": "test/lightbulb/getBrightness", 132 | "apply": "return Math.round( message / 2.55 );" 133 | }, 134 | "setBrightness": { 135 | "topic": "test/lightbulb/setBrightness", 136 | "apply": "return Math.round( message * 2.55 );" 137 | } 138 | ``` 139 | 140 | The `state` parameter holds an object which may be used to store local state used by the apply function. Additionally, `state.global` points at an object shared between all topics. 141 | 142 | This functionality is not currently available when editing MQTT topics using config-ui-x. 143 | 144 | ### Boolean Value Settings 145 | 146 | Homekit Boolean types like on/off use strings "true" and "false" in MQTT messages unless `"integerValue": true` is configured, in which case they use to "1" and "0". Alternatively, specific values can be configured using `onValue` and `offValue` (in which case `integerValue` is ignored). Other Homekit types (integer, string, etc.) are not affected by these settings. 147 | 148 | `integerValue` - set to **true** to use the values **1** and **0** to represent Boolean values instead of the strings **"true"** and **"false"** (optional, defaults to false) 149 | 150 | `onValue` - configure a specific Boolean true or *on* value (optional) 151 | 152 | `offValue` - configure a specific Boolean false or *off* value (optional) 153 | 154 | When `onValue` and `offValue` are configured, by default any other value received on the _get_ topic will be ignored. To treat unrecognized received values as off, set `otherValueOff: true`. 155 | 156 | `onlineValue`, `offlineValue` - configure specific values representing that an accessory is online or offline (received through `getOnline`). If not specified, the configured *on* and *off* values will be used to represent online and offline states (i.e. `onValue`/`offValue` if configured, otherwise **1** / **0** with `integerValue: true` or **true** / **false** with `integerValue: false`). 157 | 158 | In mqttthing versions before 1.0.23, receiving any value not matching the configured 'on value' for a Boolean characteristic turned it off. From 1.0.23, the received message must match the offValue to turn off a characteristic. 159 | To turn off on any value except the onValue, omit configuration of offValue. 160 | 161 | From version 1.0.23, mqttthing will not publish a message for a Boolean characteristic turning off if no offValue is configured. 162 | 163 | ### Accessory Information 164 | 165 | The following configuration settings may be specified if required to change information service content: 166 | 167 | `manufacturer` - sets the manufacturer name (defaults to *mqttthing*) 168 | 169 | `serialNumber` - sets the serial number (defaults to hostname and accessory name) 170 | 171 | `model` - sets the model name (defaults to the mqttthing accessory type) 172 | 173 | `firmwareRevision` - sets the firmware revision number (defaults to mqttthing version) 174 | 175 | ### Publishing Values on Start-up 176 | 177 | MQTT messages may be published on start-up, e.g. to reset accessories to a known initial state, with `startPub`. This should contain an array of objects with `topic` and `message` 178 | keys, i.e.: 179 | ```javascript 180 | "startPub": [ 181 | { "topic": "test/lightbulb/setOn", "message": "1" }, 182 | { "topic": "test/lightbulb/getOn", "message": "1" } 183 | ] 184 | ``` 185 | 186 | Previously this was an object containing MQTT topics as keys, and values to be published as values. This format will still work but the format above is preferred. 187 | 188 | ### History Service 189 | 190 | For some accessory types you can enable the History Service powered by [fakegato-history](https://github.com/simont77/fakegato-history). It will show up in the Eve App. (Home.app does not support it). 191 | 192 | Depending on the accessory type, fakegato-history may add extra entries every 10 minutes or may average the entries from the plugin and send data every 10 minutes. 193 | 194 | History is currently supported for: 195 | * Temperature Sensor 196 | * Humidity Sensor 197 | * Air Pressure Sensor 198 | * Air Quality Sensor 199 | * Motion Sensor 200 | * Contact Sensor 201 | * Outlet (power consumption) 202 | * Switch 203 | 204 | `history` - set to **true** for enabling History Service (Boolean, optional) 205 | 206 | History options may be specified in a `historyOptions` object containing one or more of the following properties: 207 | 208 | `size` - maximum size of stored data points (optional), default: 4032 209 | 210 | `noAutoTimer` - enable/disable averaging (and repeating) 10min timer (optional). Set to true to disable auto-timer. 211 | 212 | `noAutoRepeat` - enable/disable repetition of last value if no data was received in last 10min interval (optional). Set to true to disable auto-repeat. 213 | 214 | `mergeInterval` - set merge interval [minutes] for events, which are very close in time (optional, for motion sensor only, not in combination with autoTimer/autoRepeat), default: 0 215 | 216 | `persistencePath` - full path of directory in which to store history data (defaults to homebridge user storage path) 217 | 218 | Avoid the use of "/" in characteristics of the Information Service (e.g. serial number, manufacturer, etc.), since this may cause data to not appear in the history. Note that if your Eve.app is controlling more than one accessory for each type, the serial number should be unique, otherwise Eve.app will merge the histories. 219 | 220 | ### Confirmation 221 | 222 | Some accessories support confirmation for some of their 'set' topics. When enabled by configuring `confirmationPeriodms`, the accessory *must* echo anything sent to appropriate `setX` subject(s) to the corresponding `getX` subject(s). Where homebridge-mqttthing doesn't see a confirmation within the configured configuration period (specified in milliseconds), it will publish the set message again. Messages will be republished up to 3 times by default, but this can be changed by also specifying `retryLimit`. 223 | 224 | Accessories supporting message confirmation list the topics supporting message confirmation below. 225 | 226 | Mqttthing can optionally set an accessory as 'offline' when it doesn't receive confirmation messages. By default it does this if a `getOnline` topic hasn't been configured - i.e. if online state isn't already being managed explicitly. However, this behaviour can be overridden. Set `confirmationIndicateOffline` to `true` to indicate offline ('No Response') even when a `getOnline` topic is configured, or set `confirmationIndicateOffline` to `false` to disable offline indication when there is no response. 227 | 228 | ### Codecs 229 | 230 | Rather like [apply functions](#apply-functions), a codec can be used to apply transformations to incoming and outgoing data. Unlike apply functions, a codec is written 231 | in a separate JavaScript file which is referenced by the configuration. 232 | 233 | For further details, please see [Codecs.md](Codecs.md). 234 | 235 | ## Accessories 236 | 237 | For configuration details of supported accessories, please see [Accessories.md](Accessories.md). 238 | 239 | ## Grouped Accessories 240 | 241 | The services provided by multiple accessories may be grouped together by creating an accessory of type `custom` containing multiple service definitions in a `services` array. For example, 242 | the following configuration groups together two switches and a motion sensor: 243 | 244 | ```json 245 | { 246 | "accessory": "mqttthing", 247 | "type": "custom", 248 | "name": "Composite", 249 | "url": "mqttserver", 250 | "logMqtt": true, 251 | "integerValue": true, 252 | "services": [ 253 | { 254 | "type": "switch", 255 | "name": "Switch 1", 256 | "topics": { 257 | "getOn": "home/get/switch1/POWER", 258 | "setOn": "home/set/switch1/POWER" 259 | } 260 | }, 261 | { 262 | "type": "switch", 263 | "name": "Switch 2", 264 | "topics": { 265 | "getOn": "home/get/switch2/POWER", 266 | "setOn": "home/set/switch2/POWER" 267 | } 268 | }, 269 | { 270 | "type": "motionSensor", 271 | "name": "My PIR", 272 | "topics": { 273 | "getMotionDetected": "home/get/pir/STATUS", 274 | "getStatusActive": "home/get/pir/ACTIVE", 275 | "getStatusFault": "home/get/pir/FAULT", 276 | "getStatusLowBattery": "home/get/pir/BATLOW" 277 | } 278 | } 279 | ] 280 | } 281 | ``` 282 | 283 | Any settings which apply to all services may be defined within the custom-type accessory, e.g. `integerValue` above. Settings specified within individual services will override any defaults specified at the custom accessory level. 284 | 285 | Custom accessories are only intended for use with simple services, not with accessories like 'weather station' which already combine multiple services. 286 | 287 | Custom accessories cannot be configured through Config UI X. -------------------------------------------------------------------------------- /docs/Codecs.md: -------------------------------------------------------------------------------- 1 | [![npm](https://badgen.net/npm/v/homebridge-mqttthing/latest)](https://www.npmjs.com/package/homebridge-mqttthing) 2 | [![npm](https://badgen.net/npm/dt/homebridge-mqttthing)](https://www.npmjs.com/package/homebridge-mqttthing) 3 | [![Discord](https://img.shields.io/discord/432663330281226270?color=728ED5&logo=discord&label=discord)](https://discord.gg/MTpeMC) 4 | [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) 5 | 6 | # Homebridge MQTT-Thing: Codecs 7 | 8 | ## Introduction 9 | 10 | A codec can be used to apply transformations to incoming and outgoing data. Unlike apply functions, a codec is written 11 | in a separate JavaScript file which is referenced by the configuration. 12 | 13 | To use a codec, configure the path to its JavaScript file using the `codec` configuration setting. The codec will then be called to encode data before 14 | publishing and to decode received data for all configured topics. The codec can decide which topics and properties to process, and can suppress messages 15 | and generate additional messages as required. 16 | 17 | * [Structure](#structure) 18 | * [Function Reference](#function-reference) 19 | * [Properties](#properties) 20 | * [Example Codecs](#examples) 21 | * [Built-in Codecs](#built-in-codecs) 22 | * [JSON Codec](#json-codec-json) 23 | 24 | ## Structure 25 | 26 | A codec is a Node.js module which makes encode() and decode() functions available, which are called for 27 | all properties or specific properties of the configured accessory. Codecs must implement a single function, `init()`, exported through `module.exports`. For example, here is a minimal codec implementation which does nothing: 28 | 29 | ```javascript 30 | function init() { 31 | 32 | function encode( message ) { 33 | return message; // no-op 34 | } 35 | 36 | function decode( message ) { 37 | return message; // no-op 38 | } 39 | 40 | // return encode and decode functions 41 | return { 42 | encode, 43 | decode 44 | }; 45 | } 46 | 47 | module.exports = { 48 | init 49 | }; 50 | ``` 51 | 52 | This could also be written more concisely as: 53 | 54 | ```javascript 55 | module.exports = { 56 | init: function() { 57 | return { 58 | encode( message ) { 59 | return message; 60 | }, 61 | decode( message ) { 62 | return message; 63 | } 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ### Local State 70 | 71 | A codec that is used by multiple accessories will only be loaded once, so any accessory-specific state must be stored within the `init()` function. The choice to return `encode()` and `decode()` functions from `init()` (as opposed to exporting them directly) is intended to make this easier. 72 | 73 | ## Function Reference 74 | 75 | ### `init( params )` 76 | 77 | The `init()` function is passed a single object containing initialisation parameters for the accessory. 78 | 79 | * `params.log` can be used to write to Homebridge's log. 80 | * `params.config` is the accessory's configuration (as configured in `config.json`). This gives the codec access to the standard configuration settings, and lets it use its own if required. 81 | * `params.publish` may be used to publish to MQTT directly 82 | * `params.notify` may be used to send MQTT-Thing a property notification 83 | 84 | The `init()` function must return an object containing `encode()` and `decode()` functions (as described below). This can be just single `encode()` and `decode()` functions for all properties as above. More commonly a properties map containing property-specific functions is used, as follows: 85 | 86 | ```javascript 87 | function init() { 88 | return { 89 | properties: { 90 | targetProp1: { 91 | encode: encodeFunction1, 92 | decode: decodeFunction1 93 | }, 94 | targetProp2: { 95 | encode: encodeFunction2 96 | }, 97 | }, 98 | encode: defaultEncodeFunction, 99 | decode: defaultDecodeFunction 100 | } 101 | } 102 | ``` 103 | 104 | The allows different encoding/decoding logic for each property. The default `encode()`/`decode()` functions are called for properties for which no property-specific function is defined. 105 | 106 | ### `encode( message, info, output )` 107 | 108 | The `encode()` function is called to encode a message before publishing it to MQTT. It is passed three parameters: 109 | 110 | * `message` is the message to be encoded 111 | * `info` is an object holding: 112 | * `info.topic` - the MQTT topic to be published 113 | * `info.property` - the property associated with the publishing operation 114 | * `output` is a function which may be called to deliver the encoded value asynchronously 115 | 116 | The `encode()` function may either return the encoded message, or it may deliver it asynchronously by passing it as a parameter to the provided `output` function. It if does neither, no value will be published. 117 | 118 | ### `decode( message, info, output )` 119 | 120 | The `decode`() function is called to decode a message received from MQTT before passing it for processing by MQTT-Thing. It takes three parameters: 121 | 122 | * `message` is the message to be decoded 123 | * `info` is an object holding: 124 | * `info.topic` - the MQTT topic received 125 | * `info.property` the property associated with the received message 126 | * `output` is a function which may be called to deliver the decoded value asynchronously 127 | 128 | The `decode()` function may either return the decoded message, or it may deliver it asynchronously by passing it as a parameter to the provided `output` function. If it does neither, no notification will be passed on to MQTT-Thing. 129 | 130 | ### `publish( topic, message )` 131 | 132 | The `publish()` function provided in `init()`'s `params` may be used to publish a message directly to MQTT. 133 | 134 | * `topic` is the MQTT topic to publish 135 | * `message` is the message to publish to MQTT 136 | 137 | The message is published directly to MQTT, ignoring any apply function usually with the topic and not passing through the Codec's `encode()` function. 138 | 139 | ### `notify( property, message )` 140 | 141 | The `notify()` function provided in `init()`'s `params` may be used to notify MQTT-Thing of the new value for a property. This will deliver the notification to all internal subscribers to the property. Note that generally a corresponding MQTT 'get' topic must have been configured in order for internal subscribers to exist. 142 | 143 | * `property` is the MQTT-Thing property to update 144 | * `message` is the value to be passed to MQTT-Thing 145 | 146 | The message is passed directly to MQTT-Thing. It does not pass through any apply function or through the Codec's `decode()` function. 147 | 148 | ## Properties 149 | 150 | This section lists the properties available for each accessory type. All accessories may also support `batteryLevel`, `chargingState` and `statusLowBattery`. 151 | 152 | ### Air Pressure Sensor 153 | 154 | `airPressure`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery` 155 | 156 | ### Air Quality Sensor 157 | 158 | `airQuality`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery`, `carbonDioxideLevel`, `pm10density`, `pm2_5density`, `ozonedensity`, `nitrogenDioxideDensity`, `sulphurDioxideDensity`, `VOCDensity`, `carbonMonoxideLevel`, `airQualityPPM`, `currentTemperature`, `currentRelativeHumidity` 159 | 160 | ### Carbon Dioxide Sensor 161 | 162 | `carbonDioxideDetected`, `carbonDioxideLevel`, `carbonDioxidePeakLevel`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery` 163 | 164 | ### Contact Sensor 165 | 166 | `contactSensorState`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery` 167 | 168 | ### Doorbell 169 | 170 | `switch`, `brightness`, `volume`, `motionDetected` 171 | 172 | ### Fan 173 | 174 | `on`, `rotationDirection`, `rotationSpeed` 175 | 176 | ### Garage door opener 177 | 178 | `targetDoorState`, `currentDoorState`, `doorMoving`, `obstructionDetected`, `lockTargetState`, `lockCurrentState` 179 | 180 | ### Heater Cooler 181 | 182 | `active`, `currentHeaterCoolerState`, `targetHeaterCoolerState`, `currentTemperature`, `lockPhysicalControls`, `swingMode`, `coolingThresholdTemperature`, `heatingThresholdTemperature`, `temperatureDisplayUnits`, `rotationSpeed` 183 | 184 | ### Humidity Sensor 185 | 186 | `currentRelativeHumidity`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery` 187 | 188 | ### Leak Sensor 189 | 190 | `leakDetected`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery` 191 | 192 | ### Light bulb 193 | 194 | `on`, `brightness`, `hue`, `saturation`, `colorTemperature`, `white`, `HSV`, `RGB`, `RGBW`, `RGBWW` 195 | 196 | ### Light Sensor 197 | 198 | `currentAmbientLightLevel`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery` 199 | 200 | ### Lock Mechanism 201 | 202 | `lockTargetState`, `lockCurrentState` 203 | 204 | ### Microphone 205 | 206 | `mute`, `volume` 207 | 208 | ### Motion Sensor 209 | 210 | `motionDetected`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery` 211 | 212 | ### Occupancy Sensor 213 | 214 | `occupancyDetected`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery` 215 | 216 | ### Outlet 217 | 218 | `on`, `outletInUse`, `currentConsumption`, `voltage`, `electricCurrent`, `totalConsumption` 219 | 220 | ### Security System 221 | 222 | `targetState`, `currentState`, `statusFault`, `statusTampered` 223 | 224 | ### Speaker 225 | 226 | `mute`, `volume` 227 | 228 | ### StatelessProgrammableSwitch 229 | 230 | `switch`, `switch0`, `switch1`, `switch2`, ... 231 | 232 | ### Switch 233 | 234 | `on` 235 | 236 | ### Television 237 | 238 | `active`, `input`XX 239 | 240 | ### Temperature Sensor 241 | 242 | `currentTemperature`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery` 243 | 244 | ### Thermostat 245 | 246 | `currentHeatingCoolingState`, `targetHeatingCoolingState`, `currentTemperature`, `targetTemperature`, `temperatureDisplayUnits`, `currentRelativeHumidity`, `targetRelativeHumidity`, `coolingThresholdTemperature`, `heatingThresholdTemperature` 247 | 248 | ### Valve (Sprinkler, Shower, Faucet) 249 | 250 | `active`, `inUse`, `setDuration`, `remainingDuration` 251 | 252 | ### Weather Station 253 | 254 | `currentTemperature`, `statusActive`, `statusFault`, `statusTampered`, `statusLowBattery`, `currentRelativeHumidity`, `airPressure`, `weatherCondition`, `rain1h`, `rain24h`, `uvIndex`, `visibility`, `windDirection`, `windSpeed` 255 | 256 | ### Window 257 | 258 | `currentPosition`, `targetPosition`, `positionState`, `holdPosition`, `obstructionDetected` 259 | 260 | ### Window Covering (Blinds) 261 | 262 | `currentPosition`, `targetPosition`, `positionState`, `holdPosition`, `obstructionDetected`, `targetHorizontalTiltAngle`, `currentHorizontalTiltAngle`, `targetVerticalTiltAngle`, `currentVerticalTiltAngle` 263 | 264 | ## Examples 265 | 266 | When writing a codec, you may find it helpful to start with the no-op implementation in [`test/empty-codec.js`](../test/empty-codec.js). 267 | 268 | Test examples of codec capabilities can be found in [`test/test-codec.js`](../test/test-codec.js). 269 | 270 | ### Toggle switch 271 | 272 | This codec can be used to toggle the state of a switch whenever it receives any message. 273 | 274 | #### Accessory (in *config.json*) 275 | 276 | ```json 277 | { 278 | "accessory": "mqttthing", 279 | "type": "switch", 280 | "name": "Toggle Switch", 281 | "url": "mqtt-url", 282 | "logMqtt": true, 283 | "topics": { 284 | "getOn": "test/toggle/get", 285 | "setOn": "test/toggle/set" 286 | }, 287 | "codec": "toggle.js" 288 | } 289 | ``` 290 | 291 | #### Codec file (*toggle.js*) 292 | 293 | ```javascript 294 | /** 295 | * Test 'toggle' codec - toggles switch on receipt of any message 296 | * toggle.js 297 | */ 298 | 299 | 'use strict' 300 | 301 | module.exports = { 302 | init: function() { 303 | let state = false; 304 | return { 305 | properties: { 306 | on: { 307 | decode: function() { 308 | state = ! state; 309 | return state; 310 | }, 311 | encode: function( msg ) { 312 | state = msg; 313 | return msg; 314 | } 315 | } 316 | } 317 | }; 318 | } 319 | }; 320 | ``` 321 | 322 | Codec state is declared within _init()_. The codec targets only the **on** property, toggling its state whenever it receives a message and recording the current state when publishing. 323 | 324 | ### Keep-alive Codec 325 | 326 | Codecs can be used to run arbitrary JavaScript code from within MQTT-Thing, not necessarily related to encoding/decoding messages. For example, the codec below sends a 'keep-alive' message at regular intervals. The message and send interval can be configured within the accessory configuration, in `keepAliveMessage` and `keepAliveInterval` respectively. 327 | 328 | ```javascript 329 | /** 330 | * Test/Demo Homebridge-MQTTThing Codec (encoder/decoder) 331 | * Codecs allow custom logic to be applied to accessories in mqttthing, rather like apply() functions, 332 | * but in the convenience of a stand-alone JavaScript file. 333 | * 334 | * keep-alive-codec.js - sends keep-alive message at configurable interval 335 | */ 336 | 337 | 'use strict'; 338 | 339 | module.exports = { 340 | init: function( params ) { 341 | let { config, publish } = params; 342 | 343 | // publish keep-alive topic at regular interval 344 | if( config.keepAliveTopic ) { 345 | let keepAlivePeriod = config.keepAlivePeriod || 60; 346 | let keepAliveMessage = config.keepAliveMessage || ''; 347 | 348 | setInterval( () => { 349 | publish( config.keepAliveTopic, keepAliveMessage ); 350 | }, keepAlivePeriod * 1000 ); 351 | } 352 | 353 | // no encode/decode in this codec 354 | return {}; 355 | } 356 | }; 357 | ``` 358 | 359 | ## Built-in Codecs 360 | 361 | Built-in Codecs are provided with MQTT-Thing, and can be referenced without a path or `.js` suffix. For example, to load the JSON Codec, use `"codec": "json"`. 362 | 363 | ### JSON Codec (json) 364 | 365 | The JSON Codec aims to make JSON encoding and decoding, often implemented with _apply()_ functions, easier to configure. If is intended for accessories which encode multiple properties as a JSON object sent over a single topic, instead of sending separate parameters in separate topics. 366 | 367 | Mapping to and from JSON is configured using a jsonCodec object in the accessory configuration: 368 | 369 | ```json 370 | "jsonCodec": { 371 | "properties": { 372 | "on": "state.power", 373 | "RGB": "state.rgb" 374 | }, 375 | "fixed": { "fixed properties": "object (global/default)" }, 376 | "fixedByTopic": { 377 | "topic1": { "fixed properties": "object for topic1", 378 | "topic2": { "fixed properties": "object for topic2" } 379 | }, 380 | "retain": true|false 381 | } 382 | ``` 383 | 384 | The `jsonCodec` configuration object should contain a `properties` object containing strings indicating the JSON location of each property (as listed in [Properties](#properties), above). 385 | 386 | By default, the JSON codec only publishes properties which have updated. To collect all published properties for each published topic, set `"retain": true`. 387 | 388 | Fixed values (published with every message) may be specified in a `fixed` object. If multiple topics are published which should have different fixed values, these may be specified through `fixedByTopic`. Fixed values are not required in received messages; only mapped properties are extracted. 389 | 390 | For example, the following accessory configuration: 391 | 392 | ```json 393 | { 394 | "accessory": "mqttthing", 395 | "type": "lightbulb", 396 | "name": "Test RGB Light", 397 | "url": "http://192.168.10.35:1883", 398 | "topics": { 399 | "getRGB": "test/rgblight/get", 400 | "setRGB": "test/rgblight/set", 401 | "getOn": "test/rgblight/get", 402 | "setOn": "test/rgblight/set" 403 | }, 404 | "logMqtt": true, 405 | "integerValue": false, 406 | "codec": "json", 407 | "jsonCodec": { 408 | "properties": { 409 | "on": "state.power", 410 | "RGB": "state.rgb" 411 | }, 412 | "fixed": { 413 | "version": 1, 414 | "sender": "MQTT-Thing" 415 | } 416 | } 417 | } 418 | ``` 419 | 420 | ... sends and receives messages like: 421 | 422 | `{"version":1,"sender":"MQTT-Thing","state":{"power":true,"rgb":"125,82,255"}}` 423 | -------------------------------------------------------------------------------- /libs/mqttlib.js: -------------------------------------------------------------------------------- 1 | // MQTT Thing Accessory plugin for Homebridge 2 | // MQTT Library 3 | 4 | 'use strict'; // eslint-disable-line 5 | 6 | const mqtt = require( "mqtt" ); 7 | const path = require( "path" ); 8 | const fs = require("fs"); 9 | 10 | var mqttlib = new function() { 11 | 12 | function makeCodecPath( codec, homebridgePath ) { 13 | let codecPath = codec; 14 | // if it doesn't start with a '/' (i.e. not fully-qualified)... 15 | if( codecPath[ 0 ] != '/' ) { 16 | if( codecPath.substr( codecPath.length - 3 ) !== '.js' ) { 17 | // no js extension - assume it's an internal codec 18 | codecPath = path.join( __dirname, '../codecs/', codecPath + '.js' ); 19 | } else { 20 | // relative external codec is relative to homebridge userdata 21 | codecPath = path.join( homebridgePath, codecPath ); 22 | } 23 | } 24 | return codecPath; 25 | } 26 | 27 | //! Initialise MQTT. Requires context ( { log, config } ). 28 | //! Context populated with mqttClient and mqttDispatch. 29 | this.init = function( ctx ) { 30 | // MQTT message dispatch 31 | let mqttDispatch = ctx.mqttDispatch = {}; // map of topic to [ function( topic, message ) ] to handle 32 | let propDispatch = ctx.propDispatch = {}; // map of proerty to [ rawhandler( topic, message ) ] 33 | 34 | let { config, log } = ctx; 35 | let logmqtt = config.logMqtt; 36 | var clientId = 'mqttthing_' + config.name.replace(/[^\x20-\x7F]/g, "") + '_' + Math.random().toString(16).substr(2, 8); 37 | 38 | // start with any configured options object 39 | var options = config.mqttOptions || {}; 40 | 41 | // standard options set by mqtt-thing 42 | var myOptions = { 43 | keepalive: 10, 44 | clientId: clientId, 45 | protocolId: 'MQTT', 46 | protocolVersion: 4, 47 | clean: true, 48 | reconnectPeriod: 1000, 49 | connectTimeout: 30 * 1000, 50 | will: { 51 | topic: 'WillMsg', 52 | payload: 'mqtt-thing [' + ctx.config.name + '] has stopped', 53 | qos: 0, 54 | retain: false 55 | }, 56 | username: config.username, 57 | password: config.password, 58 | rejectUnauthorized: false 59 | }; 60 | 61 | // copy standard options into options unless already set by user 62 | for( var opt in myOptions ) { 63 | if( myOptions.hasOwnProperty( opt ) && ! options.hasOwnProperty( opt ) ) { 64 | options[ opt ] = myOptions[ opt ]; 65 | } 66 | } 67 | 68 | // load ca/cert/key files 69 | if( options.cafile ) { 70 | options.ca = fs.readFileSync( options.cafile ); 71 | } 72 | if( options.certfile ) { 73 | options.cert = fs.readFileSync( options.certfile ); 74 | } 75 | if( options.keyfile ) { 76 | options.key = fs.readFileSync( options.keyfile ); 77 | } 78 | 79 | // insecure 80 | if( options.insecure ) { 81 | options.checkServerIdentity = function( /* servername, cert */ ) { 82 | return undefined; /* servername and certificate are verified */ 83 | }; 84 | } 85 | 86 | // add protocol to url string, if not yet available 87 | let brokerUrl = config.url; 88 | if( brokerUrl && ! brokerUrl.includes( '://' ) ) { 89 | brokerUrl = 'mqtt://' + brokerUrl; 90 | } 91 | 92 | // log MQTT settings 93 | if( logmqtt ) { 94 | log( 'MQTT URL: ' + brokerUrl ); 95 | log( 'MQTT options: ' + JSON.stringify( options, function( k, v ) { 96 | if( k == "password" ) { 97 | return undefined; // filter out 98 | } 99 | return v; 100 | } ) ); 101 | } 102 | 103 | // create MQTT client 104 | var mqttClient = mqtt.connect(brokerUrl, options); 105 | mqttClient.on('error', function (err) { 106 | log('MQTT Error: ' + err); 107 | }); 108 | 109 | mqttClient.on('message', function (topic, message) { 110 | if (logmqtt) { 111 | log("Received MQTT: " + topic + " = " + message); 112 | } 113 | let handlers = mqttDispatch[topic]; 114 | if (handlers) { 115 | for( let i = 0; i < handlers.length; i++ ) { 116 | handlers[ i ]( topic, message ); 117 | } 118 | } else { 119 | log('Warning: No MQTT dispatch handler for topic [' + topic + ']'); 120 | } 121 | }); 122 | 123 | // Load any codec 124 | if( config.codec ) { 125 | let codecPath = makeCodecPath( config.codec, ctx.homebridgePath ); 126 | if( fs.existsSync( codecPath ) ) { 127 | // load codec 128 | log( 'Loading codec from ' + codecPath ); 129 | let codecMod = require( codecPath ); 130 | if( typeof codecMod.init === "function" ) { 131 | 132 | // direct publishing 133 | let directPub = function( topic, message ) { 134 | if( config.logMqtt ) { 135 | log( 'Publishing MQTT: ' + topic + ' = ' + message ); 136 | } 137 | mqttClient.publish( topic, message.toString(), config.mqttPubOptions ); 138 | }; 139 | 140 | // notification by property 141 | let notifyByProp = function( property, message ) { 142 | let handlers = propDispatch[ property ]; 143 | if( handlers ) { 144 | for( let i = 0; i < handlers.length; i++ ) { 145 | handlers[ i ]( '_prop-' + property, message ); 146 | } 147 | } 148 | }; 149 | 150 | // initialise codec 151 | let codec = ctx.codec = codecMod.init( { log, config, publish: directPub, notify: notifyByProp } ); 152 | if( codec ) { 153 | // encode/decode must be functions 154 | if( typeof codec.encode !== "function" ) { 155 | log.warn( 'No codec encode() function' ); 156 | codec.encode = null; 157 | } 158 | if( typeof codec.decode !== "function" ) { 159 | log.warn( 'No codec decode() function' ); 160 | codec.decode = null; 161 | } 162 | } 163 | } else { 164 | // no initialisation function 165 | log.error( 'ERROR: No codec initialisation function returned from ' + codecPath ); 166 | } 167 | } else { 168 | log.error( 'ERROR: Codec file [' + codecPath + '] does not exist' ); 169 | } 170 | } 171 | 172 | ctx.mqttClient = mqttClient; 173 | return mqttClient; 174 | }; 175 | 176 | function getApplyState( ctx, property ) { 177 | if( ! ctx.hasOwnProperty( 'applyState' ) ) { 178 | ctx.applyState = { props: {}, global: {} }; 179 | } 180 | if( ! ctx.applyState.props.hasOwnProperty( property ) ) { 181 | ctx.applyState.props[ property ] = { global: ctx.applyState.global }; 182 | } 183 | return ctx.applyState.props[ property ]; 184 | } 185 | 186 | function getCodecFunction( codec, property, functionName ) { 187 | if( codec ) { 188 | let fn; 189 | if( codec.properties && codec.properties[ property ] ) { 190 | fn = codec.properties[ property ][ functionName ]; 191 | } 192 | if( fn === undefined ) { 193 | fn = codec[ functionName ]; 194 | } 195 | return fn; 196 | } 197 | } 198 | 199 | // Subscribe 200 | this.subscribe = function( ctx, topic, property, handler ) { 201 | let rawHandler = handler; 202 | let { mqttDispatch, log, mqttClient, codec, propDispatch } = ctx; 203 | if( ! mqttClient ) { 204 | log( 'ERROR: Call mqttlib.init() before mqttlib.subscribe()' ); 205 | return; 206 | } 207 | 208 | // send through any apply function 209 | if (typeof topic != 'string') { 210 | let extendedTopic = topic; 211 | topic = extendedTopic.topic; 212 | if (extendedTopic.hasOwnProperty('apply')) { 213 | let previous = handler; 214 | let applyFn = Function( "message", "state", extendedTopic['apply'] ); //eslint-disable-line 215 | handler = function (intopic, message) { 216 | let decoded; 217 | try { 218 | decoded = applyFn( message, getApplyState( ctx, property ) ); 219 | } catch( ex ) { 220 | log( 'Decode function apply( message) { ' + extendedTopic.apply + ' } failed for topic ' + topic + ' with message ' + message + ' - ' + ex ); 221 | } 222 | if( decoded !== undefined ) { 223 | return previous( intopic, decoded ); 224 | } 225 | }; 226 | } 227 | } 228 | 229 | // send through codec's decode function 230 | let codecDecode = getCodecFunction( codec, property, 'decode' ); 231 | if( codecDecode ) { 232 | let realHandler = handler; 233 | let output = function( message ) { 234 | return realHandler( topic, message ); 235 | }; 236 | handler = function( intopic, message ) { 237 | let decoded = codecDecode( message, { topic, property }, output ); 238 | if( decoded !== undefined ) { 239 | return output( decoded ); 240 | } 241 | }; 242 | } 243 | 244 | // register property dispatch (codec only) 245 | if( codec ) { 246 | if( propDispatch.hasOwnProperty( property ) ) { 247 | // new handler for existing property 248 | propDispatch[ property ].push( rawHandler ); 249 | } else { 250 | // new property 251 | propDispatch[ property ] = [ rawHandler ]; 252 | if( ctx.config.logMqtt ) { 253 | log( 'Avalable codec notification property: ' + property ); 254 | } 255 | } 256 | } 257 | 258 | // register MQTT dispatch and subscribe 259 | if( mqttDispatch.hasOwnProperty( topic ) ) { 260 | // new handler for existing topic 261 | mqttDispatch[ topic ].push( handler ); 262 | } else { 263 | // new topic 264 | mqttDispatch[ topic ] = [ handler ]; 265 | mqttClient.subscribe(topic); 266 | } 267 | }; 268 | 269 | // Publish 270 | this.publish = function( ctx, topic, property, message ) { 271 | let { config, log, mqttClient, codec } = ctx; 272 | if( ! mqttClient ) { 273 | log( 'ERROR: Call mqttlib.init() before mqttlib.publish()' ); 274 | return; 275 | } 276 | 277 | if( message === null || topic === undefined ) { 278 | return; // don't publish if message is null or topic is undefined 279 | } 280 | 281 | // first of all, pass message through any user-supplied apply() function 282 | if (typeof topic != 'string') { 283 | // encode data with user-supplied apply() function 284 | var extendedTopic = topic; 285 | topic = extendedTopic.topic; 286 | if (extendedTopic.hasOwnProperty('apply')) { 287 | var applyFn = Function( "message", "state", extendedTopic['apply'] ); //eslint-disable-line 288 | try { 289 | message = applyFn( message, getApplyState( ctx, property ) ); 290 | } catch( ex ) { 291 | log( 'Encode function apply( message ) { ' + extendedTopic.apply + ' } failed for topic ' + topic + ' with message ' + message + ' - ' + ex ); 292 | message = null; // stop publish 293 | } 294 | if( message === null || message === undefined ) { 295 | return; 296 | } 297 | } 298 | } 299 | 300 | function publishImpl( finalMessage ) { 301 | if( config.logMqtt ) { 302 | log( 'Publishing MQTT: ' + topic + ' = ' + finalMessage ); 303 | } 304 | mqttClient.publish( topic, finalMessage.toString(), config.mqttPubOptions ); 305 | } 306 | 307 | // publish directly or through codec 308 | let codecEncode = getCodecFunction( codec, property, 'encode' ); 309 | if( codecEncode ) { 310 | // send through codec's encode function 311 | let encoded = codecEncode( message, { topic, property }, publishImpl ); 312 | if( encoded !== undefined ) { 313 | publishImpl( encoded ); 314 | } 315 | } else { 316 | // publish as-is 317 | publishImpl( message ); 318 | } 319 | }; 320 | 321 | // Confirmed publisher 322 | this.makeConfirmedPublisher = function( ctx, setTopic, getTopic, property, makeConfirmed ) { 323 | 324 | let { state, config, log } = ctx; 325 | 326 | // if confirmation isn't being used, just return a simple publishing function 327 | if( ! config.confirmationPeriodms || ! getTopic || ! makeConfirmed ) { 328 | // no confirmation - return generic publishing function 329 | return function( message ) { 330 | mqttlib.publish( ctx, setTopic, property, message ); 331 | } 332 | } 333 | 334 | var timer = null; 335 | var expected = null; 336 | var indicatedOffline = false; 337 | var retriesRemaining = 0; 338 | 339 | // subscribe to our get topic 340 | mqttlib.subscribe( ctx, getTopic, property, function( topic, message ) { 341 | if( ( message === expected || message == ( expected + '' ) ) && timer ) { 342 | clearTimeout( timer ); 343 | timer = null; 344 | } 345 | if( indicatedOffline && ! timer ) { 346 | // if we're not waiting (or no-longer waiting), a message clears the offline state 347 | state.online = true; 348 | indicatedOffline = false; 349 | log( 'Setting accessory state to online' ); 350 | } 351 | } ); 352 | 353 | // return enhanced publishing function 354 | return function( message ) { 355 | // clear any existing confirmation timer 356 | if( timer ) { 357 | clearTimeout( timer ); 358 | timer = null; 359 | } 360 | 361 | // confirmation timeout function 362 | function confirmationTimeout() { 363 | // confirmation period has expired 364 | timer = null; 365 | // indicate offline (unless accessory is publishing this explicitly - overridden with confirmationIndicateOffline) 366 | if( config.confirmationIndicateOffline !== false && ( ! config.topics.getOnline || config.confirmationIndicateOffline === true ) && ! indicatedOffline ) { 367 | state.online = false; 368 | indicatedOffline = true; 369 | log( 'Setting accessory state to offline' ); 370 | } 371 | 372 | // retry 373 | if( retriesRemaining > 0 ) { 374 | --retriesRemaining; 375 | publish(); 376 | } else { 377 | log( 'Unresponsive - no confirmation message received on ' + getTopic + ". Expecting [" + expected + "]." ); 378 | } 379 | } 380 | 381 | function publish() { 382 | // set confirmation timer 383 | timer = setTimeout( confirmationTimeout, config.confirmationPeriodms ); 384 | 385 | // publish 386 | expected = message; 387 | mqttlib.publish( ctx, setTopic, property, message ); 388 | } 389 | 390 | // initialise retry counter 391 | retriesRemaining = ( config.retryLimit === undefined ) ? 3 : config.retryLimit; 392 | 393 | // initial publish 394 | publish(); 395 | }; 396 | }; 397 | 398 | }; 399 | 400 | module.exports = mqttlib; 401 | -------------------------------------------------------------------------------- /test/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "bridge": { 3 | "name": "Homebridge", 4 | "username": "DA:7C:A3:24:AA:69", 5 | "_username": "DA:7C:A6:27:A7:69", 6 | "port": 55555, 7 | "pin": "884-65-462", 8 | "_pin": "813-65-465" 9 | }, 10 | "description": "This is an example configuration file. You can use this as a template for creating your own configuration file.", 11 | "platforms": [ 12 | { 13 | "platform": "config", 14 | "name": "Config", 15 | "port": 8080, 16 | "sudo": false 17 | } 18 | ], 19 | "accessories": [ 20 | { 21 | "type": "lightbulb-Colour", 22 | "name": "Test lightbulb", 23 | "url": "http://192.168.10.35:1883", 24 | "mqttPubOptions": { 25 | "retain": true 26 | }, 27 | "logMqtt": true, 28 | "codec": "empty-codec.js", 29 | "topics": { 30 | "getOnline": "test/lightbulb/getOnline", 31 | "getOn": "test/lightbulb/getOn", 32 | "setOn": "test/lightbulb/setOn", 33 | "getHue": "test/lightbulb/getHue", 34 | "setHue": "test/lightbulb/setHue", 35 | "getSaturation": "test/lightbulb/getSaturation", 36 | "setSaturation": "test/lightbulb/setSaturation", 37 | "getBrightness": "test/lightbulb/getBrightness", 38 | "setBrightness": "test/lightbulb/setBrightness", 39 | "getColorTemperature": "test/lightbulb/getTemperature", 40 | "setColorTemperature": "test/lightbulb/setTemperature" 41 | }, 42 | "startPub": [ 43 | { 44 | "topic": "test/lightbulb/setOn", 45 | "message": "1" 46 | }, 47 | { 48 | "topic": "test/lightbulb/getOn", 49 | "message": "1" 50 | }, 51 | { 52 | "topic": "test/lightbulb/setBrightness", 53 | "message": "80" 54 | }, 55 | { 56 | "topic": "test/lightbulb/getBrightness", 57 | "message": "80" 58 | }, 59 | { 60 | "topic": "test/lightbulb", 61 | "message": "Hello world" 62 | } 63 | ], 64 | "onlineValue": "Online", 65 | "integerValue": true, 66 | "accessory": "mqttthing" 67 | }, 68 | { 69 | "type": "lightbulb-HSV", 70 | "name": "Test HSV Light", 71 | "url": "http://192.168.10.35:1883", 72 | "logMqtt": true, 73 | "topics": { 74 | "setHSV": "test/hsvlight/setHSV", 75 | "getHSV": "test/hsvlight/getHSV" 76 | }, 77 | "startPub": [ 78 | { 79 | "topic": "test/hsvlight/setHSV" 80 | } 81 | ], 82 | "accessory": "mqttthing" 83 | }, 84 | { 85 | "accessory": "mqttthing", 86 | "type": "lightbulb-HSV", 87 | "name": "Test espurna Light", 88 | "url": "http://192.168.10.35:1883", 89 | "topics": { 90 | "getHSV": "test/hsvlight2/hsv", 91 | "setHSV": "test/hsvlight2/hsv/set", 92 | "getOn": "test/hsvlight2/on", 93 | "setOn": "test/hsvlight2/on/set" 94 | }, 95 | "logMqtt": true, 96 | "integerValue": true, 97 | "codec": "keep-alive-codec.js", 98 | "keepAliveTopic": "keep/alive", 99 | "keepAlivePeriod": 120, 100 | "keepAliveMessage": "Keep-alive!" 101 | }, 102 | { 103 | "accessory": "mqttthing", 104 | "type": "lightbulb", 105 | "name": "Test RGB Light", 106 | "url": "http://192.168.10.35:1883", 107 | "topics": { 108 | "getRGB": "test/rgblight/get", 109 | "setRGB": "test/rgblight/set", 110 | "getOn": "test/rgblight/get", 111 | "setOn": "test/rgblight/set" 112 | }, 113 | "logMqtt": true, 114 | "integerValue": false, 115 | "codec": "json", 116 | "jsonCodec": { 117 | "properties": { 118 | "on": "state.power", 119 | "RGB": "state.rgb" 120 | }, 121 | "fixed": { 122 | "version": 1, 123 | "sender": "MQTT-Thing" 124 | } 125 | } 126 | }, 127 | { 128 | "accessory": "mqttthing", 129 | "type": "lightbulb-RGBW", 130 | "name": "Test RGBW Light", 131 | "url": "http://192.168.10.35:1883", 132 | "topics": { 133 | "getRGBW": "test/rgbwlight/rgb", 134 | "setRGBW": "test/rgbwlight/rgb/set" 135 | }, 136 | "logMqtt": true, 137 | "integerValue": true, 138 | "hexPrefix": "#" 139 | }, 140 | { 141 | "accessory": "mqttthing", 142 | "type": "lightbulb-RGB", 143 | "name": "Test RGBW Light 2", 144 | "url": "http://192.168.10.35:1883", 145 | "topics": { 146 | "getRGB": "test/rgbwlight2", 147 | "setRGB": "test/rgbwlight2/set", 148 | "getWhite": "test/rgbwlight2", 149 | "setWhite": "test/rgbwlight2/set", 150 | "getOn": "test/rgbwlight2", 151 | "setOn": "test/rgbwlight2/set" 152 | }, 153 | "logMqtt": true, 154 | "integerValue": true, 155 | "hexPrefix": "#", 156 | "codec": "json", 157 | "jsonCodec": { 158 | "properties": { 159 | "RGB": "state.rgb", 160 | "white": "state.white", 161 | "on": "state.on" 162 | } 163 | } 164 | }, 165 | { 166 | "accessory": "mqttthing", 167 | "type": "lightbulb-RGBWW", 168 | "name": "Test RGBWW Light", 169 | "url": "http://192.168.10.35:1883", 170 | "topics": { 171 | "getRGBWW": "test/rgbwwlight/rgb", 172 | "setRGBWW": "test/rgbwwlight/rgb/set" 173 | }, 174 | "logMqtt": true 175 | }, 176 | { 177 | "accessory": "mqttthing", 178 | "type": "lightbulb-White", 179 | "name": "Test White Light", 180 | "url": "http://192.168.10.35:1883", 181 | "topics": { 182 | "getWhite": "test/whitelight/white", 183 | "setWhite": "test/whitelight/white/set" 184 | }, 185 | "logMqtt": true 186 | }, 187 | { 188 | "accessory": "mqttthing", 189 | "type": "lightbulb-ColTemp", 190 | "name": "Test ColorTemperature Light", 191 | "url": "http://192.168.10.35:1883", 192 | "topics": { 193 | "getOn": "test/coltemp/getOn", 194 | "setOn": "test/coltemp/setOn", 195 | "getBrightness": "test/coltemp/getBrightness", 196 | "setBrightness": "test/coltemp/setBrightness", 197 | "getColorTemperature": "test/coltemp/getTemperature", 198 | "setColorTemperature": "test/coltemp/setTemperature" 199 | }, 200 | "logMqtt": true, 201 | "integerValue": true 202 | }, 203 | { 204 | "accessory": "mqttthing", 205 | "type": "lightbulb-Dimmable", 206 | "name": "Test Dimmable Light", 207 | "url": "http://192.168.10.35:1883", 208 | "topics": { 209 | "getOn": "test/dimmable/setOn", 210 | "setOn": "test/dimmable/setOn", 211 | "getBrightness": "test/dimmable/getBrightness", 212 | "setBrightness": "test/dimmable/setBrightness" 213 | }, 214 | "logMqtt": true, 215 | "onValue": "DIMMABLE", 216 | "offValue": "OFF", 217 | "otherValueOff": true, 218 | "codec": "test-codec.js" 219 | }, 220 | { 221 | "accessory": "mqttthing", 222 | "type": "lightbulb-OnOff", 223 | "name": "Alternative Light Mode", 224 | "url": "http://192.168.10.35:1883", 225 | "topics": { 226 | "getOn": "test/dimmable/setOn", 227 | "setOn": "test/dimmable/setOn" 228 | }, 229 | "logMqtt": true, 230 | "onValue": "ALTMODE", 231 | "offValue": "OFF", 232 | "otherValueOff": true 233 | }, 234 | { 235 | "accessory": "mqttthing", 236 | "type": "switch", 237 | "name": "Test switch", 238 | "url": "http://192.168.10.35:1883", 239 | "topics": { 240 | "getOn": "test/switch/getOn", 241 | "setOn": "test/switch/setOn" 242 | }, 243 | "integerValue": true, 244 | "turnOffAfterms": 2000 245 | }, 246 | { 247 | "accessory": "mqttthing", 248 | "type": "switch", 249 | "name": "Test switch 2", 250 | "url": "http://192.168.10.35:1883", 251 | "topics": { 252 | "getOn": "test/switch2/getOn", 253 | "setOn": "test/switch2/setOn" 254 | }, 255 | "integerValue": true, 256 | "history": true 257 | }, 258 | { 259 | "accessory": "mqttthing", 260 | "type": "outlet", 261 | "name": "Test outlet", 262 | "url": "http://192.168.10.35:1883", 263 | "mqttPubOptions": { 264 | "retain": true 265 | }, 266 | "topics": { 267 | "getOn": "test/outlet/getOn", 268 | "setOn": "test/outlet/setOn", 269 | "getInUse": "test/outlet/getInUse", 270 | "getWatts": "test/outlet/getWatts", 271 | "getVolts": "test/outlet/getVolts", 272 | "getAmperes": "test/outlet/getAmperes" 273 | }, 274 | "integerValue": true, 275 | "history": true, 276 | "confirmationPeriodms": 2000, 277 | "retryLimit": 10, 278 | "logMqtt": true 279 | }, 280 | { 281 | "accessory": "mqttthing", 282 | "confirmationPeriodms": 1000, 283 | "confirmationIndicateOffline": true, 284 | "mqttPubOptions": { 285 | "retain": true 286 | }, 287 | "name": "WZ Licht Vorne", 288 | "offValue": "off", 289 | "onlineValue": "true", 290 | "onValue": "on", 291 | "retryLimit": 3, 292 | "topics": { 293 | "getOn": "shellies/shelly1-2C6BF0/relay/0", 294 | "getOnline": "shellies/shelly1-2C6BF0/online", 295 | "setOn": "shellies/shelly1-2C6BF0/relay/0/command" 296 | }, 297 | "type": "switch", 298 | "url": "http://192.168.10.35:1883" 299 | }, 300 | { 301 | "accessory": "mqttthing", 302 | "type": "motionSensor", 303 | "name": "Test motion sensor", 304 | "url": "http://192.168.10.35:1883", 305 | "topics": { 306 | "getMotionDetected": "test/motion/detected", 307 | "getStatusActive": "test/motion/active", 308 | "getStatusFault": "test/motion/fault", 309 | "getStatusTampered": "test/motion/tampered", 310 | "getStatusLowBattery": "test/motion/lowBattery", 311 | "getBatteryLevel": "test/motion/batteryLevel", 312 | "getChargingState": "test/motion/chargingState" 313 | }, 314 | "integerValue": true, 315 | "chargingStateValues": [ 316 | "NotCharging", 317 | "Charging", 318 | "NotChargeable" 319 | ], 320 | "logMqtt": true, 321 | "turnOffAfterms": 3000, 322 | "history": { 323 | "mergeInterval": 5 324 | } 325 | }, 326 | { 327 | "accessory": "mqttthing", 328 | "type": "occupancySensor", 329 | "name": "Test occupancy sensor", 330 | "url": "http://192.168.10.35:1883", 331 | "topics": { 332 | "getOccupancyDetected": "test/occupancy/detected", 333 | "getStatusActive": "test/occupancy/active", 334 | "getStatusFault": "test/occupancy/fault", 335 | "getStatusTampered": "test/occupancy/tampered", 336 | "getStatusLowBattery": "test/occupancy/lowBattery" 337 | }, 338 | "integerValue": true 339 | }, 340 | { 341 | "accessory": "mqttthing", 342 | "type": "lightSensor", 343 | "name": "Test light sensor", 344 | "url": "http://192.168.10.35:1883", 345 | "logMqtt": true, 346 | "topics": { 347 | "getCurrentAmbientLightLevel": "test/light/level" 348 | } 349 | }, 350 | { 351 | "accessory": "mqttthing", 352 | "type": "temperatureSensor", 353 | "name": "Test temperature sensor", 354 | "url": "http://192.168.10.35:1883", 355 | "topics": { 356 | "getCurrentTemperature": "test/temperature" 357 | }, 358 | "history": true, 359 | "logMqtt": true, 360 | "maxTemperature": 150 361 | }, 362 | { 363 | "accessory": "mqttthing", 364 | "type": "humiditySensor", 365 | "name": "Test humidity sensor", 366 | "url": "http://192.168.10.35:1883", 367 | "topics": { 368 | "getCurrentRelativeHumidity": "test/humidity" 369 | }, 370 | "history": { 371 | "size": 1000, 372 | "autoRepeat": false, 373 | "persistencePath": "history" 374 | } 375 | }, 376 | { 377 | "accessory": "mqttthing", 378 | "type": "airPressureSensor", 379 | "name": "Test air pressure sensor", 380 | "url": "http://192.168.10.35:1883", 381 | "topics": { 382 | "getAirPressure": "test/airpressure" 383 | }, 384 | "history": true 385 | }, 386 | { 387 | "accessory": "mqttthing", 388 | "type": "weatherStation", 389 | "name": "Test weather station", 390 | "serviceNames": { 391 | "temperature": "WS-Temperature", 392 | "humidity": "WS-Humidity", 393 | "airPressure": "WS-AirPressure", 394 | "weather": "WS-WeatherData" 395 | }, 396 | "url": "mqtt://192.168.179.5:1883", 397 | "topics": { 398 | "getCurrentTemperature": "test/weather/temperature", 399 | "getCurrentRelativeHumidity": "test/weather/humidity", 400 | "getAirPressure": "test/weather/airpressure", 401 | "getWeatherCondition": "test/weather/condition", 402 | "getRain1h": "test/weather/rain1", 403 | "getRain24h": "test/weather/rain24", 404 | "getUVIndex": "test/weather/uv", 405 | "getVisibility": "test/weather/visibility", 406 | "getWindDirection": "test/weather/winddirection", 407 | "getWindSpeed": "test/weather/windspeed" 408 | }, 409 | "history": false 410 | }, 411 | { 412 | "accessory": "mqttthing", 413 | "type": "contactSensor", 414 | "name": "Test contact sensor", 415 | "url": "http://192.168.10.35:1883", 416 | "topics": { 417 | "getContactSensorState": "test/contact/state" 418 | }, 419 | "integerValue": true, 420 | "history": true 421 | }, 422 | { 423 | "accessory": "mqttthing", 424 | "type": "doorbell", 425 | "name": "Test doorbell", 426 | "url": "http://192.168.10.35:1883", 427 | "logMqtt": true, 428 | "topics": { 429 | "getSwitch": "test/doorbell/ring", 430 | "getMotionDetected": "test/doorbell/motion/detected", 431 | "setVolume": "test/doorbell/volume", 432 | "setBrightness": "test/doorbell/brightness" 433 | }, 434 | "switchValues": [ 435 | "SINGLE", 436 | "DOUBLE", 437 | "LONG" 438 | ], 439 | "integerValue": true 440 | }, 441 | { 442 | "accessory": "mqttthing", 443 | "type": "securitySystem", 444 | "name": "Test Security System", 445 | "url": "http://192.168.10.35:1883", 446 | "logMqtt": true, 447 | "topics": { 448 | "setTargetState": "test/security/target", 449 | "getTargetState": "test/security/current", 450 | "getCurrentState": "test/security/current" 451 | }, 452 | "targetStateValues": [ 453 | "StayArm", 454 | "AwayArm", 455 | "NightArm", 456 | "Disarmed" 457 | ], 458 | "currentStateValues": [ 459 | "StayArm", 460 | "AwayArm", 461 | "NightArm", 462 | "Disarmed", 463 | "Triggered" 464 | ], 465 | "restrictTargetState": [ 466 | 1, 467 | 2, 468 | 3 469 | ] 470 | }, 471 | { 472 | "accessory": "mqttthing", 473 | "type": "smokeSensor", 474 | "name": "Test smoke sensor", 475 | "url": "http://192.168.10.35:1883", 476 | "logMqtt": true, 477 | "topics": { 478 | "getSmokeDetected": "test/smoke/detected" 479 | }, 480 | "integerValue": true 481 | }, 482 | { 483 | "accessory": "mqttthing", 484 | "type": "leakSensor", 485 | "name": "Test leak sensor", 486 | "url": "http://192.168.10.35:1883", 487 | "logMqtt": true, 488 | "topics": { 489 | "getLeakDetected": "test/leak/detected" 490 | }, 491 | "integerValue": true, 492 | "resetStateAfterms": 5000 493 | }, 494 | { 495 | "accessory": "mqttthing", 496 | "type": "statelessProgrammableSwitch", 497 | "name": "Test Stateless Programmable Switch", 498 | "url": "http://192.168.10.35:1883", 499 | "logMqtt": true, 500 | "topics": { 501 | "getSwitch": "test/spswitch/state" 502 | }, 503 | "restrictSwitchValues": [ 504 | 0, 505 | 1 506 | ] 507 | }, 508 | { 509 | "accessory": "mqttthing", 510 | "type": "statelessProgrammableSwitch", 511 | "name": "Multi-switch", 512 | "url": "http://192.168.10.35:1883", 513 | "logMqtt": true, 514 | "topics": { 515 | "getSwitch": [ 516 | "test/mspswitch/1", 517 | "test/mspswitch/2", 518 | "test/mspswitch/3" 519 | ] 520 | } 521 | }, 522 | { 523 | "accessory": "mqttthing", 524 | "type": "garageDoorOpener", 525 | "name": "Test Garage Door", 526 | "url": "http://192.168.10.35:1883", 527 | "logMqtt": true, 528 | "topics": { 529 | "setTargetDoorState": "test/garage/target", 530 | "getTargetDoorState": "test/garage/target", 531 | "getCurrentDoorState": "test/garage/current", 532 | "setLockTargetState": "test/lock/target", 533 | "getLockTargetState": "test/garagelock/target", 534 | "getLockCurrentState": "test/garagelock/current", 535 | "getObstructionDetected": "test/garage/obstruction" 536 | }, 537 | "doorValues": [ 538 | "Open", 539 | "Closed", 540 | "Opening", 541 | "Closing", 542 | "Stopped" 543 | ], 544 | "lockValues": [ 545 | "Unsecured", 546 | "Secured", 547 | "Jammed", 548 | "Unknown" 549 | ] 550 | }, 551 | { 552 | "accessory": "mqttthing", 553 | "type": "garageDoorOpener", 554 | "name": "Simple Garage Door", 555 | "url": "http://192.168.10.35:1883", 556 | "logMqtt": true, 557 | "topics": { 558 | "setTargetDoorState": "test/garage2/target", 559 | "getTargetDoorState": "test/garage2/target", 560 | "getDoorMoving": "test/garage2/moving" 561 | }, 562 | "doorTargetValues": [ 563 | "Open", 564 | "Closed" 565 | ] 566 | }, 567 | { 568 | "accessory": "mqttthing", 569 | "type": "lockMechanism", 570 | "name": "Test Lock", 571 | "url": "http://192.168.10.35:1883", 572 | "logMqtt": true, 573 | "topics": { 574 | "setLockTargetState": "test/lock/target", 575 | "getLockTargetState": "test/lock/current", 576 | "getLockCurrentState": "test/lock/current" 577 | }, 578 | "lockValues": [ 579 | "Unsecured", 580 | "Secured", 581 | "Jammed", 582 | "Unknown" 583 | ] 584 | }, 585 | { 586 | "accessory": "mqttthing", 587 | "type": "fan", 588 | "name": "Test Fan", 589 | "url": "http://192.168.10.35:1883", 590 | "logMqtt": true, 591 | "topics": { 592 | "getOn": "test/fan/getOn", 593 | "setOn": "test/fan/setOn", 594 | "getRotationDirection": "test/fan/getRotationDirection", 595 | "setRotationDirection": "test/fan/setRotationDirection", 596 | "getRotationSpeed": "test/fan/getRotationSpeed", 597 | "setRotationSpeed": "test/fan/setRotationSpeed" 598 | }, 599 | "integerValue": true 600 | }, 601 | { 602 | "accessory": "mqttthing", 603 | "type": "lightbulb", 604 | "name": "OnOffTest", 605 | "url": "http://192.168.10.35:1883", 606 | "logMqtt": "true", 607 | "topics": { 608 | "getOn": "test/onoff/getOn", 609 | "setOn": "test/onoff/setOn" 610 | }, 611 | "onValue": "on", 612 | "offValue": "off" 613 | }, 614 | { 615 | "accessory": "mqttthing", 616 | "type": "switch", 617 | "name": "Morning Scene", 618 | "url": "http://192.168.10.35:1883", 619 | "logMqtt": "true", 620 | "topics": { 621 | "getOn": "test/scene/name", 622 | "setOn": "test/scene/name" 623 | }, 624 | "onValue": "morning" 625 | }, 626 | { 627 | "accessory": "mqttthing", 628 | "type": "switch", 629 | "name": "Afternoon Scene", 630 | "url": "http://192.168.10.35:1883", 631 | "logMqtt": "true", 632 | "topics": { 633 | "getOn": "test/scene/name", 634 | "setOn": "test/scene/name" 635 | }, 636 | "onValue": "afternoon" 637 | }, 638 | { 639 | "accessory": "mqttthing", 640 | "type": "switch", 641 | "name": "Evening Scene", 642 | "url": "http://192.168.10.35:1883", 643 | "logMqtt": "true", 644 | "topics": { 645 | "getOn": "test/scene/name", 646 | "setOn": "test/scene/name" 647 | }, 648 | "onValue": "evening" 649 | }, 650 | { 651 | "accessory": "mqttthing", 652 | "type": "speaker", 653 | "name": "Speaker", 654 | "url": "http://192.168.10.35:1883", 655 | "logMqtt": "true", 656 | "topics": { 657 | "getMute": "test/speaker/getMute", 658 | "setMute": "test/speaker/setMute", 659 | "getVolume": "test/speaker/getVolume", 660 | "setVolume": "test/speaker/setVolume" 661 | } 662 | }, 663 | { 664 | "accessory": "mqttthing", 665 | "type": "microphone", 666 | "name": "Microphone", 667 | "url": "http://192.168.10.35:1883", 668 | "logMqtt": "true", 669 | "topics": { 670 | "getMute": "test/microphone/getMute", 671 | "setMute": "test/microphone/setMute", 672 | "getVolume": "test/microphone/getVolume", 673 | "setVolume": "test/microphone/setVolume" 674 | } 675 | }, 676 | { 677 | "accessory": "mqttthing", 678 | "type": "windowCovering", 679 | "name": "Blind", 680 | "url": "http://192.168.10.35:1883", 681 | "logMqtt": "true", 682 | "topics": { 683 | "getCurrentPosition": "test/blind/getCurrentPosition", 684 | "setTargetPosition": "test/blind/setTargetPosition", 685 | "getPositionState": "test/blind/getPositionState", 686 | "setTargetVerticalTiltAngle": "test/blind/setTargetVerticalTiltAngle", 687 | "getCurrentVerticalTiltAngle": "test/blind/getCurrentVerticalTiltAngle" 688 | } 689 | }, 690 | { 691 | "accessory": "mqttthing", 692 | "type": "window", 693 | "name": "Window", 694 | "url": "http://192.168.10.35:1883", 695 | "logMqtt": "true", 696 | "topics": { 697 | "getCurrentPosition": "test/window/getCurrentPosition", 698 | "setTargetPosition": "test/window/setTargetPosition", 699 | "getPositionState": "test/window/getPositionState" 700 | } 701 | }, 702 | { 703 | "accessory": "mqttthing", 704 | "type": "carbonDioxideSensor", 705 | "name": "CO2", 706 | "url": "http://192.168.10.35:1883", 707 | "logMqtt": "true", 708 | "topics": { 709 | "getCarbonDioxideDetected": "test/co2/get" 710 | } 711 | }, 712 | { 713 | "accessory": "mqttthing", 714 | "type": "airQualitySensor", 715 | "name": "AirQuality", 716 | "url": "http://192.168.10.35:1883", 717 | "logMqtt": "true", 718 | "topics": { 719 | "getAirQuality": "test/airquality/get", 720 | "getAirQualityPPM": "test/airqualityppm/get", 721 | "getPM10Density": "test/airquality/pm10", 722 | "getPM2_5Density": "test/airquality/pm2_5", 723 | "getOzoneDensity": "test/airquality/ozone", 724 | "getSulphurDioxideDensity": "test/airquality/so2", 725 | "getNitrogenDioxideDensity": "test/airquality/no2", 726 | "getVOCDensity": "test/airquality/voc", 727 | "getCarbonMonoxideLevel": "test/airquality/co", 728 | "getCarbonDioxideLevel": "test/airquality/co2", 729 | "getCurrentTemperature": "test/airquality/temperature", 730 | "getCurrentRelativeHumidity": "test/airquality/humidity" 731 | }, 732 | "history": true 733 | }, 734 | { 735 | "accessory": "mqttthing", 736 | "type": "valve", 737 | "valveType": "sprinkler", 738 | "name": "Sprinkler", 739 | "url": "http://192.168.10.35:1883", 740 | "topics": { 741 | "getActive": "test/sprinkler/getActive", 742 | "setActive": "test/sprinkler/setActive", 743 | "getInUse": "test/sprinkler/getInUse", 744 | "getDuration": "test/sprinkler/getDuration", 745 | "setDuration": "test/sprinkler/setDuration", 746 | "getRemainingDuration": "test/sprinkler/getRemainingDuration" 747 | }, 748 | "integerValue": true, 749 | "durationTimer": true, 750 | "maxDuration": 180 751 | }, 752 | { 753 | "accessory": "mqttthing", 754 | "type": "thermostat", 755 | "name": "Thermostat", 756 | "url": "http://192.168.10.35:1883", 757 | "logMqtt": "true", 758 | "topics": { 759 | "getCurrentHeatingCoolingState": "test/thermostat/getCurrentState", 760 | "setCurrentHeatingCoolingState": "test/thermostat/setCurrentState", 761 | "getTargetHeatingCoolingState": "test/thermostat/getTargetState", 762 | "setTargetHeatingCoolingState": "test/thermostat/setTargetState", 763 | "getCurrentTemperature": "test/thermostat/getCurrentTemperature", 764 | "setTargetTemperature": "test/thermostat/setTargetTemperature", 765 | "getTargetTemperature": "test/thermostat/getTargetTemperature", 766 | "setTemperatureDisplayUnits": "test/thermostat/setTemperatureDisplayUnits", 767 | "getTemperatureDisplayUnits": "test/thermostat/getTemperatureDisplayUnits" 768 | }, 769 | "minTemperature": 5, 770 | "maxTemperature": 27.5, 771 | "restrictHeatingCoolingState": [ 772 | 0, 773 | 1 774 | ] 775 | }, 776 | { 777 | "accessory": "mqttthing", 778 | "type": "heaterCooler", 779 | "name": "HeaterCooler", 780 | "url": "http://192.168.10.35:1883", 781 | "logMqtt": true, 782 | "topics": { 783 | "setActive": "test/heatcool/setActive", 784 | "getActive": "test/heatcool/getActive", 785 | "getCurrentHeaterCoolerState": "test/heatcool/getCurrentHeaterCoolerState", 786 | "setTargetHeaterCoolerState": "test/heatcool/setCurrentHeaterCoolerState", 787 | "getCurrentTemperature": "test/heatcool/getCurrentTemperature", 788 | "setCoolingThresholdTemperature": "test/heatcool/setCoolingThresholdTemperature", 789 | "setHeatingThresholdTemperature": "test/heatcool/setHeatingThresholdTemperature", 790 | "setRotationMode": "test/heatcool/setRotationMode", 791 | "setSwingMode": "test/heatcool/setSwingMode", 792 | "setRotationSpeed": { 793 | "topic": "test/heatcool/setRotationSpeed", 794 | "apply": "return [ 'off', 'min', 'low', 'medium', 'high', 'max' ][ Math.floor( message / 20 ) ];" 795 | } 796 | } 797 | }, 798 | { 799 | "accessory": "mqttthing", 800 | "type": "television", 801 | "name": "Television", 802 | "url": "http://192.168.10.35:1883", 803 | "topics": { 804 | "setActive": "test/television/setActive", 805 | "getActive": "test/television/getActive", 806 | "setActiveInput": "test/television/setActiveInput", 807 | "getActiveInput": "test/television/getActiveInput", 808 | "setRemoteKey": "test/television/setRemoteKey" 809 | }, 810 | "inputs": [ 811 | { 812 | "name": "Live TV", 813 | "value": "TV" 814 | }, 815 | { 816 | "name": "HDMI 1", 817 | "value": "HDMI1" 818 | }, 819 | { 820 | "name": "HDMI 2", 821 | "value": "HDMI2" 822 | }, 823 | { 824 | "name": "HDMI 3", 825 | "value": "HDMI3" 826 | }, 827 | { 828 | "name": "Amazon Prime Video", 829 | "value": "AMAZON" 830 | } 831 | ], 832 | "logMqtt": true, 833 | "integerValue": true 834 | }, 835 | { 836 | "accessory": "mqttthing", 837 | "type": "switch", 838 | "name": "Ucam Gartenhaus Audio", 839 | "manufacturer": "INSTAR", 840 | "model": "IN-9008 FullHD", 841 | "serialNumber": "1234567890", 842 | "firmwareRevision": "0.1", 843 | "url": "http://192.168.10.35:1883", 844 | "username": "USERNAME", 845 | "password": "PASSWORD", 846 | "caption": "Audio", 847 | "mqttOptions": { 848 | "keepalive": 30 849 | }, 850 | "mqttPubOptions": { 851 | "retain": true 852 | }, 853 | "logMqtt": true, 854 | "topics": { 855 | "getOn": { 856 | "topic": "instar/local/status/multimedia/audio/enable/high", 857 | "apply": "return JSON.parse(message).val;" 858 | }, 859 | "setOn": { 860 | "topic": "instar/local/multimedia/audio/enable/high", 861 | "apply": "return JSON.stringify({val: (message)})" 862 | } 863 | }, 864 | "onValue": "1", 865 | "offValue": "0", 866 | "confirmationPeriodms": 1000, 867 | "retryLimit": 3 868 | }, 869 | { 870 | "accessory": "mqttthing", 871 | "type": "switch", 872 | "name": "Hallway Lights", 873 | "url": "http://192.168.10.35:1883", 874 | "manufacturer": "Sonoff", 875 | "model": "Touch", 876 | "serialNumber": "sonoff1", 877 | "topics": { 878 | "setOn": "cmnd/sonoff1/power", 879 | "getOn": "stat/sonoff1/POWER", 880 | "getOnline": { 881 | "topic": "tele/sonoff1/LWT", 882 | "apply": "return message == 'Online' ? 'ON' : 'OFF';" 883 | } 884 | }, 885 | "onValue": "ON", 886 | "offValue": "OFF", 887 | "confirmationPeriodms": 1000, 888 | "retryLimit": 3, 889 | "logMqtt": true, 890 | "startPub": { 891 | "cmnd/sonoff1/power": "" 892 | }, 893 | "turnOffAfterms": 6000 894 | }, 895 | { 896 | "accessory": "mqttthing", 897 | "type": "statelessProgrammableSwitch", 898 | "name": "4 Button Remote", 899 | "url": "http://192.168.10.35:1883", 900 | "caption": "4 Button Remote", 901 | "topics": { 902 | "getSwitch": [ 903 | { 904 | "topic": "tele/RF/RESULT", 905 | "apply": "return JSON.parse( message ).RfReceived.Data;" 906 | }, { 907 | "topic": "tele/RF/RESULT", 908 | "apply": "return JSON.parse( message ).RfReceived.Data;" 909 | }, { 910 | "topic": "tele/RF/RESULT", 911 | "apply": "return JSON.parse( message ).RfReceived.Data;" 912 | }, { 913 | "topic": "tele/RF/RESULT", 914 | "apply": "return JSON.parse( message ).RfReceived.Data;" 915 | } 916 | ] 917 | }, 918 | "switchValues": [ 919 | [ "A56021" ], 920 | [ "A56022" ], 921 | [ "A56028" ], 922 | [ "A56024" ] 923 | ], 924 | "restrictSwitchValues": [ 925 | 0 926 | ], 927 | "logMqtt": true 928 | }, 929 | { 930 | "accessory": "mqttthing", 931 | "type": "irrigationSystem", 932 | "name": "Irrigation", 933 | "url": "http://192.168.10.35:1883", 934 | "topics": { 935 | "getActive": "test/irrigation/getActive", 936 | "setActive": "test/irrigation/setActive" 937 | }, 938 | "zones": [ 939 | { 940 | "name": "Zone 1", 941 | "topics": { 942 | "getActive": "test/irrigation/1/getActive", 943 | "setActive": "test/irrigation/1/setActive", 944 | "getInUse": "test/irrigation/1/getInUse" 945 | } 946 | }, 947 | { 948 | "name": "Zone 2", 949 | "topics": { 950 | "getActive": "test/irrigation/2/getActive", 951 | "setActive": "test/irrigation/2/setActive", 952 | "getInUse": "test/irrigation/2/getInUse" 953 | } 954 | }, 955 | { 956 | "name": "Zone 3", 957 | "topics": { 958 | "getActive": "test/irrigation/3/getActive", 959 | "setActive": "test/irrigation/3/setActive", 960 | "getInUse": "test/irrigation/3/getInUse" 961 | } 962 | } 963 | ], 964 | "durationTimer": true, 965 | "integerValue": true 966 | }, 967 | { 968 | "accessory": "mqttthing", 969 | "type": "airPurifier", 970 | "name": "Air Purifier", 971 | "url": "http://192.168.10.35:1883", 972 | "topics": { 973 | "getActive": "test/airpurifier/getActive", 974 | "setActive": "test/airpurifier/setActive", 975 | "getCurrentAirPurifierState": "test/airpurifier/getCurrentState", 976 | "getTargetAirPurifierState": "test/airpurifier/getTargetState", 977 | "setTargetAirPurifierState": "test/airpurifier/setTargetState", 978 | "getRotationSpeed": "test/airpurifier/getRotationSpeed", 979 | "getSwingMode": "test/airpurifier/getSwingMode", 980 | "setLockPhysicalControls": "test/airpurifier/setLockPhysicalControls" 981 | }, 982 | "integerValue": true 983 | } 984 | ] 985 | } 986 | --------------------------------------------------------------------------------