From 3794e342b0e8141328fb85ad1f8e99857f7a7cd3 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Mon, 30 Nov 2020 14:34:01 +0100 Subject: [PATCH] feat: hass discovery (#20) * feat: bind target values to current * feat(wip): hass discovery * fix: better discovery support * fix: hass socket apis * fix: alarmType and meterType mapping * fix: use commandClasses instead of hardCoded numbers * refactor: better docs on methods * fix: converted some values in hass devices.js * fix: converted some more values in devices.js * feat: skip discovety flag * fix: lint issues * fix: ignore discovery * fix: converted some more valueIds of devices.json * fix: lint issues * fix: push target value in values * fix: mode_map and fan _mode_map * fix: allow empty json device * fix: added ccSpecific to value meta * fix: setpoints valueIds * fix: commandclasses import * fix: set defaiult endpoint to 0 * feat: added ccSpecific * fix: undefined in num2hex * fix: use 5.4.1-alpha.0 * fix: probably fix to map template bug * fix: mode_map template creation * fix: added brackets to keys * fix: added comments * fix: getMappedValuesTemplate check type of value * fix: lint errors * docs: fixed hass docs * docs: migration and why zwavejs * docs: bitmask and color values * fix: lint issues * docs: fix for review * fix: typo * fix: lint on readme * docs: fix * docs: added some points to why zwavejs section * fix: payload parse of modes when discovery is enabled * fix: missing ccSpecific values (#35) * fix: binary sensors, units and undefined labels (#36) * fix: lint issues Co-authored-by: V Aretakis --- .prettierignore | 3 +- .vscode/settings.json | 3 + README.md | 272 +++++++++--------- app.js | 14 +- hass/configurations.js | 22 +- hass/devices.js | 200 +++++++------ lib/Constants.js | 2 +- lib/Gateway.js | 491 ++++++++++++++++++++------------ lib/ZwaveClient.js | 33 ++- lib/utils.js | 2 +- src/components/ControlPanel.vue | 3 +- 11 files changed, 623 insertions(+), 422 deletions(-) diff --git a/.prettierignore b/.prettierignore index e3f74e3ad48..b6822880335 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,4 +1,5 @@ CHANGELOG.md +README.md .github/ISSUE_TEMPLATE dist -store \ No newline at end of file +store diff --git a/.vscode/settings.json b/.vscode/settings.json index db3370aeca9..ba8f8999cb0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -8,5 +8,8 @@ "wallaby.startAutomatically": true, "[vue]": { "editor.defaultFormatter": "dbaeumer.vscode-eslint" + }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } } diff --git a/README.md b/README.md index b676de241a4..23c465d41e1 100644 --- a/README.md +++ b/README.md @@ -14,9 +14,9 @@ [![Total alerts](https://img.shields.io/lgtm/alerts/g/zwave-js/zwavejs2mqtt.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/zwave-js/zwavejs2mqtt/alerts/) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/zwave-js/zwavejs2mqtt.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/zwave-js/zwavejs2mqtt/context:javascript) -[![Join channel](https://img.shields.io/badge/SLACK-zwave2mqtt.slack.com-red.svg?style=popout&logo=slack&logoColor=red)](https://join.slack.com/t/zwave2mqtt/shared_invite/enQtNjc4NjgyNjc3NDI2LTc3OGQzYmJlZDIzZTJhMzUzZWQ3M2Q3NThmMjY5MGY1MTc4NjFiOWZhZWE5YjNmNGE0OWRjZjJiMjliZGQyYmU 'Join channel') +[![Join channel](https://img.shields.io/badge/SLACK-zwave2mqtt.slack.com-red.svg?style=popout&logo=slack&logoColor=red)](https://join.slack.com/t/zwave2mqtt/shared_invite/enQtNjc4NjgyNjc3NDI2LTc3OGQzYmJlZDIzZTJhMzUzZWQ3M2Q3NThmMjY5MGY1MTc4NjFiOWZhZWE5YjNmNGE0OWRjZjJiMjliZGQyYmU "Join channel") -[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/MVg9wc2HE 'Buy Me A Coffee') +[![Buy Me A Coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/MVg9wc2HE "Buy Me A Coffee") [![dockeri.co](https://dockeri.co/image/zwavejs/zwavejs2mqtt)](https://hub.docker.com/r/zwavejs/zwavejs2mqtt) @@ -40,6 +40,8 @@ Fully configurable Zwave to MQTT **Gateway** and **Control Panel**. - [Kubernetes way](#kubernetes-way) - [NodeJS or PKG version](#nodejs-or-pkg-version) - [Reverse Proxy Setup](#reverse-proxy-setup) + - [Why Zwavejs](#why-zwavejs) + - [Migrating From Zwave2Mqtt](#migrating-from-zwave2mqtt) - [:nerd_face: Development](#nerd_face-development) - [Developing against a different backend](#developing-against-a-different-backend) - [:wrench: Usage](#wrench-usage) @@ -51,7 +53,7 @@ Fully configurable Zwave to MQTT **Gateway** and **Control Panel**. - [:file_folder: Nodes Management](#file_folder-nodes-management) - [Add a node](#add-a-node) - [Remove a node](#remove-a-node) - - [Replace failed node (NOT IMPLEMENTED YET)](#replace-failed-node-not-implemented-yet) + - [Replace failed node](#replace-failed-node) - [Remove a failed node](#remove-a-failed-node) - [:star: Features](#star-features) - [:robot: Home Assistant integration (BETA)](#robot-home-assistant-integration-beta) @@ -60,7 +62,6 @@ Fully configurable Zwave to MQTT **Gateway** and **Control Panel**. - [Edit existing component](#edit-existing-component) - [Add new component](#add-new-component) - [Custom Components](#custom-components) - - [Identify the Device id](#identify-the-device-id) - [Thermostats](#thermostats) - [Fans](#fans) - [Thermostats with Fans](#thermostats-with-fans) @@ -150,6 +151,28 @@ kubectl apply -k https://raw.githubusercontent.com/zwave-js/zwavejs2mqtt/master/ If you need to setup ZWave To MQTT behind a reverse proxy that needs a _subpath_ to work, take a look at [the reverse proxy configuration docs](docs/subpath.md). +## Why Zwavejs + +1. Entirely written in JS (Typescript). This is good for many reasons: + - We can drop the `node-openzwave-shared` that is maintained by me but would need a complete refactor and it's hard to maintain both projects. + - It will not require to compile OZW and we can have more control of updates/versions with zwavejs releases. + - JS it's straightforward to debug all through the stack rather than a black box that is abstracted by another library +2. Better support/collaboration: OZW is widely used and the author had many other related projects to maintain/support causing many delays or even no responses at all to some issues. Zwave is a good protocol but there are many devices compatibility issues and most of the issues on z2m were related to them. It's become really clear from the time building and maintaining z2m that the community is really important and have found working with @AlCalzone and the growing dev community around zwavejs to be really beneficial for fast paced change and this project is fully embraced by that community too. +3. Its device database keeps growing and uses configurations imported both from OpenHAB and OZW (ozw db import is waiting to be approved check updates [here](https://github.com/OpenZWave/open-zwave/issues/2461)). +4. Better code testing and overall features: it supports OTA Updates, Secure Node Replace and there is a work in progress for security S2 that are not supported by OZW + +### Migrating From Zwave2Mqtt + +For everyone that is coming from Zwave2Mqtt there will be some breaking changes: + +- `settings.json` are almost the same, you can easily export them from Z2M and import them in ZJS2M by using the `Export` and `Import` buttons in Settings tab. After importing them you only will have to edit some Zwave settings and all Gateway Values entries as now the value ids have changed +- `scenes.json` need to be rewrited for the same reason as valueIds have changed. I suggest to manually recreate them using the UI and trash the old one +- You cannot use the old OZW cache file but don't worry it will be automatially generated and the nodes names/locations will be restored from `nodes.json` file +- `nodes.json` can be imported but you will have to manually edit it and delete all nodes `hassDevices` as them will not work in the new implementation (alternatively instead of deleting you can manually convert them, see next steps) +- Values ids unique strings have changed, in Z2M valueIds were identified by `///` now them are `////` where `property` and `propertyKey` (can be undefined) can be both numbers or strings based on the value. So essentially if you are using Hass or Mqtt functions all topics will change, [here](https://github.com/zwave-js/zwavejs2mqtt/pull/20/files#diff-4a25087ac983e835241cfb02c43c408df47b81f77546ef07c4dcfe9acf019eeeR4) you can see how I have translated some valueids of `devices.js` from the old format to the new one. +- List values are no longer handled as `strings` but as `numbers` by default. For example to change a Thermostat Mode in Z2M you used to send `Heat` or `Off` now you will need to send `1` (Heat) or `0` (Off). This will make it lot easier to handle list values +- All bitmask values are now splitted in separeted values and are booleans. The same for rgb colors, now all colors will be handled separately, there will be a valueId to control Red (0-255), another for Green (0-255) and so on... There is an open issue on zwavejs to also create a valueId with the hex color string. Also now some values are splitted in `targetValue` and `currentValue`, the first one is used to send commands the second one to see the actual state. This could seems tricky at first sight but there are some reasons behind this. + ## :nerd_face: Development Developers who wants to debug the application have to open 2 terminals. @@ -376,9 +399,9 @@ To add a node using the UI, go to Control Panel and from the actions dropdown me To remove a node using the UI, go to Control Panel and from the actions dropdown menu select `Start exclusion`, click send (:airplane:) button to enable the exclusion mode in your controller and enable the exclusion mode in your device to. `Controller status` should show `Exclusion started` when exclusion has been successfully enabled on the controller. Wait few seconds and your node will be removed from the table. -### Replace failed node (NOT IMPLEMENTED YET) +### Replace failed node -To replace a failed node from the UI you have to use the command `Replace Failed Node`, if everything is ok the controller will start inclusion mode and status will be `Waiting`, now enable inclusion on your device to add it to the network by replacing the failed one. +To replace a failed node from the UI you have to use the command `Replace Failed Node`, the controller will start inclusion mode and status will be `Waiting`, a popup will ask you if you want to start it in `Secure mode`. Now enable inclusion on your device to add it to the network by replacing the failed one. ### Remove a failed node @@ -428,18 +451,18 @@ mqtt: discovery_prefix: broker: [YOUR MQTT BROKER] # Remove if you want to use builtin-in MQTT broker birth_message: - topic: 'hass/status' # or homeassistant/status if z2m version >= 4.0.0 - payload: 'online' + topic: "homeassistant/status" + payload: "online" will_message: - topic: 'hass/status' # or homeassistant/status if z2m version >= 4.0.0 - payload: 'offline' + topic: "homeassistant/status" + payload: "offline" ``` Mind you that if you want to use the embedded broker of Home Assistant you have to [follow this guide](https://www.home-assistant.io/docs/mqtt/broker#embedded-broker). zwavejs2mqtt is expecting Home Assistant to send it's birth/will -messages to `hass/status` (or `homeassistant/status` if z2m version >= 4.0.0). Be sure to add this to your `configuration.yaml` if you want +messages to `homeassistant/status`. Be sure to add this to your `configuration.yaml` if you want zwavejs2mqtt to resend the cached values when Home Assistant restarts. zwavejs2mqtt try to do its best to guess how to map devices from Zwave to HASS. At the moment it try to guess the device to generate based on zwave values command classes, index and units of the value. When the discovered device doesn't fit your needs you can you can set custom a `device_class` to values using Gateway value table. @@ -471,59 +494,43 @@ If no device is selected you can manually insert a device JSON configuration. If ### Custom Components -At the moment auto discovery just creates components like `sensor`, `cover` `binary_sensor` and `switch`. For more complex components like `climate` and `fan` you need to provide a configuration. Components configurations are stored in `hass/devices.js` file. Here are contained all components that Zwave2MQTT needs to create for each Zwave device type. The key is the Zwave device unique id (`--`) the value is an array with all HASS components to create for that Zwave Device. - -**UPDATE**: Starting from version 2.0.7 you can specify your custom devices configuration inside `store/customDevices(.js|.json)` file. This allows users that use Docker to create their custom hass devices configuration without the need to build a new container. If using `.json` format zwavejs2mqtt will watch for file changes and automatically load new components on runtime without need to restart the application. - -> ONCE YOU SUCCESSFULLY INTEGRATE NEW COMPONENTS PLEASE SEND A PR! - -#### Identify the Device id - -Starting from version 2.2.0 device id is shown on node tab of control panel before the inputs for update the node name and locations. - -Before version 2.2.0 you can get the device id in this ways: - -First (and easier) option is to add a random value in gateway values table for the desired device, the device id will be visible in first column of the table (`Devices`) between square brackets `[] Device Name` +At the moment auto discovery just creates components like `sensor`, `cover` `binary_sensor` and `switch`. For more complex components like `climate` and `fan` you need to provide a configuration. Components configurations are stored in `hass/devices.js` file. Here are contained all components that zwavejs2mqtt needs to create for each Zwave device type. The key is the Zwave **device id**(`--`) the value is an array with all HASS components to create for that Zwave Device. -Second option would be to retrieve it from [here](https://github.com/zwave-js/open-zwave/blob/master/config/manufacturer_specific.xml). Each device has Manufacturerid, product id and a product type in **HEX format** and needs to be converted in decimal: +To get the **Device id** of a specific node go to Control Panel, select a node in the table and select the Node tab, it will be displayed under Node Actions dropdown menu. -```xml - - - - - -``` - -In this example, if we have choose `Heatit Thermostat TF 056`: - -- Manufacturer Id: `19b` --> `411` -- Product Id: `202` --> `514` -- Product type: `3` --> `3` +You can specify your custom devices configuration inside `store/customDevices(.js|.json)` file. This allows users that use Docker to create their custom hass devices configuration without the need to build a new container. If using `.json` format zwavejs2mqtt will watch for file changes and automatically load new components on runtime without need to restart the application. -So in decimal format will become: `411-514-3`. This is the device id of `Heatit Thermostat TF 056` +> ONCE YOU SUCCESSFULLY INTEGRATE NEW COMPONENTS PLEASE SEND A PR! #### Thermostats ```js -{ // Heatit Thermostat TF 021 (ThermoFloor AS) - "type": "climate", - "object_id": "thermostat", - "values": ["64-1-0", "49-1-1", "67-1-1", "67-1-2"], - "mode_map": {"off": "Off", "heat": "Heat (Default)", "cool": "Cool"}, - "setpoint_topic": { "Heat (Default)": "67-1-1", "Cool": "67-1-2" }, - "default_setpoint": "67-1-1", - "discovery_payload": { - "min_temp": 15, - "max_temp": 30, - "modes": ["off", "heat", "cool"], - "mode_state_topic": "64-1-0", - "mode_command_topic": true, - "current_temperature_topic": "49-1-1", - "current_temperature_template": "{{ value_json.value }}", - "temperature_state_template": "{{ value_json.value }}", - "temperature_command_topic": true - } +{ + type: 'climate', + object_id: 'thermostat', + values: [ + '64-0-mode', + '49-0-Air temperature', + '67-0-setpoint-1', + '67-0-setpoint-2' + ], + mode_map: { off: 0, heat: 1, cool: 2 }, + setpoint_topic: { + 'Heat (Default)': '67-0-setpoint-1', + Cool: '67-0-setpoint-2' + }, + default_setpoint: '67-0-setpoint-1', + discovery_payload: { + min_temp: 15, + max_temp: 30, + modes: ['off', 'heat', 'cool'], + mode_state_topic: '64-0-mode', + mode_command_topic: true, + current_temperature_topic: '49-0-Air temperature', + current_temperature_template: '{{ value_json.value }}', + temperature_state_template: '{{ value_json.value }}', + temperature_command_topic: true + } } ``` @@ -531,13 +538,13 @@ So in decimal format will become: `411-514-3`. This is the device id of `Heatit - **object_id**: The unique id of this object (must be unique for the device) - **values**: Array of values used by this component - **mode_map**: Key-Value object where keys are [MQTT Climate](https://www.home-assistant.io/components/climate.mqtt/) modes and values are the matching thermostat modes values -- **setpoint_topic**: Key-Value object where keys are the modes of the Zwave thermostat and values are the matching setpoint `value_id` (use this if your thermostat has more than one setpoint) +- **setpoint_topic**: Key-Value object where keys are the modes of the Zwave thermostat and values are the matching setpoint `value id` (use this if your thermostat has more than one setpoint) - **default_setpoint**: The default thermostat setpoint. - **discovery_payload**: The payload sent to hass to discover this device. Check [here](https://www.home-assistant.io/integrations/climate.mqtt/) for a list with all supported options - **min_temp/max_temp**: Min/Max temperature of the thermostat - **modes**: Array of Hass Climate supported modes. Allowed values are `[“auto”, “off”, “cool”, “heat”, “dry”, “fan_only”]` - - **mode_state_topic**: `value_id` of mode value - - **current_temperature_topic**: `value_id` of current temperature value + - **mode_state_topic**: `value id` of mode value + - **current_temperature_topic**: `value id` of current temperature value - **current_temperature_template/temperature_state_template**: Template used to fetch the value from the MQTT payload - **temperature_command_topic/mode_command_topic**: If true this values are subscribed to this topics to send commands from Hass to change this values @@ -546,24 +553,26 @@ Thermostats are most complex components to create, in this device example the se #### Fans ```js -{ // GE 1724 Dimmer - "type": "fan", - "object_id": "dimmer", - "values": ["38-1-0"], - "discovery_payload": { - "command_topic": "38-1-0", - "speed_command_topic": "38-1-0", - "speed_state_topic": "38-1-0", - "state_topic": "38-1-0", - "speeds": ["off", "low", "medium", "high"], - "payload_low_speed": 24, - "payload_medium_speed": 50, - "payload_high_speed": 99, - "payload_off": 0, - "payload_on": 99, - "state_value_template": "{% if (value_json.value | int) == 0 %} 0 {% else %} 99 {% endif %}", - "speed_value_template": "{% if (value_json.value | int) == 25 %} 24 {% elif (value_json.value | int) == 51 %} 50 {% elif (value_json.value | int) == 99 %} 99 {% else %} 0 {% endif %}" - } +{ + type: 'fan', + object_id: 'dimmer', + values: ['38-0-currentValue', '38-0-targetValue'], + discovery_payload: { + command_topic: '38-0-currentValue', + speed_command_topic: '38-0-targetValue', + speed_state_topic: '38-0-currentValue', + state_topic: '38-0-currentValue', + speeds: ['off', 'low', 'medium', 'high'], + payload_low_speed: 24, + payload_medium_speed: 50, + payload_high_speed: 99, + payload_off: 0, + payload_on: 255, + state_value_template: + '{% if (value_json.value | int) == 0 %} 0 {% else %} 255 {% endif %}', + speed_value_template: + '{% if (value_json.value | int) == 0 %} 0 {% elif (value_json.value | int) <= 32 %} 24 {% elif (value_json.value | int) <= 66 %} 50 {% elif (value_json.value | int) <= 99 %} 99 {% endif %}' + } } ``` @@ -582,58 +591,47 @@ Thermostats are most complex components to create, in this device example the se The main template is like the thermostat template. The things to add are: ```js -{ // GoControl GC-TBZ48 (Linear Nortek Security Control LLC) - "type": "climate", - "object_id": "thermostat", - "values": [ - "49-1-1", - "64-1-0", - "66-1-0", // <-- add fan values - "67-1-1", - "67-1-2", - "68-1-0" // <-- add fan values - ], - "fan_mode_map": { // <-- add fan modes map - "on": "On", - "auto": "Auto" - }, - "mode_map": { - "off": "Off", - "heat": "Heat", - "cool": "Cool", - "auto": "Auto" - }, - "setpoint_topic": { - "Heat": "67-1-1", - "Cool": "67-1-2" - }, - "default_setpoint": "67-1-1", - "discovery_payload": { - "min_temp": 60, - "max_temp": 85, - "modes": [ - "off", - "heat", - "cool", - "auto" - ], - "fan_modes": [ // <-- add fan supported modes - "on", - "auto" - ], - "action_topic": "66-1-0", - "mode_state_topic": "64-1-0", - "mode_command_topic": true, - "current_temperature_topic": "49-1-1", - "current_temperature_template": "{{ value_json.value }}", - "temperature_state_template": "{{ value_json.value }}", - "temperature_low_command_topic": true, - "temperature_low_state_template": "{{ value_json.value }}", - "temperature_high_command_topic": true, - "temperature_high_state_template": "{{ value_json.value }}", - "fan_mode_command_topic": true, - "fan_mode_state_topic": "68-1-0" // <-- add fan state topic - } +{ + type: 'climate', + object_id: 'thermostat', + values: [ + '49-0-Air temperature', + '64-0-mode', + '66-0-state', // <-- add fan values + '67-0-setpoint-1', + '67-0-setpoint-2', + '68-0-mode' // <-- add fan values + ], + mode_map: { + off: 0, + heat: 1, + cool: 2 + }, + fan_mode_map: { // <-- add fan mode_map + auto: 0, + on: 1 + }, + setpoint_topic: { + Heat: '67-0-setpoint-1', + Cool: '67-0-setpoint-2' + }, + default_setpoint: '67-0-setpoint-1', + discovery_payload: { + min_temp: 50, + max_temp: 85, + modes: ['off', 'heat', 'cool'], + fan_modes: ['auto', 'on'], // <-- add fan supported modes + action_topic: '66-0-state', + action_template: '{{ value_json.value | lower }}', + current_temperature_topic: '49-0-Air temperature', + current_temperature_template: '{{ value_json.value }}', + fan_mode_state_topic: '68-0-mode', // <-- add fan state topic + fan_mode_command_topic: true, // <-- add fan command topic + mode_state_topic: '64-0-mode', + mode_command_topic: true, + temperature_state_template: '{{ value_json.value }}', + temperature_command_topic: true + } } ``` @@ -802,28 +800,18 @@ _**Note**: Each one of the following environment variables corresponds to their > A: My device is X and has been discovered as Y, why? -**B: Hass Discovery is not easy, zwave have many different devices with different values. To try to understand how to discover a specific value I have used** [this](https://github.com/zwave-js/open-zwave/blob/master/config/Localization.xml) **file that shows what kind of value is expected based on value class and index. Unfortunately not all devices respect this specifications so for those cases I have created Hass Devices table where you can manually fix the discovery payload and than save it to make it persistent. I have also created a file** `/hass/devices.js` **where I place all devices specific values configuration, your contribution is needed there, so submit a PR with your files specification to help it grow.** +**B: Hass Discovery is not easy, zwave have many different devices with different values. Unfortunately not all devices respect specifications so for those cases I have created Hass Devices table where you can manually fix the discovery payload and than save it to make it persistent. I have also created a file `/hass/devices.js` where I place all devices specific values configuration, your contribution is needed there, so submit a PR with your files specification to help it grow.** ## :pray: Thanks Thanks to this people for help with issues tracking and contributions: - [**Chris Nesbitt-Smith**](https://github.com/chrisns) +- [**AlCalzone**](https://github.com/AlCalzone) - [**Jorge Schrauwen**](https://github.com/sjorge) - [**Jay**](https://github.com/jshridha) - [**Thiago Oliveira**](https://github.com/chilicheech) -## :pencil: TODOs - -- [x] Better logging -- [x] Dockerize application -- [x] Package application with PKG -- [x] HASS integration, check [zigbee2mqtt](https://github.com/Koenkk/zigbee2mqtt/blob/master/lib/extension/homeassistant.js) -- [ ] Add unit test -- [ ] JSON validator for settings and scenes -- [ ] Better nodes status management using 'testNode' -- [x] Network graph to show neighbours using [vue-d3-network](https://github.com/emiliorizzo/vue-d3-network) - ## :bowtie: Author [Daniel Lando](https://github.com/robertsLando) diff --git a/app.js b/app.js index 2d5b89f3254..f4fca76cb02 100644 --- a/app.js +++ b/app.js @@ -103,25 +103,25 @@ app.startSocket = function (server) { socket.on('HASS_API', async function (data) { switch (data.apiName) { case 'delete': - gw.publishDiscovery(data.device, data.node_id, true, true) + gw.publishDiscovery(data.device, data.nodeId, true, true) break case 'discover': - gw.publishDiscovery(data.device, data.node_id, false, true) + gw.publishDiscovery(data.device, data.nodeId, false, true) break case 'rediscoverNode': - gw.rediscoverNode(data.node_id) + gw.rediscoverNode(data.nodeId) break case 'disableDiscovery': - gw.disableDiscovery(data.node_id) + gw.disableDiscovery(data.nodeId) break case 'update': - gw.zwave.updateDevice(data.device, data.node_id) + gw.zwave.updateDevice(data.device, data.nodeId) break case 'add': - gw.zwave.addDevice(data.device, data.node_id) + gw.zwave.addDevice(data.device, data.nodeId) break case 'store': - await gw.zwave.storeDevices(data.devices, data.node_id, data.remove) + await gw.zwave.storeDevices(data.devices, data.nodeId, data.remove) break } }) diff --git a/hass/configurations.js b/hass/configurations.js index e0b0e31a597..580fde8e6fa 100755 --- a/hass/configurations.js +++ b/hass/configurations.js @@ -3,9 +3,9 @@ module.exports = { // Binary sensor https://www.home-assistant.io/components/binary_sensor.mqtt - binary_sensor_occupancy: { + binary_sensor_motion: { type: 'binary_sensor', - object_id: 'occupancy', + object_id: 'motion', discovery_payload: { payload_on: true, payload_off: false, @@ -43,9 +43,9 @@ module.exports = { device_class: 'lock' } }, - binary_sensor_water_leak: { + binary_sensor_water: { type: 'binary_sensor', - object_id: 'water_leak', + object_id: 'water', discovery_payload: { payload_on: true, payload_off: false, @@ -73,9 +73,19 @@ module.exports = { device_class: 'gas' } }, - binary_sensor_carbon_monoxide: { + binary_sensor_co: { type: 'binary_sensor', - object_id: 'carbon_monoxide', + object_id: 'co', + discovery_payload: { + payload_on: true, + payload_off: false, + value_template: '{{ value_json.value }}', + device_class: 'safety' + } + }, + binary_sensor_co2: { + type: 'binary_sensor', + object_id: 'co2', discovery_payload: { payload_on: true, payload_off: false, diff --git a/hass/devices.js b/hass/devices.js index f7b465d8b29..d48d508ae80 100644 --- a/hass/devices.js +++ b/hass/devices.js @@ -3,12 +3,12 @@ const FAN_DIMMER = { type: 'fan', object_id: 'dimmer', - values: ['38-1-0'], + values: ['38-0-currentValue', '38-0-targetValue'], discovery_payload: { - command_topic: '38-1-0', - speed_command_topic: '38-1-0', - speed_state_topic: '38-1-0', - state_topic: '38-1-0', + command_topic: '38-0-currentValue', + speed_command_topic: '38-0-targetValue', + speed_state_topic: '38-0-currentValue', + state_topic: '38-0-currentValue', speeds: ['off', 'low', 'medium', 'high'], payload_low_speed: 24, payload_medium_speed: 50, @@ -26,33 +26,40 @@ const FAN_DIMMER = { const THERMOSTAT_2GIG = { type: 'climate', object_id: 'thermostat', - values: ['49-1-1', '64-1-0', '66-1-0', '67-1-1', '67-1-2', '68-1-0'], + values: [ + '49-0-Air temperature', + '64-0-mode', + '66-0-state', + '67-0-setpoint-1', + '67-0-setpoint-2', + '68-0-mode' + ], mode_map: { - off: 'Off', - heat: 'Heat', - cool: 'Cool' + off: 0, + heat: 1, + cool: 2 }, fan_mode_map: { - auto: 'Auto Low', - on: 'On Low' + auto: 0, + on: 1 }, setpoint_topic: { - Heat: '67-1-1', - Cool: '67-1-2' + Heat: '67-0-setpoint-1', + Cool: '67-0-setpoint-2' }, - default_setpoint: '67-1-1', + default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 50, max_temp: 85, modes: ['off', 'heat', 'cool'], fan_modes: ['auto', 'on'], - action_topic: '66-1-0', + action_topic: '66-0-state', action_template: '{{ value_json.value | lower }}', - current_temperature_topic: '49-1-1', + current_temperature_topic: '49-0-Air temperature', current_temperature_template: '{{ value_json.value }}', - fan_mode_state_topic: '68-1-0', + fan_mode_state_topic: '68-0-mode', fan_mode_command_topic: true, - mode_state_topic: '64-1-0', + mode_state_topic: '64-0-mode', mode_command_topic: true, temperature_state_template: '{{ value_json.value }}', temperature_command_topic: true @@ -64,17 +71,25 @@ const THERMOSTAT_2GIG = { const STELLA_ZWAVE = { type: 'climate', object_id: 'thermostat', - values: ['64-1-0', '49-1-1', '67-1-1', '67-1-11'], - mode_map: { off: 'Off', heat: 'Comfort', cool: 'Energy Saving' }, - setpoint_topic: { Comfort: '67-1-1', 'Energy Saving': '67-1-11' }, - default_setpoint: '67-1-1', + values: [ + '64-0-mode', + '49-0-Air temperature', + '67-0-setpoint-1', + '67-0-setpoint-11' + ], + mode_map: { off: 0, heat: 1, cool: 11 }, + setpoint_topic: { + Comfort: '67-0-setpoint-1', + 'Energy Saving': '67-0-setpoint-11' + }, + default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 0, max_temp: 50, modes: ['off', 'heat', 'cool'], - mode_state_topic: '64-1-0', + mode_state_topic: '64-0-mode', mode_command_topic: true, - current_temperature_topic: '49-1-1', + current_temperature_topic: '49-0-Air temperature', temp_step: 0.5, current_temperature_template: '{{ value_json.value }}', temperature_state_template: '{{ value_json.value }}', @@ -86,17 +101,25 @@ const STELLA_ZWAVE = { const SPIRIT_ZWAVE_PLUS = { type: 'climate', object_id: 'thermostat', - values: ['64-1-0', '49-1-1', '67-1-1', '67-1-11'], - mode_map: { off: 'Off', heat: 'Heat', cool: 'Heat Eco' }, - setpoint_topic: { Heat: '67-1-1', 'Heat Eco': '67-1-11' }, - default_setpoint: '67-1-1', + values: [ + '64-0-mode', + '49-0-Air temperature', + '67-0-setpoint-1', + '67-0-setpoint-11' + ], + mode_map: { off: 0, heat: 1, cool: 11 }, + setpoint_topic: { + Heat: '67-0-setpoint-1', + 'Heat Eco': '67-0-setpoint-11' + }, + default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 8, max_temp: 28, modes: ['off', 'heat', 'cool'], - mode_state_topic: '64-1-0', + mode_state_topic: '64-0-mode', mode_command_topic: true, - current_temperature_topic: '49-1-1', + current_temperature_topic: '49-0-Air temperature', temp_step: 0.5, current_temperature_template: '{{ value_json.value }}', temperature_state_template: '{{ value_json.value }}', @@ -107,15 +130,15 @@ const SPIRIT_ZWAVE_PLUS = { const DANFOSS_TRV_ZWAVE = { type: 'climate', object_id: 'thermostat', - values: ['49-1-1', '67-1-1'], - setpoint_topic: { Heat: '67-1-1' }, - default_setpoint: '67-1-1', + values: ['49-0-Air temperature', '67-0-setpoint-1'], + setpoint_topic: { Heat: '67-0-setpoint-1' }, + default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 4, max_temp: 28, mode_command_topic: false, temp_step: 0.5, - current_temperature_topic: '49-1-1', + current_temperature_topic: '49-0-Air temperature', current_temperature_template: '{{ value_json.value }}', temperature_state_template: '{{ value_json.value }}', temperature_command_topic: true @@ -125,11 +148,11 @@ const DANFOSS_TRV_ZWAVE = { const COVER = { type: 'cover', object_id: 'position', - values: ['38-1-0'], + values: ['38-0-currentValue', '38-0-targetValue'], discovery_payload: { - command_topic: '38-1-0', - position_topic: '38-1-0', - set_position_topic: '38-1-0', + command_topic: '38-0-targetValue', + position_topic: '38-0-currentValue', + set_position_topic: '38-0-targetValue', value_template: '{{ (value_json.value / 99 * 100) | round(0) }}', position_open: 99, position_closed: 0, @@ -143,18 +166,18 @@ module.exports = { { type: 'climate', object_id: 'HRT4-ZW', - values: ['49-1-1', '67-1-1'], + values: ['49-0-Air temperature', '67-0-setpoint-1'], mode_map: { - off: 'Off', - heat: 'Heating' + off: 0, + heat: 1 }, - setpoint_topic: { Heat: '67-1-1' }, - default_setpoint: '67-1-1', + setpoint_topic: { Heat: '67-0-setpoint-1' }, + default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 5, max_temp: 30, modes: ['off', 'heat'], - current_temperature_topic: '49-1-1', + current_temperature_topic: '49-0-Air temperature', current_temperature_template: '{{ value_json.value }}', temperature_state_template: '{{ value_json.value }}', temperature_command_topic: true @@ -166,17 +189,25 @@ module.exports = { { type: 'climate', object_id: 'thermostat', - values: ['64-1-0', '49-1-1', '67-1-1', '67-1-2'], - mode_map: { off: 'Off', heat: 'Heat (Default)', cool: 'Cool' }, - setpoint_topic: { 'Heat (Default)': '67-1-1', Cool: '67-1-2' }, - default_setpoint: '67-1-1', + values: [ + '64-0-mode', + '49-0-Air temperature', + '67-0-setpoint-1', + '67-0-setpoint-2' + ], + mode_map: { off: 0, heat: 1, cool: 2 }, + setpoint_topic: { + 'Heat (Default)': '67-0-setpoint-1', + Cool: '67-0-setpoint-2' + }, + default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 15, max_temp: 30, modes: ['off', 'heat', 'cool'], - mode_state_topic: '64-1-0', + mode_state_topic: '64-0-mode', mode_command_topic: true, - current_temperature_topic: '49-1-1', + current_temperature_topic: '49-0-Air temperature', current_temperature_template: '{{ value_json.value }}', temperature_state_template: '{{ value_json.value }}', temperature_command_topic: true @@ -188,17 +219,22 @@ module.exports = { { type: 'climate', object_id: 'thermostat', - values: ['64-1-0', '49-1-1', '67-1-1', '67-1-2'], - mode_map: { off: 'Off', heat: 'Heat', cool: 'Cool' }, - setpoint_topic: { Heat: '67-1-1', Cool: '67-1-2' }, - default_setpoint: '67-1-1', + values: [ + '64-0-mode', + '49-0-Air temperature', + '67-0-setpoint-1', + '67-0-setpoint-2' + ], + mode_map: { off: 0, heat: 1, cool: 2 }, + setpoint_topic: { Heat: '67-0-setpoint-1', Cool: '67-0-setpoint-2' }, + default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 15, max_temp: 30, modes: ['off', 'heat', 'cool'], - mode_state_topic: '64-1-0', + mode_state_topic: '64-0-mode', mode_command_topic: true, - current_temperature_topic: '49-1-1', + current_temperature_topic: '49-0-Air temperature', current_temperature_template: '{{ value_json.value }}', temperature_state_template: '{{ value_json.value }}', temperature_command_topic: true @@ -210,13 +246,13 @@ module.exports = { { type: 'light', object_id: 'rgbw_bulb', - values: ['38-1-0', '51-1-0'], + values: ['38-0-currentValue', '38-0-targetValue', '51-1-0'], // FIXME: Handle color CC discovery_payload: { - state_topic: '38-1-0', - command_topic: '38-1-0', + state_topic: '38-0-currentValue', + command_topic: '38-0-targetValue', on_command_type: 'brightness', - brightness_state_topic: '38-1-0', - brightness_command_topic: '38-1-0', + brightness_state_topic: '38-0-currentValue', + brightness_command_topic: '38-0-targetValue', state_value_template: '{{ "OFF" if value_json.value == 0 else "ON" }}', brightness_value_template: '{{ (value_json.value) | round(0) }}', brightness_scale: '99', @@ -240,14 +276,14 @@ module.exports = { { type: 'climate', object_id: 'pool_thermostat', - values: ['49-1-1', '67-1-1'], - default_setpoint: '67-1-1', + values: ['49-0-Air temperature', '67-0-setpoint-1'], + default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 40, max_temp: 104, modes: ['heat'], temperature_unit: 'F', - current_temperature_topic: '49-1-1', + current_temperature_topic: '49-0-Air temperature', current_temperature_template: '{{ value_json.value }}', temperature_command_topic: true, temperature_state_template: '{{ value_json.value }}' @@ -256,14 +292,14 @@ module.exports = { { type: 'climate', object_id: 'spa_thermostat', - values: ['49-1-1', '67-1-7'], - default_setpoint: '67-1-7', + values: ['49-0-Air temperature', '67-0-Furnace'], + default_setpoint: '67-0-Furnace', discovery_payload: { min_temp: 40, max_temp: 104, modes: ['heat'], temperature_unit: 'F', - current_temperature_topic: '49-1-1', + current_temperature_topic: '49-0-Air temperature', current_temperature_template: '{{ value_json.value }}', temperature_command_topic: true, temperature_state_template: '{{ value_json.value }}' @@ -272,60 +308,60 @@ module.exports = { { type: 'switch', object_id: 'circuit_1', - values: ['37-1-0'], + values: ['37-1-currentValue', '37-1-targetValue'], discovery_payload: { payload_off: false, payload_on: true, - state_topic: '37-1-0', - command_topic: '37-1-0', + state_topic: '37-1-currentValue', + command_topic: '37-1-targetValue', value_template: '{{ value_json.value }}' } }, { type: 'switch', object_id: 'circuit_2', - values: ['37-2-0'], + values: ['37-2-currentValue', '37-2-targetValue'], discovery_payload: { payload_off: false, payload_on: true, - state_topic: '37-2-0', - command_topic: '37-2-0', + state_topic: '37-2-currentValue', + command_topic: '37-2-targetValue', value_template: '{{ value_json.value }}' } }, { type: 'switch', object_id: 'circuit_3', - values: ['37-3-0'], + values: ['37-3-currentValue', '37-3-targetValue'], discovery_payload: { payload_off: false, payload_on: true, - state_topic: '37-3-0', - command_topic: '37-3-0', + state_topic: '37-3-currentValue', + command_topic: '37-3-targetValue', value_template: '{{ value_json.value }}' } }, { type: 'switch', object_id: 'circuit_4', - values: ['37-4-0'], + values: ['37-4-currentValue', '37-4-targetValue'], discovery_payload: { payload_off: false, payload_on: true, - state_topic: '37-4-0', - command_topic: '37-4-0', + state_topic: '37-1-currentValue', + command_topic: '37-4-targetValue', value_template: '{{ value_json.value }}' } }, { type: 'switch', object_id: 'circuit_5', - values: ['37-5-0'], + values: ['37-5-currentValue', '37-5-targetValue'], discovery_payload: { payload_off: false, payload_on: true, - state_topic: '37-5-0', - command_topic: '37-5-0', + state_topic: '37-5-currentValue', + command_topic: '37-5-targetValue', value_template: '{{ value_json.value }}' } } diff --git a/lib/Constants.js b/lib/Constants.js index 28b4b98de82..ff2cc18940e 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -324,7 +324,7 @@ module.exports = { 0x63: 'user_code', 0x66: 'barrier_operator', 0x70: 'configuration', - 0x71: 'alarm', + 0x71: 'notification', 0x72: 'manufacturer_specific', 0x73: 'powerlevel', 0x75: 'protection', diff --git a/lib/Gateway.js b/lib/Gateway.js index 006f3112878..0ef5843a238 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -9,6 +9,8 @@ const path = require('path') const reqlib = require('app-root-path').require const utils = reqlib('/lib/utils.js') const EventEmitter = require('events') +const { AlarmSensorType } = require('zwave-js') +const { CommandClasses } = require('@zwave-js/core') const Constants = reqlib('/lib/Constants.js') const debug = reqlib('/lib/debug')('Gateway') const inherits = require('util').inherits @@ -346,7 +348,7 @@ function onNodeStatus (node) { // discover node values (that are not part of a device) for (const id in node.values) { - this.discoverValue(node, node.values[id]) + this.discoverValue(node, id) } } @@ -537,9 +539,9 @@ function deviceInfo (node, nodeName) { return { identifiers: ['zwavejs2mqtt_' + this.zwave.homeHex + '_node' + node.id], manufacturer: node.manufacturer, - model: node.product + ' (' + node.productid + ')', + model: node.productDescription + ' (' + node.productLabel + ')', name: nodeName, - sw_version: node.version || version + sw_version: node.firmwareVersion || version } } @@ -563,12 +565,22 @@ function getDiscoveryTopic (hassDevice, nodeName) { * @returns {String} The template to use for the mode */ function getMappedValuesTemplate (modeMap, defaultValue) { - const map = {} - for (const key in modeMap) map[modeMap[key]] = key + const map = [] + // JSON.stringify converts props to strings and this breaks the template + // Error: "0": "off" Working: 0: "off" + for (const key in modeMap) { + map.push( + `${ + typeof modeMap[key] === 'number' + ? modeMap[key] + : '"' + modeMap[key] + '"' + }: "${key}"` + ) + } - return `{{ ${JSON.stringify( - map - )}[value_json.value] | default('${defaultValue}') }}` + return `{{ {${map.join( + ',' + )}}[value_json.value] | default('${defaultValue}') }}` } /** @@ -593,12 +605,17 @@ function setDiscoveryValue (payload, prop, node) { */ Gateway.prototype.parsePayload = function (payload, valueId, valueConf) { try { - payload = payload.hasOwnProperty('value') ? payload.value : payload + payload = + typeof payload === 'object' && payload.hasOwnProperty('value') + ? payload.value + : payload + + const hassDevice = this.discovered[valueId.id] // Hass payload parsing - if (this.discovered[valueId.id]) { + if (hassDevice) { // parse payload for switches - const isDimmer = isRgbDimmer(this.discovered[valueId.id].object_id) + const isDimmer = isRgbDimmer(hassDevice.object_id) if ( (valueId.type === 'boolean' || isDimmer) && @@ -609,29 +626,38 @@ Gateway.prototype.parsePayload = function (payload, valueId, valueConf) { } if (isDimmer) { + // TODO: should we use valueId.max instead of 99 ? if (typeof payload === 'boolean') payload = payload ? 99 : 0 else payload = Math.round((payload / 255) * 99) } // map modes coming from hass - if (valueId.list && valueId.states.find(v => v.value === payload)) { - const hassDevice = this.discovered[valueId.id] - if (hassDevice) { - // for thermostat_fan_mode command class use the fan_mode_map - if (valueId.commandClass === 0x44 && hassDevice.fan_mode_map) { - payload = hassDevice.fan_mode_map[payload] - } else if (hassDevice.mode_map) { - // for other command classes use the mode_map - payload = hassDevice.mode_map[payload] - } + if (valueId.list && isNaN(payload)) { + // for thermostat_fan_mode command class use the fan_mode_map + if ( + valueId.commandClass === CommandClasses['Thermostat Fan Mode'] && + hassDevice.fan_mode_map + ) { + payload = hassDevice.fan_mode_map[payload] + } else if ( + valueId.commandClass === CommandClasses['Thermostat Mode'] && + hassDevice.mode_map + ) { + // for other command classes use the mode_map + payload = hassDevice.mode_map[payload] } } - // switch_toggle_binary and color - if (valueId.commandClass === 0x28) payload = 1 - else if (valueId.commandClass === 0x29) { + if (valueId.commandClass === CommandClasses['Binary Toggle Switch']) { + payload = 1 + } else if ( + valueId.commandClass === CommandClasses['Multilevel Toggle Switch'] + ) { payload = valueId.value > 0 ? 0 : 0xff - } else if (valueId.commandClass === 0x33 && typeof payload === 'string') { + } else if ( + valueId.commandClass === CommandClasses['Color Switch'] && + typeof payload === 'string' + ) { const rgb = payload.split(',') if (rgb.length === 3) { payload = '#' + rgbToHex(rgb[0]) + rgbToHex(rgb[1]) + rgbToHex(rgb[2]) @@ -695,6 +721,12 @@ Gateway.prototype.close = async function () { } } +/** + * Calculates the node topic based on gateway settings + * + * @param {NodeObj} node internal node object + * @returns The node topic + */ Gateway.prototype.nodeTopic = function (node) { const topic = [] @@ -725,7 +757,15 @@ Gateway.prototype.nodeTopic = function (node) { return topic.join('/') } -Gateway.prototype.valueTopic = function (node, valueId, returnObject) { +/** + * Calculates the valueId topic based on gateway settings + * + * @param {NodeObj} node Internal node object + * @param {ValueObj} valueId Internal ValueId object + * @param {boolean} returnObject Set this to true to also return the targetTopic and the valueConf + * @returns The value topic string or an object + */ +Gateway.prototype.valueTopic = function (node, valueId, returnObject = false) { const topic = [] let valueConf @@ -742,6 +782,15 @@ Gateway.prototype.valueTopic = function (node, valueId, returnObject) { topic.push(valueConf.topic) } + let targetTopic + + if (returnObject && valueId.targetValue) { + const targetValue = node.values[valueId.targetValue] + if (targetValue) { + targetTopic = this.valueTopic(node, targetValue, false) + } + } + // if is not in configuration values array get the topic // based on gateway type if manual type this will be skipped if (topic.length === 0) { @@ -785,14 +834,23 @@ Gateway.prototype.valueTopic = function (node, valueId, returnObject) { topic[i] = this.mqtt.cleanName(topic[i]) } - return returnObject - ? { topic: topic.join('/'), valueConf: valueConf } - : topic.join('/') + const toReturn = { + topic: topic.join('/'), + valueConf: valueConf, + targetTopic: targetTopic + } + + return returnObject ? toReturn : toReturn.topic } else { return null } } +/** + * Rediscover all hass devices of this node + * + * @param {number} nodeID + */ Gateway.prototype.rediscoverNode = function (nodeID) { const node = this.zwave.nodes[nodeID] if (node) { @@ -806,13 +864,18 @@ Gateway.prototype.rediscoverNode = function (nodeID) { // discover node values (that are not part of a device) for (const id in node.values) { - this.discoverValue(node, node.values[id]) + this.discoverValue(node, id) } this.zwave.sendToSocket(this.zwave.socketEvents.nodeUpdated, node) } } +/** + * Disable the discovery of all devices of this node + * + * @param {number} nodeID + */ Gateway.prototype.disableDiscovery = function (nodeID) { const node = this.zwave.nodes[nodeID] if (node && node.hassDevices) { @@ -824,6 +887,14 @@ Gateway.prototype.disableDiscovery = function (nodeID) { } } +/** + * Publish a discovery payload to discover a device in hass using mqtt auto discovery + * + * @param {HassDevice} hassDevice The hass device configuration to use for the discovery + * @param {number} nodeId The node id + * @param {boolean} deleteDevice Enable this to remove the selected device from hass discovery + * @param {boolean} update Update an hass device of a specific node in zwaveClient and send the event to socket + */ Gateway.prototype.publishDiscovery = function ( hassDevice, nodeId, @@ -831,39 +902,33 @@ Gateway.prototype.publishDiscovery = function ( update ) { try { - if (hassDevice.ignoreDiscovery) { - return - } else { - hassDevice.ignoreDiscovery = false - } - - // set values as discovered - for (let k = 0; k < hassDevice.values.length; k++) { - this.discovered[nodeId + '-' + hassDevice.values[k]] = hassDevice - } - - if (this.config.payloadType === 2) { - // Payload is set to "Just Value" - const p = hassDevice.discovery_payload - const template = - 'value' + - (p.hasOwnProperty('payload_on') && p.hasOwnProperty('payload_off') - ? " == 'true'" - : '') - - for (const k in p) { - if (typeof p[k] === 'string') { - p[k] = p[k].replace(/value_json\.value/g, template) + this.setDiscovery(nodeId, hassDevice, deleteDevice) + + // don't discovery this device when ignore is true + if (!hassDevice.ignoreDiscovery) { + if (this.config.payloadType === 2) { + // Payload is set to "Just Value" + const p = hassDevice.discovery_payload + const template = + 'value' + + (p.hasOwnProperty('payload_on') && p.hasOwnProperty('payload_off') + ? " == 'true'" + : '') + + for (const k in p) { + if (typeof p[k] === 'string') { + p[k] = p[k].replace(/value_json\.value/g, template) + } } } - } - this.mqtt.publish( - hassDevice.discoveryTopic, - deleteDevice ? '' : hassDevice.discovery_payload, - { qos: 0, retain: this.config.retainedDiscovery || false }, - this.config.discoveryPrefix - ) + this.mqtt.publish( + hassDevice.discoveryTopic, + deleteDevice ? '' : hassDevice.discovery_payload, + { qos: 0, retain: this.config.retainedDiscovery || false }, + this.config.discoveryPrefix + ) + } if (update) { this.zwave.updateDevice(hassDevice, nodeId, deleteDevice) @@ -873,6 +938,32 @@ Gateway.prototype.publishDiscovery = function ( } } +/** + * Set internal discovery reference of a valueId + * + * @param {number} nodeId The node id + * @param {HassDevice} hassDevice Hass device configuration + * @param {boolean} deleteDevice Remove the device from the map + */ +Gateway.prototype.setDiscovery = function ( + nodeId, + hassDevice, + deleteDevice = false +) { + for (let k = 0; k < hassDevice.values.length; k++) { + const vId = nodeId + '-' + hassDevice.values[k] + if (deleteDevice && this.discovered[vId]) { + delete this.discovered[vId] + } else { + this.discovered[vId] = hassDevice + } + } +} + +/** + * Rediscover all nodes and their values/devices + * + */ Gateway.prototype.rediscoverAll = function () { // skip discovery if discovery not enabled if (!this.config.hassDiscovery) return @@ -889,6 +980,12 @@ Gateway.prototype.rediscoverAll = function () { } // end foreach node } +/** + * Discover an hass device (from customDevices.js|json) + * + * @param {NodeObj} node node object + * @param {HassDevice} hassDevice hass device + */ Gateway.prototype.discoverDevice = function (node, hassDevice) { const hassID = hassDevice ? hassDevice.type + '_' + hassDevice.object_id @@ -1021,6 +1118,8 @@ Gateway.prototype.discoverDevice = function (node, hassDevice) { // This configuration is not stored in nodes.json hassDevice.persistent = false + hassDevice.ignoreDiscovery = !!hassDevice.ignoreDiscovery + node.hassDevices[hassID] = hassDevice this.publishDiscovery(hassDevice, node.id) @@ -1031,57 +1130,69 @@ Gateway.prototype.discoverDevice = function (node, hassDevice) { 'Error while discovering device %s of node %d: %s', hassID, node.id, - error.message + error ) } } -Gateway.prototype.discoverValue = function (node, valueId) { - if (this.discovered[valueId.id] || valueId.genre !== 'user') return +/** + * Try to guess the best way to discover this valueId in Hass + * + * @param {NodeObj} node Internal node object + * @param {String} vId value id without the node prefix + */ +Gateway.prototype.discoverValue = function (node, vId) { + const valueId = node.values[vId] + + if (!valueId || this.discovered[valueId.id]) return try { const result = this.valueTopic(node, valueId, true) - const topic = result.topic - - if (!topic) return + if (!result.topic) return const valueConf = result.valueConf + const getTopic = this.mqtt.getTopic(result.topic) + const setTopic = result.targetTopic + ? this.mqtt.getTopic(result.targetTopic, true) + : null + const nodeName = getNodeName(node) - const type = Constants.commandClass(valueId.commandClass) let cfg - switch (type) { - case 'switch_binary': - case 'switch_all': - case 'switch_toggle_binary': - if (valueId.index === 0) { - const rgb = node.values['51-1-0'] - if (rgb) { - cfg = copy(hassCfg.light_rgb_switch) - cfg.discovery_payload.rgb_state_topic = this.mqtt.getTopic( - this.valueTopic(node, rgb) - ) - cfg.discovery_payload.rgb_command_topic = - cfg.discovery_payload.rgb_state_topic + '/set' - } else { - cfg = copy(hassCfg.switch) - } + const cmdClass = valueId.commandClass + + switch (cmdClass) { + case CommandClasses['Binary Switch']: + case CommandClasses['All Switch']: + case CommandClasses['Binary Toggle Switch']: + if (valueId.isCurrentValue) { + // TODO: Needs https://github.com/zwave-js/node-zwave-js/issues/806 + // const rgb = node.values['51-1-0'] + // if (rgb) { + // cfg = copy(hassCfg.light_rgb_switch) + // cfg.discovery_payload.rgb_state_topic = this.mqtt.getTopic( + // this.valueTopic(node, rgb) + // ) + // cfg.discovery_payload.rgb_command_topic = + // cfg.discovery_payload.rgb_state_topic + '/set' + // } else { + cfg = copy(hassCfg.switch) } else return break - case 'barrier_operator': - if (valueId.index === 1) { + case CommandClasses['Barrier Operator']: + if (valueId.isCurrentValue) { cfg = copy(hassCfg.barrier_state) } else return break - case 'switch_multilevel': - case 'switch_toggle_multilevel': - if (valueId.index === 0) { + case CommandClasses['Multilevel Switch']: + case CommandClasses['Multilevel Toggle Switch']: + if (valueId.isCurrentValue) { const specificDeviceClass = Constants.specificDeviceClass( - node.generic_device_class, - node.specific_device_class + node.deviceClass.generic, + node.deviceClass.specific ) // Use a cover_position configuration if ... if ( @@ -1093,12 +1204,11 @@ Gateway.prototype.discoverValue = function (node, valueId) { ].includes(specificDeviceClass) ) { cfg = copy(hassCfg.cover_position) - cfg.discovery_payload.state_topic = this.mqtt.getTopic(topic) - cfg.discovery_payload.command_topic = - cfg.discovery_payload.state_topic + '/set' - cfg.discovery_payload.position_topic = this.mqtt.getTopic(topic) + cfg.discovery_payload.state_topic = getTopic + cfg.discovery_payload.command_topic = setTopic + cfg.discovery_payload.position_topic = getTopic cfg.discovery_payload.set_position_topic = - cfg.discovery_payload.position_topic + '/set' + cfg.discovery_payload.command_topic cfg.discovery_payload.value_template = '{{ value_json.value | round(0) }}' cfg.discovery_payload.position_open = 99 @@ -1107,65 +1217,68 @@ Gateway.prototype.discoverValue = function (node, valueId) { cfg.discovery_payload.payload_close = 0 } else { // ... otherwise use a light dimmer configuration + // TODO: Needs https://github.com/zwave-js/node-zwave-js/issues/806 // brightness level - const rgb = node.values['51-1-0'] - if (rgb) { - cfg = copy(hassCfg.light_rgb_dimmer) - cfg.discovery_payload.rgb_state_topic = this.mqtt.getTopic( - this.valueTopic(node, rgb) - ) - cfg.discovery_payload.rgb_command_topic = - cfg.discovery_payload.rgb_state_topic + '/set' - cfg.discovery_payload.brightness_state_topic = this.mqtt.getTopic( - topic - ) - cfg.discovery_payload.brightness_command_topic = - cfg.discovery_payload.brightness_state_topic + '/set' - } else { - cfg = copy(hassCfg.light_dimmer) - } + // const rgb = node.values['51-1-0'] + // if (rgb) { + // cfg = copy(hassCfg.light_rgb_dimmer) + // cfg.discovery_payload.rgb_state_topic = this.mqtt.getTopic( + // this.valueTopic(node, rgb) + // ) + // cfg.discovery_payload.rgb_command_topic = this.mqtt.getTopic(result.targetTopic) + '/set' + // cfg.discovery_payload.brightness_state_topic = this.mqtt.getTopic( + // topic + // ) + // cfg.discovery_payload.brightness_command_topic = + // cfg.discovery_payload.brightness_state_topic + '/set' + // } else { + cfg = copy(hassCfg.light_dimmer) + // } } } else return break - case 'door_lock': - if (valueId.index === 0) { + case CommandClasses['Door Lock']: + if (valueId.isCurrentValue) { // lock state cfg = copy(hassCfg.lock) } else { return } break - case 'sound_switch': - // https://github.com/OpenZWave/open-zwave/blob/master/config/Localization.xml#L1575 - if (valueId.index === 2) { + case CommandClasses['Sound Switch']: + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/SoundSwitchCC.ts + if (valueId.property === 'volume') { // volume control cfg = copy(hassCfg.volume_dimmer) - cfg.discovery_payload.brightness_state_topic = this.mqtt.getTopic( - topic - ) - cfg.discovery_payload.command_topic = - cfg.discovery_payload.brightness_state_topic + '/set' + cfg.discovery_payload.brightness_state_topic = getTopic + cfg.discovery_payload.command_topic = getTopic + '/set' cfg.discovery_payload.brightness_command_topic = cfg.discovery_payload.command_topic } else { return } break - case 'central_scene': - case 'scene_activation': + case CommandClasses['Central Scene']: + case CommandClasses['Scene Activation']: cfg = copy(hassCfg.central_scene) break - case 'sensor_binary': - // try to guess the type - if (/\bmotion\b/gi.test(valueId.label)) { - cfg = copy(hassCfg.binary_sensor_occupancy) - } else if (/\btamper\b/gi.test(valueId.label)) { - cfg = copy(hassCfg.binary_sensor_tamper) - } else if (/\balarm\b/gi.test(valueId.label)) { - cfg = copy(hassCfg.binary_sensor_alarm) - } else { - cfg = copy(hassCfg.binary_sensor_contact) + case CommandClasses['Binary Sensor']: + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/BinarySensorCC.ts#L41 + + // change the sensorTypeName to use directly valueId.property, as the old way was returning a number + // add a comment which shows the old way of achieving this value. This change fixes the Binary Sensor + // discovery + let sensorTypeName = valueId.property + + if (sensorTypeName) { + sensorTypeName = this.mqtt.cleanName( + sensorTypeName.toLocaleLowerCase() + ) } + // TODO: Implement all BinarySensorTypes + cfg = hassCfg['binary_sensor_' + sensorTypeName] + // if cannot discover anything, assume contact type + cfg = cfg ? copy(cfg) : copy(hassCfg.binary_sensor_contact) if (valueConf) { if (valueConf.device_class) { @@ -1176,48 +1289,61 @@ Gateway.prototype.discoverValue = function (node, valueId) { } break - case 'sensor_alarm': - const alarmMap = { - 0: 'general', - 1: 'smoke', - 2: 'carbon_monoxide', - 3: 'carbon_dioxide', - 4: 'heat', - 5: 'flood' + case CommandClasses['Alarm Sensor']: + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/AlarmSensorCC.ts#L40 + if (valueId.property === 'state') { + cfg = copy(hassCfg.binary_sensor_alarm) + cfg.object_id += AlarmSensorType[valueId.propertyKey] + ? '_' + AlarmSensorType[valueId.propertyKey] + : '' + } else { + return } - cfg = copy(hassCfg.binary_sensor_alarm) - cfg.object_id += alarmMap[valueId.index] - ? '_' + alarmMap[valueId.index] - : '' break - case 'alarm': + case CommandClasses.Notification: + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/NotificationCC.ts + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json cfg = copy(hassCfg.sensor_generic) - cfg.object_id = 'alarm_' + Constants.alarmType(valueId.index) + cfg.object_id = 'notification_' + valueId.property cfg.discovery_payload.icon = 'mdi:alarm-light' break - case 'sensor_multilevel': - case 'meter': - case 'meter_pulse': - case 'time': - case 'energy_production': - case 'battery': + case CommandClasses['Multilevel Sensor']: + case CommandClasses.Meter: + case CommandClasses['Pulse Meter']: + case CommandClasses.Time: + case CommandClasses['Energy Production']: + case CommandClasses.Battery: let sensor = null - // https://github.com/OpenZWave/open-zwave/blob/master/config/Localization.xml#L885 - if (type === 'sensor_multilevel') { - sensor = Constants.sensorType(valueId.index) - } else if (type === 'meter') { - // https://github.com/OpenZWave/open-zwave/blob/master/config/Localization.xml#L680 - sensor = Constants.meterType(valueId.index) - } else if (type === 'meter_pulse') { + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/MultilevelSensorCC.ts + if (cmdClass === CommandClasses['Multilevel Sensor']) { + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/sensorTypes.json + // In some cases Multilevel Sensors offer Reset option or DeltaTime sensors, but do not include ccSpecific + // information. With this change, we target only the sensors and not the additional Properties. + if (valueId.ccSpecific) { + sensor = Constants.sensorType(valueId.ccSpecific.sensorType) + } else { + return + } + } else if (cmdClass === CommandClasses.Meter) { + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/MeterCC.ts + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/meters.json + // In some cases Metering devices offer Reset option or DeltaTime sensors, but do not include ccSpecific + // information. With this change, we target only the sensors and not the additional Properties. + if (valueId.ccSpecific) { + sensor = Constants.meterType(valueId.ccSpecific.meterType) + } else { + return + } + } else if (cmdClass === CommandClasses['Pulse Meter']) { sensor = { sensor: 'pulse', objectId: 'meter', props: {} } - } else if (type === 'time') { - if (valueId.index === 0) { + } else if (cmdClass === CommandClasses.Time) { + if (valueId.isCurrentValue) { sensor = { sensor: 'date', objectId: 'current', @@ -1226,15 +1352,19 @@ Gateway.prototype.discoverValue = function (node, valueId) { } } } else return - } else if (type === 'energy_production') { - sensor = Constants.productionType(valueId.index) - } else if (type === 'battery') { - if (valueId.index === 0) { + } else if (cmdClass === CommandClasses['Energy Production']) { + // TODO: class not yet supported by zwavejs + // sensor = Constants.productionType(valueId.property) + return + } else if (cmdClass === CommandClasses.Battery) { + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/BatteryCC.ts#L258 + if (valueId.property === 'level') { sensor = { sensor: 'battery', objectId: 'level', props: { - device_class: 'battery' + device_class: 'battery', + unit_of_measurement: '%' // this is set if Driver doesn't offer unit of measurement } } } else return @@ -1246,13 +1376,9 @@ Gateway.prototype.discoverValue = function (node, valueId) { Object.assign(cfg.discovery_payload, sensor.props || {}) - if (valueId.units) { - const hassUnitOfMeasurementMap = { - C: '°C', - F: '°F' - } - cfg.discovery_payload.unit_of_measurement = - hassUnitOfMeasurementMap[valueId.units] || valueId.units + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/scales.json + if (valueId.unit) { + cfg.discovery_payload.unit_of_measurement = valueId.unit } // check if there is a custom value configuration for this valueID @@ -1265,7 +1391,7 @@ Gateway.prototype.discoverValue = function (node, valueId) { } break // case 'color': - // if (valueId.index === 0) { + // if (valueId.isCurrentValue) { // cfg = copy(hassCfg.light_rgb) // cfg.discovery_payload.rgb_state_topic = this.mqtt.getTopic(topic) // cfg.discovery_payload.rgb_command_topic = cfg.discovery_payload.rgb_state_topic + '/set' @@ -1281,13 +1407,13 @@ Gateway.prototype.discoverValue = function (node, valueId) { !payload.hasOwnProperty('state_topic') || payload.state_topic === true ) { - payload.state_topic = this.mqtt.getTopic(topic) + payload.state_topic = getTopic } else if (payload.state_topic === false) { delete payload.state_topic } if (payload.command_topic === true) { - payload.command_topic = this.mqtt.getTopic(topic, true) + payload.command_topic = setTopic || getTopic + '/set' } // Set availability topic using node status topic @@ -1309,7 +1435,7 @@ Gateway.prototype.discoverValue = function (node, valueId) { // Check if another value already exists and add the index to object_id to make it unique if (node.hassDevices[cfg.type + '_' + cfg.object_id]) { - cfg.object_id += '_' + valueId.index + cfg.object_id += '_' + valueId.property } // Set a friendly name for this component @@ -1321,11 +1447,18 @@ Gateway.prototype.discoverValue = function (node, valueId) { const discoveryTopic = getDiscoveryTopic(cfg, nodeName) cfg.discoveryTopic = discoveryTopic - cfg.values = [valueId.id] + cfg.values = [vId] + + if (valueId.targetValue) { + cfg.values.push(valueId.targetValue) + } // This configuration is not stored in nodes.json cfg.persistent = false + // skip discovery flag, default to false + cfg.ignoreDiscovery = false + node.hassDevices[cfg.type + '_' + cfg.object_id] = cfg this.publishDiscovery(cfg, node.id) diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index d34ae039090..80673818ea9 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -118,7 +118,7 @@ function driverReady () { // eslint-disable-next-line no-unused-vars for (const [nodeId, node] of this.driver.controller.nodes) { - // Reset the node status + // setup node events bindNodeEvents.call(this, node) // Make sure we didn't miss the ready event @@ -650,10 +650,14 @@ function updateValueMetadata (zwaveNode, zwaveValue, zwaveValueMeta) { readable: zwaveValueMeta.readable, writeable: zwaveValueMeta.writeable, description: zwaveValueMeta.description, - label: zwaveValueMeta.label, + label: zwaveValueMeta.label || zwaveValue.propertyName + ' (property)', // when label is missing, re use propertyName. Usefull for webinterface default: zwaveValueMeta.default } + if (zwaveValueMeta.ccSpecific) { + valueId.ccSpecific = zwaveValueMeta.ccSpecific + } + let genre = '' if (applicationCCs[zwaveValue.commandClass]) { @@ -715,6 +719,17 @@ function addValue (zwaveNode, zwaveValue) { ) valueId.value = zwaveNode.getValue(zwaveValue) + if (isCurrentValue(valueId)) { + valueId.isCurrentValue = true + const targetValue = findTargetValue( + valueId, + zwaveNode.getDefinedValueIDs() + ) + if (targetValue) { + valueId.targetValue = getValueID(targetValue, false) + } + } + debug(`Node ${zwaveNode.id}: value added ${valueId.id} => ${valueId.value}`) node.values[getValueID(valueId)] = valueId @@ -771,6 +786,20 @@ function getDeviceID (node) { )}-${parseInt(node.productType)}` } +function isCurrentValue (valueId) { + return valueId.propertyName && /current/i.test(valueId.propertyName) +} + +function findTargetValue (zwaveValue, definedValueIds) { + return definedValueIds.find( + v => + v.commandClass === zwaveValue.commandClass && + v.endpoint === zwaveValue.endpoint && + v.propertyKey === zwaveValue.propertyKey && + /target/i.test(v.propertyName) + ) +} + /** * Get a valueId from a valueId object * diff --git a/lib/utils.js b/lib/utils.js index 716299bbd6f..5a2cafb3cb8 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -14,7 +14,7 @@ module.exports = { return path.join(...paths) }, num2hex (num) { - const hex = num.toString(16) + const hex = num >= 0 ? num.toString(16) : 'XXXX' return '0x' + '0'.repeat(4 - hex.length) + hex } } diff --git a/src/components/ControlPanel.vue b/src/components/ControlPanel.vue index 7d2f9a2b567..dd108a40933 100644 --- a/src/components/ControlPanel.vue +++ b/src/components/ControlPanel.vue @@ -938,7 +938,7 @@ export default { } this.errorDevice = !valid - return valid || 'JSON test failed' + return this.deviceJSON === '' || valid || 'JSON test failed' }, async importConfiguration () { if ( @@ -1116,6 +1116,7 @@ export default { if ( device && (await this.$listeners.showConfirm( + 'Rediscover Device', 'Are you sure you want to re-discover selected device?' )) ) {