110 Commits

Author SHA1 Message Date
Alex M
de14f71616 3.1.5 2024-02-19 21:03:42 +03:00
Alex M
adb59a0d9d Update README.md
Friendly topics
2024-02-19 19:48:39 +03:00
Alex M
f865e259a1 Added friendly topics for the devices behind gateway
You can now use:
tuya/gw1/lv/dps/2/command
instead of tuya/zgw1/2342cd828dfsxckkk/dps/2/command
2024-02-19 19:42:02 +03:00
Alex M
4ea2f0a58f Added friendly topics for the devices behind gateway
You can now use:
tuya/gw1/lv/dps/2/command
instead of tuya/zgw1/2342cd828dfsxckkk/dps/2/command
2024-02-19 19:40:13 +03:00
lehanspb
f3d0d0602e decription for systemd startup script 2023-02-15 16:47:43 +03:00
lehanspb
bb97e731a9 v3.1.4 2023-02-15 13:46:48 +03:00
lehanspb
9a9a314d14 decription for systemd startup script 2023-02-15 13:39:38 +03:00
lehanspb
9080a41d9a DPS Topics for devices behind Tuya Gateway 2023-02-15 13:30:18 +03:00
lehanspb
f6bd28c41a Description for MQTT DPS topics for subdevices (cid) behind Tuya Gateway 2023-02-15 12:37:46 +03:00
lehanspb
1d1ab0d62a Description for MQTT DPS topics for subdevices (cid) behind Tuya Gateway 2023-02-15 12:36:25 +03:00
lehanspb
a170287e9e OpenHAB 3.x exmples 2023-02-15 12:21:22 +03:00
lehanspb
47734e03d0 Description for MQTT DPS topics for subdevices (cid) behind Tuya Gateway 2023-02-15 12:16:10 +03:00
lehanspb
eb800133a0 Some comments 2023-02-15 10:37:22 +03:00
lehanspb
6aded16979 Added cid support and dp-refresh function 2023-02-15 10:34:17 +03:00
lehanspb
7cc3d1d5a6 Change packege.json and requirements 2023-02-15 10:22:14 +03:00
Alex M
59d1131db5 Update README.md 2023-02-13 17:04:58 +03:00
tsightler
36f3854ca9 Update README.md 2021-06-01 13:12:35 -04:00
tsightler
b7fc73de3c Update bug_report.md 2021-02-09 12:13:08 -05:00
tsightler
2b92e2b27c Update feature_request.md 2021-02-09 12:12:36 -05:00
tsightler
35dde4bd11 Update feature_request.md 2021-02-09 12:11:57 -05:00
tsightler
d833151beb Update feature_request.md 2021-02-09 12:10:06 -05:00
tsightler
c179282393 Update README.md 2021-02-09 12:07:19 -05:00
tsightler
8b532d9df1 v3.0.4
* Fix reconnect failure for some error cases
* Bump tuyapi version to latest
2021-01-27 13:46:55 -05:00
tsightler
02d5124b62 v3.03
* Fix reconnect failure in some disconnect cases
* Remove duplicate republish function
2021-01-12 20:19:42 -05:00
tsightler
a5b344faeb Update to 3.0.2 (#63)
* Update 3.0.2
* Fix (hopefully) uninitialized key values for devices which block get requests for some DPS values based on operating mode (seen with at least one RGBTW light that will not return color DPS key while in white mode).
* Modify HSB/HSBHEX guessing in RGBTW light to attempt to deal with issue above
* Implement automatic reconnect when device disconnects socket on it's side
* Update package dependencies to latest versions
2021-01-05 22:18:19 -05:00
tsightler
aa80270ba2 Update 3.0.1 (#61)
* Script to merge device additions and changes into devices.conf (#49)
* Add republish on reconnect
* Filter additional invalid characters '+','#','/' from topic line (replace with '_')
* Minor bugfixes for parse values
* Include script for merging new devices into existing config
* Update dependency

Co-authored-by: tsightler <tsightler@gmail.com>
Co-authored-by: Doug Krahmer <doug.git@remhark.com>
2020-12-23 20:22:09 -05:00
tsightler
b29b98911a Revert "Dev (#60)"
This reverts commit 44a5c6adbf.
2020-12-23 20:18:39 -05:00
tsightler
44a5c6adbf Dev (#60)
3.0.1 Update
* Script to merge device additions and changes into devices.conf (#49)
* Fix typos in README.md
* Add republish on reconnect
* Filter additional invalid characters '+','#','/' from topic line (replace with '_')
* Minor bugfixes for parse values
* Include script for merging new devices into existing config
* Update dependency

Co-authored-by: tsightler <tsightler@gmail.com>
Co-authored-by: Doug Krahmer <doug.git@remhark.com>
2020-12-23 20:10:29 -05:00
tsightler
bda39b03b4 Update DEVICES.md 2020-10-27 18:15:27 -04:00
tsightler
48dd1c64b0 Update README.md 2020-10-19 21:40:16 -04:00
tsightler
55014cda3b Update CHANGELOG.md 2020-10-18 21:44:17 -04:00
tsightler
8f8d4fb185 Merge pull request #44 from TheAgentK/dev
Release 3.0.0
2020-10-18 21:29:07 -04:00
tsightler
91b41be953 Merge branch 'master' into dev 2020-10-18 21:27:34 -04:00
tsightler
d61c1f7cee Release 3.0.0
3.0.0 Release

Major changes from 2.1.0:
* Completely new configuration engine
* Completely new topic structure
* New template engine for creating friendly topic structure from raw DPS values
* Pre-defined templates for some common devices
* Directly control devices via Tuya JSON topic or via DPS key topics
2020-10-18 20:53:58 -04:00
tsightler
51bbd612f2 3.0.0 Documentation update 2020-10-17 17:21:38 -04:00
tsightler
5219c94cd7 3.0.0 Documentation update 2020-10-17 16:38:57 -04:00
tsightler
66541558dd 3.0.0 Documentation updates 2020-10-17 14:13:35 -04:00
tsightler
535b6edecf 3.0.0 Documentation updates 2020-10-17 13:53:22 -04:00
tsightler
45a26f84de 3.0.0 Documentation updates 2020-10-17 13:32:50 -04:00
tsightler
1147b9e8c1 3.0.0 Documentation updates 2020-10-16 22:03:45 -04:00
tsightler
1033e129de 3.0.0 Documentation update 2020-10-16 21:52:18 -04:00
tsightler
4a0a167863 3.0.0 Documentation updates 2020-10-16 21:43:59 -04:00
tsightler
b4942dffa1 3.0.0 documentation updates 2020-10-16 21:42:10 -04:00
tsightler
1abcdb02dc 3.0.0 Documentation Update 2020-10-16 21:32:27 -04:00
tsightler
ab2d4da5d6 Simplify color logic
* Color/white switch logic simplified and more reliable
* Clean up variable names
2020-10-16 01:23:54 -04:00
tsightler
748a5cae39 Minor enhancements
* Properly disconnect from devices on exit
* Monitor for Home Assistant status and resend discovery
2020-10-15 11:05:41 -04:00
tsightler
271697439c Always set saturation to 0 in white mode 2020-10-14 11:40:43 -04:00
tsightler
8f129617b0 Color Tweaks/Cleanups
Minor tweaks and misc cleanups preparing for 3.0.0 release
2020-10-13 00:39:56 -04:00
tsightler
60b50c760e 3.0.0-beta4
* Default to generic device
* Improved debugging granularity/increased categories
* Add heartbeat monitoring for availability
* Catch more failure cases with retry (still some missing I'd guess)
* Switch to MathJS evaluate for simple math transforms
* RGBTW: Switch base scale for all friendly topics to 100 (automatic conversion on backend)
* RGBTW: Add color temperature support
* RGBTW: Improve autodetection
* RGBTW: Improved white/color mode handling (still work to do here)
2020-10-12 16:14:22 -04:00
tsightler
d5217ce237 3.0.0-beta3
* Improve RGBTW white/color logic
* Rebase MQTT topic brightess to 100 scale (vs 255/1000)
* Added based auto-discovery for RGBTW light
* Added math functions
2020-10-07 08:26:13 -04:00
tsightler
38d3092af3 Update rgbtw-light.js 2020-10-05 09:56:04 -04:00
tsightler
d75e2189da Update tuya-device.js 2020-10-05 08:35:56 -04:00
tsightler
4ff6fc221e More RGBTW tweaks 2020-10-05 02:05:34 -04:00
tsightler
3705efaaed More RGB fixes 2020-10-04 23:34:37 -04:00
tsightler
6c72afd8ab Color support for HSBHEX 2020-10-04 01:04:54 -04:00
tsightler
3605037b69 Granular state updates
* State topics now only update if data for the specific DPS changed
* Rework RGBTW support for more reliable white/color mode switching
2020-10-03 22:41:57 -04:00
tsightler
df01beb8fc RGBTW light fixes 2020-10-03 13:32:48 -04:00
tsightler
9176b8f85c Reorganize tuya-device 2020-10-03 11:45:41 -04:00
tsightler
68d8743882 Fix for no template 2020-10-03 10:38:43 -04:00
tsightler
99f5358794 Remove console output 2020-10-02 23:24:42 -04:00
tsightler
f598d246cf 3.0.0-beta2
* Implement basic template model
* Generic device can specify template in devices.conf
* Simple switch, dimmer, and RGBTW specific support files
2020-10-02 23:16:53 -04:00
tsightler
1b33c60226 Update README.md 2020-09-30 20:21:22 -04:00
tsightler
56b87aa27e Update tuya-mqtt.js 2020-09-25 00:34:43 -04:00
tsightler
311144a9ed Update tuya-mqtt.js 2020-09-25 00:27:10 -04:00
tsightler
77b697f5bb Update tuya-mqtt.js 2020-09-24 15:30:47 -04:00
tsightler
1181f66c32 Device/DPS commands
* Add ability to set individual DPS values via <topic>/dps/<#>/command
* Can still use Tuya JSON via <topic>/dps/command
* Simple on/off sent to <topic>/command
* Brightness sent to <topic>/brightness_command
* Simple device type detection (currently only for sockets/switches/dimmers and non-RGB lights, other devices are unknown get DPS 1 on/off in state and all other values accessible via DPS
2020-09-22 01:01:43 -04:00
tsightler
d8a24d4e6f Use device.conf 2020-09-21 17:53:46 -04:00
tsightler
670a87e9fb Use devices.json for discovery
* Usese devices.json from "tuya-cli wizard" for device discovery, no topic config required
* Uses device discovery by default, but can manually edit devices.json to add IP address, protocol version, or change
* Creates short topic name using just "friendly name" or "id".
2020-09-21 00:23:31 -04:00
tsightler
d3b8b9244f Update feature_request.md 2020-09-18 12:00:05 -04:00
tsightler
3b60f3d4d5 Update bug_report.md 2020-09-18 11:56:43 -04:00
tsightler
390d81c893 Merge pull request #40 from TheAgentK/dev
Merge 2.1.0 with Master
2020-09-17 22:02:54 -04:00
tsightler
b0549bf392 Merge branch 'master' into dev 2020-09-17 22:02:06 -04:00
tsightler
989834821a Update README.md 2020-09-17 21:58:17 -04:00
tsightler
9944f326c5 Update README.md 2020-09-17 21:57:38 -04:00
tsightler
df08c6fc8a Update README.md 2020-09-17 21:52:15 -04:00
tsightler
d05ac857d8 Update README.md 2020-09-17 21:50:10 -04:00
tsightler
868b0977b0 Update README.md 2020-09-17 21:47:21 -04:00
tsightler
82faeff78e Update README.md 2020-09-17 21:40:28 -04:00
tsightler
89a7bfd04d Release 2.1.0
* Merge protocol 3.3 support
* Update TuyAPI to 5.3.1
* Fixes for color settings issues with some devices
* Support to force device status update with schema query
2020-09-17 21:38:52 -04:00
tsightler
1bc87b513c Merge pull request #39 from tsightler/master
Merge protocol 3.3 support
2020-09-17 20:41:35 -04:00
tsightler
01fd5abcff Merge branch 'dev' into master 2020-09-17 20:41:12 -04:00
tsightler
9091398810 Merge pull request #35 from vkoop/feature/fix-documentation
fix missing string escaping in documentation
2020-09-17 20:27:22 -04:00
tsightler
65a9922a7f Merge pull request #21 from GadgetAngel/dev
This update allows for an essential TuyAPI command to be implemented and fixes issues with the TuyaColorLight.prototype.setColor Method label:enhancement
2020-09-17 20:27:08 -04:00
Viktor Koop
14fc702041 fix missing string escaping in documentation 2019-12-23 17:54:37 +01:00
tsightler
4a605f4536 Bump dependency to tuyapi 5.1.2
tuyapi 5.1.2 includes fix for memory leak so also removed hack for disconnect/reconnecting devices.
2019-07-31 23:55:15 -04:00
tsightler
ec8d2c62a5 Fix silly bug in discover/3.3 support
Fix silly bug in discover/3.3 support
2019-07-29 11:53:46 -04:00
tsightler
a554cd3f58 Work around memory leak in tuyapi >5.1.x
Work around memory leak in tuyapi >5.1.x
2019-07-29 08:03:06 -04:00
tsightler
fb79927020 Merge branch 'master' of https://github.com/tsightler/tuya-mqtt 2019-06-22 23:31:45 -04:00
tsightler
279590eb71 Support for manual protocol 3.3
Add support to explisitly set protocol version.
2019-06-22 23:31:38 -04:00
tsightler
6f0e03d3f2 Update README.md 2019-06-18 23:04:18 -04:00
tsightler
c6e88ac08b Update README.md 2019-06-18 22:53:56 -04:00
tsightler
a747e122c6 Update README.md 2019-06-18 22:52:34 -04:00
tsightler
604256709a Update README.md 2019-06-18 22:48:59 -04:00
tsightler
3b72a000c8 tuyapi 5.1.x and protocol 3.3
Initial update to work with tuyapi 5.1.x and support for Tuya protocol ver 3.3 via device discovery.
2019-06-18 22:32:18 -04:00
GadgetAngel
fa228074b3 Merge remote-tracking branch 'origin/dev' into dev 2019-04-26 04:47:14 -04:00
GadgetAngel
54745c4f8b 1. This update allows for an essential TuyAPI command to be implemented via the tuya-mqtt.exe MQTT server. {"schema": true} is the ONLY COMMAND that the TuyAPI GET method implements. Also fixes a problem with the setColor method of TuyaColorLight.
2. This update does not set dps 3 and dps 4 when setting the bulb in colour mode because some tuya bulbs ignore the dps 5 setting if you set either dps 3 or dps 4
3. This update uses the correct format for dps 5: if the bulb is in colour mode than the dps 3 and dps 4 are ignored but if you set it now some tuya bulbs will ignore dps 5 because you set dps 3 or dps 4
   A. So, FOR colour mode the bulb looks at dps 1, dps 2, and dps 5.
	i.   DPS 5 is in the following format:
	ii.  HSL to HEX format are the leftmost hex digits (hex digits 14 - 9)
	iii. hex digits 8 - 5 are the HSB/HSL Hue value in HEX format
    iv.  hex digits 4 - 3 are the HSB/HSL Saturation percentage as a value (converted to 0-255 scale) in HEX format
    v.   hex digits 2 - 1 are the HSB Brightness percentage as a value (converted to 25-255 scale) in HEX format

   B. if the bulb is in white mode then the dps 5 value is ignored by the bulb, FOR white mode the bulb looks at dps 1, dps 2, dps 3 and dps 4
	i.	 DPS 3 is the HSB/HSL Brightness percentage converted to a value from 25 to 255 in decimal format
    ii.  DPS 4 is the HSB/HSL Saturation percentage converted to a value from 0 to 255 in decimal format

{"schema": true} allows the user to establish that proper communications with the tuya device can occur WITHOUT actually changing the present STATE of the device.  This is the only command that will query the tuya device.

The current documentation says that you can query the tuya device over the "dps" TOPIC but from what I see the present state of the software does not force the tuya device to respond when using the "dps" TOPIC.

Since {"schema": true} is a command that forces a response from the tuya device, this command has been implemented under the "command" TOPIC. So "command" is the action and '{"schema": true}' becomes the command.  The response is returned just like all other commands.  I, use this command to guarantee communications has been established with the tuya device.  If this "schema" command fails, tuya-mqtt will indicate the result in the openhab.log file and then I, can find out what is physically wrong with the communications.  If this command fails the first time due to "socket" error and then goes through on the second attempt then I know that the error was due to TCP communications problem on initial startup.  This command helps as a work-a-round for the "ERROR: socket problem"
2019-04-26 04:46:15 -04:00
GadgetAngel
b5f1f1fd31 correct file
sorry for the screw up.....must be getting tired
2019-04-20 22:57:29 -04:00
GadgetAngel
4ea15db772 This update allows for an essential TuyAPI command to be implemented via the tuya-mqtt.exe MQTT server. {"schema": true} is the ONLY COMMAND that the TuyAPI GET method implements. Also fixes a problem with the setColor method of TuyaColorLight. When passing colorValue the curly braces are part of the colorValue string, but when you hit the .split to break up the string into Hue, Saturation and Brightness the curly braces are still considered part of the string so the Hue value was always returning a NaN value due to { was part of the Hue. Therefore I created a private function that strips off the beginning and ending curly braces so the right numeric values can be found.
{"schema": true} allows the user to establish that proper communications with the tuya device can occur WITHOUT actually changing the present STATE of the device.  This is the only command that will query the tuya device.

The current documentation says that you can query the tuya device over the "dps" TOPIC but from what I see the present state of the software does not force the tuya device to respond when using the "dps" TOPIC.

Since {"schema": true} is a command that forces a response from the tuya device, this command has been implemented under the "command" TOPIC. So "command" is the action and '{"schema": true}' becomes the command.  The response is returned just like all other commands.  I, use this command to guarantee communications has been established with the tuya device.  If this "schema" command fails, tuya-mqtt will indicate the result in the openhab.log file and then I, can find out what is physically wrong with the communications.  If this command fails the first time due to "socket" error and then goes through on the second attempt then I know that the error was due to TCP communications problem on initial startup.  This command helps as a work-a-round for the "ERROR: socket problem"
2019-04-20 17:50:38 -04:00
GadgetAngel
61433e74a9 This update allows for an essential TuyAPI command to be implemented via the tuya-mqtt.exe MQTT server. {"schema": true} is the ONLY COMMAND that the TuyAPI GET method implements.
{"schema": true} allows the user to establish that proper communications with the tuya device can occur WITHOUT actually changing the present STATE of the device.  This is the only command that will query the tuya device.

The current documentation says that you can query the tuya device over the "dps" TOPIC but from what I see the present state of the software does not force the tuya device to respond when using the "dps" TOPIC.

Since {"schema": true} is a command that forces a response from the tuya device, this command has been implemented under the "command" TOPIC. So "command" is the action and '{"schema": true}' becomes the command.  The response is returned just like all other commands.  I, use this command to guarantee communications has been established with the tuya device.  If this "schema" command fails, tuya-mqtt will indicate the result in the openhab.log file and then I, can find out what is physically wrong with the communications.  If this command fails the first time due to "socket" error and then goes through on the second attempt then I know that the error was due to TCP communications problem on initial startup.  This command helps as a work-a-round for the "ERROR: socket problem"
2019-04-20 09:29:01 -04:00
TheAgentK
825b9f97ce Merge pull request #19 from TheAgentK/dev
Merge DEV version v.2.0.1

Closes #15
2019-04-09 20:08:22 +02:00
KarstenSiedentopp
cfefbabe90 ADD: support to toggle device state 2019-04-09 19:23:15 +02:00
KarstenSiedentopp
59acf6d2a4 UPD: CHANGELOG 2019-04-09 18:44:41 +02:00
KarstenSiedentopp
1485ae0689 added debug informations 2019-04-09 18:20:16 +02:00
KarstenSiedentopp
7a9dde716c UPD: README and CHANGELOG 2019-04-09 18:20:03 +02:00
KarstenSiedentopp
e9e46a7b77 added project changelog 2019-04-08 18:24:42 +02:00
KarstenSiedentopp
998028fa1c update project informations 2019-04-08 18:24:27 +02:00
KarstenSiedentopp
18ebcbdd74 MOD: remove device type from topic if not set 2019-04-08 18:14:45 +02:00
KarstenSiedentopp
86e602d6cf ADD: capability to set multiple dps values 2019-04-08 17:09:59 +02:00
KarstenSiedentopp
7637a372f7 updated tuyAPI to version 4.0.4 2019-04-08 17:07:30 +02:00
KarstenSiedentopp
dd2385f6e2 updated TuyAPI to v 4.x 2019-02-20 22:25:28 +01:00
21 changed files with 4206 additions and 1177 deletions

View File

@@ -4,9 +4,11 @@ about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**!!! Please Read This First !!!**
If you are having difficulty controlling a specific device, please verify that the device works with tuya-cli prior to opening an issue here. If you are unable to control the device via tuya-cli it will not be possible to control it with tuya-mqtt. If the device is working with tuya-cli, please post your working commands and the equivalent commands for tuya-mqtt.
**Describe the bug**
A clear and concise description of what the bug is.

View File

@@ -7,14 +7,5 @@ assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
**!!! Please Read This First !!!**
Feature requiest for this project are not being accepted at this time as this project is in maintainance mode only.

4
.gitignore vendored
View File

@@ -1,6 +1,8 @@
devices/
old/
test/
config.json
.vscode/
.bak
# Logs
logs

86
CHANGELOG.md Normal file
View File

@@ -0,0 +1,86 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [3.1.5]
### Added
- Added support of friendly names for the subDevices behind gateway (processDpsKeyWcidNameCommand)
### Changed
- Functions publishTopics and publishDpsTopics now can publish subDevice's friendly name to MQTT
## [3.1.0]
Forked from https://github.com/TheAgentK/tuya-mqtt
### Added
- Added support for Zigdee subdevices for Tuya Wireless Gateway
- Added new dp-refresh funtion according to tuyapi documentation
- Command and state topics for DPS keys for subdevices
### Changed
- Updated some libraries to latest version
## [3.0.0]
The 3.0.0 release is a major refactor of the project with significant changes from previous version. Only major additions and changes are listed below.
### Added
- Added templating engine for mapping device DPS value to friendly topics
- Added pre-defined template for common devices
- Command topics for DPS keys
### Changed
- Configuration for devices is now via devices.conf file (based on output format of 'tuya-cli wizard' command)
- Default device presents only raw DPS data
- Commands sent only via MQTT messages to command topics
- Updates all libraries to latest version
### Removed
- Topic based configuraiton has been removed
## [2.1.0]
### Added
- Added ability to update validate communicaton with device and update state topic by issuing { "schema": true } command
- Added support for protocol 3.3 either via automatic device discovery or manual specification when using IP address
### Changed
- Can specify "discover" instead of IP address to automatically find device (only works if device on same IP subnet as system running this script). This mode will also automatically detect 3.1 and 3.3 protocol devices
- Can manually specific protocol via ver3.1/ver3.3 in topic line after tuya/
- Bump Tuyapi version to v5.3.x
- Bump MQTT version to v4.x.x
- Moved openHAB config to it's own document since many users use this with other tools
- Verious other fixes and cleanups
## [2.0.1]
### Added
- Added capability to set multiple dps values over MQTT-Command
- Custom Set-Function for TuyAPI-Class (added error handling for "index [1] not found" error)
### Changed
- MQTT-Topic no longer requires a device type
- Updated TuyAPI to v4.x.x
### Removed
- remove device type from topic
## [2.0.0]
### Added
- support for OH MQTT-Binding 2.4
- default QoS of 2, if not set through config.json
### Changed
- Updated TuyAPI to v3.x.x
- Constant off states after start #11
### Removed
- custom set function for TuyAPI
## [1.0.0]
### Added
- Add ability to connect to protected MQTT server
- Added seperate configuration file
### Changed
- TuyAPI repository not found
### Removed

400
README.md
View File

@@ -1,169 +1,231 @@
# TuyaAPI-MQTT Client
MQTT interface for Tuya home automation devices sold under various names.
This is a wrapper script for the Project codetheweb/tuyapi. https://github.com/codetheweb/tuyapi
This project provides an MQTT client for communication with the home automation devices.
:exclamation: There is a greate Step-By-Step guide from user HolgiHab at openhab community ([Step-By-Step Guide](
https://community.openhab.org/t/step-by-step-guide-for-adding-tuya-bulbs-smart-life-to-oh2-using-tuya-mqtt-js-by-agentk/59371)). This guide is not only for light bulbs, but also applies to sockets. :exclamation:
## Instructions:
Download this project to your openhab2-script-folder "/etc/openhab2/scripts" and install tuyapi from the same folder that the tuya-mqtt.js is in
```
cd /etc/openhab2/scripts
git clone git@github.com:TheAgentK/tuyaapi_mqtt.git // this project
cd tuyaapi_mqtt
npm install //downloads codetheweb/tuyapi
```
This involves MIM of the connection. Instructions can be found here: https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md
Create your configuration file:
```
cp config.json.sample config.json
nano config.json // edit the configuration file
```
Start command
```
node tuya-mqtt.js
// For debugging purpose
DEBUG=* tuya-mqtt.js
```
MQTT Topic
```
Current device state:
tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/state
Change device state (by topic):
tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/command/<STATE>
Example:
tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/command/on
tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/command/off
Change device state (by payload)
Use with OpenHAB 2.X MQTT bindings or others where only a single command topic is preferred
tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/command // State as Payload (on,off)
Color for lightbulb:
Example:
tuya/lightbulb/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/color // Color as Payload as hexColor
Read data from device:
tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/dps // returns JSON.stringify(dps) values, use with care, does not always contain all dps values
tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/dps/<tuya-dps-id> // return single dps data value
```
#### Issues
There are some reliability issues with tuyapi. Latest changes changed the syntax but still getting error maybe at an even higher rate.
All questions regarding the TuyaAPI please ask in the project https://github.com/codetheweb/tuyapi .
## Example items for OpenHAB 1.x Bindings (still works with >2.4 but only if legacy 1.x MQTT bindings are enabled)
#### simple switch on/off
```
Switch tuya_kitchen_coffeemachine_mqtt "Steckdose Kaffeemaschine" <socket> (<GROUPS>) ["Switchable"] {
mqtt="<[broker:tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/state:state:default:.*],
>[broker:tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/command/on:command:ON:true],
>[broker:tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/command/off:command:OFF:false]"
}
Switch tuya_livingroom_ledstrip_tv "LED Regal" <lightbulb> (<GROUPS>) ["Lighting"] {
mqtt="<[broker:tuya/lightbulb/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/state:state:default:.*],
>[broker:tuya/lightbulb/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/command/on:command:ON:true],
>[broker:tuya/lightbulb/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/command/off:command:OFF:false]"
}
```
#### change color of lightbulb
```
# .items
Group gTuyaLivingColor "Tuya color group" <lightbulb>
Color tuya_livingroom_colorpicker "Stehlampe farbe" (LivingDining, Wohnzimmer)
String tuya_livingroom_ledstrip_tv_color "Set color [%s]" (gTuyaLivingColor, LivingDining, Wohnzimmer) {
mqtt=">[broker:tuya/lightbulb/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/color:command:*:default]"
}
# .rules
import org.openhab.core.library.types.HSBType;
rule "Set HSB value of item RGBLed to RGB color value"
when
Item tuya_livingroom_colorpicker received command
then
var appName = "Colorpicker.livingroom"
var color = receivedCommand.toString;
// get all colors and send it via mqtt if light ist enabled
gTuyaLivingColor.members.forEach[ i |
var name = i.name;
var stateName = name.toString.split("_color").get(0);
var stateItem = gTuyaLights.allMembers.filter [ conf | conf.name.contains(stateName.toString) ].head;
if(stateItem.state == ON){
logInfo(appName, name + " change to color: " + color);
i.sendCommand(color);
Thread::sleep(400);
}
]
end
```
## Example items for OpenHAB 2.4 Bindings
#### simple switch on/off
With OpenHAB 2.X MQTT bindings you can add devices using a generic MQTT Thing via PaperUI or
configuration files. For PaperUI simply at the generic MQTT Thing and set the state and
command topics as follows:
```
tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/state
tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/command
```
If you prefer using configuration files vs PaperUI, it should look something like this:
```
Bridge mqtt:broker:myUnsecureBroker [ host="192.168.0.42", secure=false ]
{
Thing mqtt:topic:mything {
Channels:
Type switch : tuya_kitchen_coffeemachine_mqtt "Steckdose Kaffeemaschine" [ stateTopic="tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/state", commandTopic="tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/command" ]
}
}
```
For a light with color you would need a separate channel with the command topic set to
tuya/<tuyaAPI-type>/<tuyaAPI-id>/<tuyaAPI-key>/<tuyaAPI-ip>/color and link that to your
color item.
#### Basic UI sitemap
```
Switch item=tuya_kitchen_coffeemachine_mqtt mappings=[ON="On", OFF="Off"]
Switch item=tuya_livingroom_ledstrip_tv mappings=[ON="On", OFF="Off"]
# Colorpicker for Lightbulbs
Colorpicker item=tuya_livingroom_colorpicker label="RGB Lampenfarbe" icon="slider" sendFrequency=30000
```
## Contributors
- [TheAgentK](https://github.com/TheAgentK)
- [tsightler](https://github.com/tsightler)
- [Tycale](https://github.com/Tycale)
- [crashdummymch](https://github.com/crashdummymch)
## Related Projects:
- https://github.com/codetheweb/tuyapi
- https://github.com/unparagoned/njsTuya
- https://github.com/clach04/python-tuya
- https://github.com/Marcus-L/m4rcus.TuyaCore
- Specs: https://docs.tuya.com/en/cloudapi/cloud_access.html
[![forthebadge](https://forthebadge.com/images/badges/made-with-javascript.svg)](https://forthebadge.com)
[![forthebadge](https://forthebadge.com/images/badges/built-with-love.svg)](https://forthebadge.com)
# tuya-mqtt
# Note
I'm not a developer and I'm new to nodejs.
I just need to get data from Zigbee devices behind Tuya Wireless Gateway and the ability to control them using MQTT commands.
I forked this repository [TheAgentK](https://github.com/TheAgentK/tuya-mqtt) and made some changes.
# About
This project is a bridge that allows locally controlling IOT devices manufactured by Tuya Inc., and sold under many different brands, via simple MQTT topics. It effectively translate the Tuya protocol to easy to use topics.
Using this script requires obtaining the device ID and local keys for each of your devices after they are configured via the Tuya/Smart Life or other Tuya compatible app (there are many). With this information it is possible to communicate locally with Tuya devices using Tuya protocol version 3.1 and 3.3 without using the Tuya Cloud service, however, getting the keys requires signing up for a Tuya IOT developer account or using one of several other alternative methods (such as dumping the memory of a Tuya based app running on Android).
To acquire keys for your device please see the instructions at the TuyAPI project (on which this script is based) available at the [TuyAPI GitHub site](https://github.com/codetheweb/tuyapi/blob/master/docs/SETUP.md).
**Acquiring device keys is outside the scope of this project!** Issues opened regarding acquiring keys will likely be closed without comment. Please verify that your device can be queried and controlled via tuya-cli before opening any issue. If your device can't be controlled by tuya-cli then it cannot be used with this project.
## Installation
Download this project to your system into any directory (example below uses /opt/tuya-mqtt) and install tuyapi from the same folder that the tuya-mqtt.js is in
```
// switch to opt directory
cd /opt
// clone this project
git clone https://github.com/lehanspb/tuya-mqtt
// change directory to the project directory
cd tuya-mqtt
//installs this project along with codetheweb/tuyapi project
npm install
```
## Configuration
Tuya-mqtt has two different configuration files. The first is config.json, a simple file which contains settings for connection to the MQTT broker. The second is devices.conf, a JSON5 formatted file which defines the Tuya devices that the script should connect to and expose via MQTT. This file uses the same basic format as the "tuya-cli wizard" outputs when used to acquire the device keys, so it can be used as the basis for your tuya-mqtt device configuration.
### Setting up config.json:
```
cp config.json.sample config.json
```
Edit config.json with your MQTT broker settings and save:
```
nano config.json
```
### Setting up devices.conf:
If you use the "tuya-cli wizard" method to acquire your device keys you can leverage the output of this tool as the start of your devices.conf file. Otherwise, you want to create a file using a formate like this:
```
[
{
name: 'Tuya Device 1',
version: '3.3',
ip: '192.168.20.251',
id: '86435357d8b123456789',
key: '8b2a69c9876543210'
},
{
name: 'Tuya Device 2',
version: '3.3',
ip: '192.168.20.252',
id: 'eb532eea7d12345678abc',
key: '899810012345678',
subDevices:
[ { name: 'subdevice1',
id: 'zt431eda8d12345678awc',
cid: '1a24fkfffe6b4e24'
},
{ name: 'subdevice1',
id: 'rb737qea7v15342678aq3',
cid: '1a24fkfffe0t2c29'
}
]
}
]
```
Note that, because the format is JSON5, which is a superset of JSON, you can use standard, strict JSON syntax, or the more forgiving JSON5 format, or even mix and match in the same file.
By default tuya-mqtt will attempt to find the device and automatically detect the Tuya protocol version, however, this only works if the system running tuya-mqtt is on the same network/subnet as the devices being controlled. If this is not the case, or if automatic detection fails for some other reason, it is possible to specify the IP address and protocol manually by adding the "ip:" property to the devices.conf file. Note that if the IP address is specified manually it is required to also manually specify the protocol version using the "version:" parameter as either "3.1" or "3.3". The easiest way to determine the protocol version is to try controlling the device with tuya-cli and try each version to see which one works.
While the above syntax may be enough to create a working tuya-mqtt install with raw DPS values accessible via DPS topics, the full functionality of tuya-mqtt 3.0 is only unlocked by configuring device types to get. Please see the full [DEVICES](docs/DEVICES.md) documentation for details.
### Starting tuya-mqtt
```
node tuya-mqtt.js
```
To enable debugging output (required when opening an issue):
```
DEBUG=tuya-mqtt:* tuya-mqtt.js
or
DEBUG=* tuya-mqtt.js
for full debugging
```
### Systemd script for Debian-like OS
Just create file /etc/systemd/system/tuya-mqtt.service
```
[Unit]
Description=tuya-mqtt
After=network.target
[Service]
ExecStart=/usr/bin/node /opt/tuya-mqtt/tuya-mqtt.js
Restart=always
User=openhab
Group=openhab
Environment=PATH=/usr/bin/
Environment=NODE_ENV=production
WorkingDirectory=/opt/tuya-mqtt/
[Install]
WantedBy=multi-user.target
```
Enable and run:
```
systemctl enable tuya-mqtt.service
systemctl start tuya-mqtt
```
### Updating devices.conf with new and/or changed devices:
After adding or changing devices to your Tuya account the devices.conf file can be automatically updated with all new devices and name/key changes by using the merge-devices.js script. Create a file named new-devices.conf with the new "tuya-cli wizard" output then run ```node merge-devices.js```. A dated backup of the original devices.conf file will be created automatically before changes are made. Devices are only added and updated, never removed. The resulting devices.conf file will be neatly formatted and sorted alphabetically by device name.
To prevent device entries from being updated by the merge script, add property "allowMerge: false" to the device definition in the devices.conf file.
### Usage Overview
Tuya devices work by mapping device functions to various values stored in data points (referred to as DPS values) which are referenced via an index number, referred to as the DPS key. For example, a simple on/off switch may have a single DPS value, stored in DPS kep 1 (DPS1). This value is likely to have a setting of true/false representing the on/off state of the device. The device state can be read via DPS1, and, for values that can be changed (some DPS values are read-only), sending true/false to DPS1 will turn the device on/off. A simple dimmer might have the same DPS1 value, but an additional DPS2 value from 1-255 representing the state of the dimmer. More complex devices use more DPS keys with various values representing the states and control functions of the device.
The tuya-mqtt script provides access to these DPS keys and their values via MQTT, allowing any tool that can use MQTT to monitor and control these devices via a local network connection. In addition to providing access to the raw DPS data, there is also a template engine that allows those DPS values to be mapped to device specific topics, called "friendly topics", allowing for consistent mapping even between devices that use different DPS keys for the same functions. These friendly topics also support various transforms and other functions that make it easier for other devices to communicate with Tuya devices without a detailed understanding of the data formats Tuya devices use.
### MQTT Topic Overview
The top level topics are created using the device name or ID as the primary identifier. If the device name is available, it will be converted to lowercase and any spaces replace with underscores('_') characters so, for example, if the device as the name "Kitchen Table", the top level topic would be:
```
tuya/kitchen_table/
```
If the device name was not available in the devices.conf file, tuya-mqtt falls back to using the device ID for the top level topic:
```
tuya/86435357d8b123456789/
```
All additional state/command topics are then built below this level. You can view the connectivity status of the device using the status topic, which reports online/offline based on whether tuya-mqtt has an active connection to the device or not. The script monitors both the device socket connection for errors and also device heartbeats, to report proper status.
```
tuya/kitchen_table/state --> online/offline
```
You can also trigger the device to send an immediate update of all known device DPS topics by sending the message "get-states" to the command topic (this topic exist for all devices):
```
tuya/kitchen_table/command <-- get-states
```
As noted above, tuya-mqtt supports two distinct topic types for interfacing with and controlling devices. For all devices, the DPS topics are always published and commands are accepted, however, friendly topics are the generally recommended approach but require you to use a pre-defined device template or create a customer template for your device when using the generic device.
If you do create a template for your device, please feel free to share it with the community as adding additional pre-defined devices is desired for future versions of tuya-mqtt. There is a templates section of the project that you can submit a PR for your templates.
If you would like to use the raw DPS topics, please jump to the [DPS topics](#dps-topics) section of this document.
## Friendly Topics
Friendly topics are only available when using a pre-defined device template or, for the generic device, when you have defined a custom template for your device. Friendly topics use the tuya-mqtt templating engine to map raw Tuya DPS key values to easy to consume topics and transform the data where needed.
Another advantage of friendly topics is that not all devices respond to schema requests (i.e. a request to report all DPS topics the device uses). Because of this, it's not always possible for tuya-mqtt to know which DPS topics to acquire state information from during initial startup. With a defined template the required DPS keys for each friendly topic are configured and tuya-mqtt will always query these DPS key values during initial connection to the device and report their state appropriately.
For more details on using friendly topics, please read the [DEVICES](docs/DEVICES.md) documentation which discusses how to configure supported devices or define a custom template.
## DPS Topics
Controlling devices directly via DPS topics requires enough knowledge of the device to know which topics accept what values. Described below are two different methods for interfacing with DPS values, the JSON DPS topic, and the individual DPS key topics.
### DPS JSON topic
The JSON DPS topic allows controlling Tuya devices by sending Tuya native style JSON messages to the command topic, and by monitoring for Tuya style JSON replies on the state topic. You can get more details on this format by reading the [TuyAPI documentation](https://codetheweb.github.io/tuyapi/index.html), but, for example, to turn off a dimmer switch you could issue a MQTT message containing the JSON value ```{dps: 1, set: false}``` to the DPS/command topic for the device. If you wanted to turn the dimmer on, and set brightness to 50%, you could issue separate messages ```{dps: 1, set: true}``` and then ```{dps: 2, set: 128}```, or, the Tuya JSON protocol also allows setting multiple values in a single set command using the format ```{'multiple': true, 'data': {'1': true, '2': 128}}```. JSON state and commands should use the DPS/state and DPS/command topics respectively. Below is an example of the topics:
```
tuya/dimmer_device/DPS/state
tuya/dimmer_device/DPS/command
```
### DPS Key topics
In addition to the JSON DPS topic, it's also possible to use the DPS key topics. DPS key topics allow you to monitor and send simple bool/number/string values directly to DPS keys without having to use the Tuya JSON format, the conversion to Tuya JSON is handled by tuya-mqtt. Using the example from above, turning on the dimmer and setting brightness to 50% you would simply issue the message "true" to DPS/1/command and the message "128" to DPS/2/command.
```
tuya/dimmer_device/DPS/1/state --> true/false for on/off state
tuya/dimmer_device/DPS/2/command <-- 1-255 for brightness state
tuya/dimmer_device/DPS/1/state --> accept true/false for turning device on/off
tuya/dimmer_device/DPS/2/command <-- accepts 1-255 for controlling brightness level
```
**!!! Important Note !!!**
When sending commands directly to DPS values there are no limitation on what values are sent as tuya-mqtt has no way to know what are valid vs invalid for any given DPS key. Sending values that are out-of-range or of different types than the DPS key expects can cause unpredictable behavior of your device, from causing timeouts, to reboots, to hanging the device. While I've never seen a device fail to recover after a restart, please keep this in mind when sending commands to your device.
## DPS Topics for devices behind Tuya Gateway
In addition to the DPS Key topics, it's possible to use the DPS for devices behind Tuya Gateway.
'cid' - is the subdevice id.
'cidname' - is the name of subdevice (from devices.conf)
This example demostrates DPS values and commands for Tuya Smart Thermostat Radiator Valve behind Tuya Gateway:
```
Thermostat mode:
tuya/zgw1/thermostat/dsp/4/state --> {"4":"auto"}
Possible values: auto/temp_auto/holiday/manual/comfort/eco/BOOST
tuya/zgw1/thermostat/dps/4/command <-- auto
Temperature Setpoint:
tuya/zgw1/thermostat/dps/2/state --> {"2": 220}
Where 220 - 22.0 Celsius
tuya/zgw1/thermostat/dps/command <-- 225
Current Temperature:
tuya/zgw1/thermostat/dps/3/state --> {"3": 225}
Where 225 - 22.5 Celsius
Valve percent:
tuya/zgw1/thermostat/dps/109/state --> {"109": 30}
Where 30 - 30%
```
You can also use JSON commands with 'cid'
```
tuya/zgw1/dps/command <-- {"dps": 2, "set": 225, "cid": "2c34f13fde594a34"}
```
## Issues
Not all Tuya protocols are supported. For example, some devices use protocol 3.2 which currently remains unsupported by the TuyAPI project due to lack of enough information to reverse engineer the protocol. If you are unable to control your devices with tuya-mqtt please verify that you can query and control them with tuya-cli first. If tuya-cli works, then this script should also work, if it doesn't then this script will not work either.
## Integration with openHAB
openHAB 3.x examples are [here](docs/openHAB.md).
## Contributors
- [TheAgentK](https://github.com/TheAgentK)
- [lehanspb](https://github.com/lehanspb)
## Related Projects:
- https://github.com/codetheweb/tuyapi

View File

@@ -1,29 +0,0 @@
// Object to capture process exits and call app specific cleanup function
var debug = require('debug')('Cleanup');
function noOp() {};
exports.Cleanup = function Cleanup(callback) {
// attach user callback to the process event emitter
// if no callback, it will still exit gracefully on Ctrl-C
callback = callback || noOp;
process.on('cleanup', callback);
// do app specific cleaning before exiting
process.on('exit', function () {
process.emit('cleanup');
});
// catch ctrl+c event and exit normally
process.on('SIGINT', function () {
debug('Ctrl-C...');
process.exit(2);
});
//catch uncaught exceptions, trace, then exit normally
process.on('uncaughtException', function (e) {
debug('Uncaught Exception...', e.stack);
process.exit(99);
});
};

View File

@@ -3,6 +3,5 @@
"port": 1883,
"topic": "tuya/",
"mqtt_user": "",
"mqtt_pass": "",
"qos": 2
"mqtt_pass": ""
}

29
devices/generic-device.js Normal file
View File

@@ -0,0 +1,29 @@
const TuyaDevice = require('./tuya-device')
const debug = require('debug')('tuya-mqtt:device')
const utils = require('../lib/utils')
class GenericDevice extends TuyaDevice {
async init() {
this.deviceData.mdl = 'Generic Device'
// Check if custom template in device config
if (this.config.hasOwnProperty('template')) {
// Map generic DPS topics to device specific topic names
this.deviceTopics = this.config.template
} else {
// Try to get schema to at least know what DPS keys to get initial update
const result = await this.device.get({"schema": true})
if (!utils.isJsonString(result)) {
if (result === 'Schema for device not available') {
debug('Device id '+this.config.id+' failed schema discovery and no custom template defined')
debug('Cannot get initial DPS state data for device '+this.options.name+' but data updates will be publish')
}
}
}
// Get initial states and start publishing topics
this.getStates()
}
}
module.exports = GenericDevice

179
devices/rgbtw-light.js Normal file
View File

@@ -0,0 +1,179 @@
const TuyaDevice = require('./tuya-device')
const debug = require('debug')('tuya-mqtt:device-detect')
const debugDiscovery = require('debug')('tuya-mqtt:discovery')
const utils = require('../lib/utils')
class RGBTWLight extends TuyaDevice {
async init() {
// If no manual config try to detect device settings
if (!this.config.dpsPower) {
await this.guessLightInfo()
}
// If detection failed and no manual config return without initializing
if (!this.guess.dpsPower && !this.config.dpsPower) {
debug('Automatic discovery of Tuya bulb settings failed and no manual configuration')
return
}
// Set device specific variables
this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : this.guess.dpsPower
this.config.dpsMode = this.config.dpsMode ? this.config.dpsMode : this.guess.dpsMode
this.config.dpsWhiteValue = this.config.dpsWhiteValue ? this.config.dpsWhiteValue : this.guess.dpsWhiteValue
this.config.whiteValueScale = this.config.whiteValueScale ? this.config.whiteValueScale : this.guess.whiteValueScale
this.config.dpsColorTemp = this.config.dpsColorTemp ? this.config.dpsColorTemp : this.guess.dpsColorTemp
this.config.minColorTemp = this.config.minColorTemp ? this.config.minColorTemp : 154 // ~6500K
this.config.maxColorTemp = this.config.maxColorTemp ? this.config.maxColorTemp : 400 // ~2500K
this.config.colorTempScale = this.config.colorTempScale ? this.config.colorTempScale : this.guess.colorTempScale
this.config.dpsColor = this.config.dpsColor ? this.config.dpsColor : this.guess.dpsColor
this.config.colorType = this.config.colorType ? this.config.colorType : this.guess.colorType
this.deviceData.mdl = 'RGBTW Light'
this.isRgbtwLight = true
// Set white value transform math
let whiteValueStateMath
let whiteValueCommandMath
if (this.config.whiteValueScale === 255) {
// Devices with brightness scale of 255 seem to not allow values
// less then 25 (10%) without producing timeout errors.
whiteValueStateMath = '/2.3-10.86'
whiteValueCommandMath = '*2.3+25'
} else {
// For other scale (usually 1000), 10-1000 seems OK.
whiteValueStateMath = '/('+this.config.whiteValueScale+'/100)'
whiteValueCommandMath = '*('+this.config.whiteValueScale+'/100)'
}
// Map generic DPS topics to device specific topic names
this.deviceTopics = {
state: {
key: this.config.dpsPower,
type: 'bool'
},
white_brightness_state: {
key: this.config.dpsWhiteValue,
type: 'int',
topicMin: 0,
topicMax: 100,
stateMath: whiteValueStateMath,
commandMath: whiteValueCommandMath
},
hs_state: {
key: this.config.dpsColor,
type: this.config.colorType,
components: 'h,s'
},
color_brightness_state: {
key: this.config.dpsColor,
type: this.config.colorType,
components: 'b'
},
hsb_state: {
key: this.config.dpsColor,
type: this.config.colorType,
components: 'h,s,b'
},
mode_state: {
key: this.config.dpsMode,
type: 'str'
}
}
// If device supports Color Temperature add color temp device topic
if (this.config.dpsColorTemp) {
// Values used for tranforming from 1-255 scale to mireds range
const rangeFactor = (this.config.maxColorTemp-this.config.minColorTemp)/100
const scaleFactor = this.config.colorTempScale/100
const tuyaMaxColorTemp = this.config.maxColorTemp/rangeFactor*scaleFactor
this.deviceTopics.color_temp_state = {
key: this.config.dpsColorTemp,
type: 'int',
topicMin: this.config.minColorTemp,
topicMax: this.config.maxColorTemp,
stateMath: '/'+scaleFactor+'*-'+rangeFactor+'+'+this.config.maxColorTemp,
commandMath: '/'+rangeFactor+'*-'+scaleFactor+'+'+tuyaMaxColorTemp
}
}
// Send home assistant discovery data and give it a second before sending state updates
this.initDiscovery()
await utils.sleep(1)
// Get initial states and start publishing topics
this.getStates()
}
initDiscovery() {
const configTopic = 'homeassistant/light/'+this.config.id+'/config'
const discoveryData = {
name: (this.config.name) ? this.config.name : this.config.id,
state_topic: this.baseTopic+'state',
command_topic: this.baseTopic+'command',
brightness_state_topic: this.baseTopic+'color_brightness_state',
brightness_command_topic: this.baseTopic+'color_brightness_command',
brightness_scale: 100,
hs_state_topic: this.baseTopic+'hs_state',
hs_command_topic: this.baseTopic+'hs_command',
white_value_state_topic: this.baseTopic+'white_brightness_state',
white_value_command_topic: this.baseTopic+'white_brightness_command',
white_value_scale: 100,
availability_topic: this.baseTopic+'status',
payload_available: 'online',
payload_not_available: 'offline',
unique_id: this.config.id,
device: this.deviceData
}
if (this.config.dpsColorTemp) {
discoveryData.color_temp_state_topic = this.baseTopic+'color_temp_state'
discoveryData.color_temp_command_topic = this.baseTopic+'color_temp_command'
discoveryData.min_mireds = this.config.minColorTemp
discoveryData.max_mireds = this.config.maxColorTemp
}
debugDiscovery('Home Assistant config topic: '+configTopic)
debugDiscovery(discoveryData)
this.publishMqtt(configTopic, JSON.stringify(discoveryData))
}
async guessLightInfo() {
this.guess = new Object()
debug('Attempting to detect light capabilites and DPS values...')
debug('Querying DPS 2 for white/color mode setting...')
// Check if DPS 2 contains typical values for RGBTW light
const mode2 = await this.device.get({"dps": 2})
const mode21 = await this.device.get({"dps": 21})
if (mode2 && (mode2 === 'white' || mode2 === 'colour' || mode2.toString().includes('scene'))) {
debug('Detected likely Tuya color bulb at DPS 1-5, checking more details...')
this.guess = {'dpsPower': 1, 'dpsMode': 2, 'dpsWhiteValue': 3, 'whiteValueScale': 255, 'dpsColorTemp': 4, 'colorTempScale': 255, 'dpsColor': 5}
} else if (mode21 && (mode21 === 'white' || mode21 === 'colour' || mode21.toString().includes('scene'))) {
debug('Detected likely Tuya color bulb at DPS 20-24, checking more details...')
this.guess = {'dpsPower': 20, 'dpsMode': 21, 'dpsWhiteValue': 22, 'whiteValueScale': 1000, 'dpsColorTemp': 23, 'colorTempScale': 1000, 'dpsColor': 24}
}
if (this.guess.dpsPower) {
debug('Attempting to detect if bulb supports color temperature...')
const colorTemp = await this.device.get({"dps": this.guess.dpsColorTemp})
if (colorTemp !== '' && colorTemp >= 0 && colorTemp <= this.guess.colorTempScale) {
debug('Detected likely color temperature support')
} else {
debug('No color temperature support detected')
this.guess.dpsColorTemp = 0
}
debug('Attempting to detect Tuya color format used by device...')
const color = await this.device.get({"dps": this.guess.dpsColor})
if (this.guess.dpsPower === 1) {
this.guess.colorType = (color && color.length === 12) ? 'hsb' : 'hsbhex'
} else {
this.guess.colorType = (color && color.length === 14) ? 'hsbhex' : 'hsb'
}
debug ('Detected Tuya color format '+this.guess.colorType.toUpperCase())
}
}
}
module.exports = RGBTWLight

76
devices/simple-dimmer.js Normal file
View File

@@ -0,0 +1,76 @@
const TuyaDevice = require('./tuya-device')
const debug = require('debug')('tuya-mqtt:device')
const debugDiscovery = require('debug')('tuya-mqtt:discovery')
const utils = require('../lib/utils')
class SimpleDimmer extends TuyaDevice {
async init() {
// Set device specific variables
this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : 1
this.config.dpsBrightness = this.config.dpsBrightness ? this.config.dpsBrightness : 2
this.config.brightnessScale = this.config.brightnessScale ? this.config.brightnessScale : 255
this.deviceData.mdl = 'Dimmer Switch'
// Set white value transform math
let brightnessStateMath
let brightnessCommandMath
if (this.config.brightnessScale === 255) {
// Devices with brightness scale of 255 seem to not allow values
// less then 25 (10%) without producing timeout errors.
brightnessStateMath = '/2.3-10.86'
brightnessCommandMath = '*2.3+25'
} else {
// For other scale (usually 1000), 10-1000 seems OK.
brightnessStateMath = '/('+this.config.brightnessScale+'/100)'
brightnessCommandMath = '*('+this.config.brightnessScale+'/100)'
}
// Map generic DPS topics to device specific topic names
this.deviceTopics = {
state: {
key: this.config.dpsPower,
type: 'bool'
},
brightness_state: {
key: this.config.dpsBrightness,
type: 'int',
topicMin: 0,
topicMax: 100,
stateMath: brightnessStateMath,
commandMath: brightnessCommandMath
}
}
// Send home assistant discovery data and give it a second before sending state updates
this.initDiscovery()
await utils.sleep(1)
// Get initial states and start publishing topics
this.getStates()
}
initDiscovery() {
const configTopic = 'homeassistant/light/'+this.config.id+'/config'
const discoveryData = {
name: (this.config.name) ? this.config.name : this.config.id,
state_topic: this.baseTopic+'state',
command_topic: this.baseTopic+'command',
brightness_state_topic: this.baseTopic+'brightness_state',
brightness_command_topic: this.baseTopic+'brightness_command',
brightness_scale: 100,
availability_topic: this.baseTopic+'status',
payload_available: 'online',
payload_not_available: 'offline',
unique_id: this.config.id,
device: this.deviceData
}
debugDiscovery('Home Assistant config topic: '+configTopic)
debugDiscovery(discoveryData)
this.publishMqtt(configTopic, JSON.stringify(discoveryData))
}
}
module.exports = SimpleDimmer

49
devices/simple-switch.js Normal file
View File

@@ -0,0 +1,49 @@
const TuyaDevice = require('./tuya-device')
const debug = require('debug')('tuya-mqtt:device')
const debugDiscovery = require('debug')('tuya-mqtt:discovery')
const utils = require('../lib/utils')
class SimpleSwitch extends TuyaDevice {
async init() {
// Set device specific variables
this.config.dpsPower = this.config.dpsPower ? this.config.dpsPower : 1
this.deviceData.mdl = 'Switch/Socket'
// Map generic DPS topics to device specific topic names
this.deviceTopics = {
state: {
key: this.config.dpsPower,
type: 'bool'
}
}
// Send home assistant discovery data and give it a second before sending state updates
this.initDiscovery()
await utils.sleep(1)
// Get initial states and start publishing topics
this.getStates()
}
initDiscovery() {
const configTopic = 'homeassistant/switch/'+this.config.id+'/config'
const discoveryData = {
name: (this.config.name) ? this.config.name : this.config.id,
state_topic: this.baseTopic+'state',
command_topic: this.baseTopic+'command',
availability_topic: this.baseTopic+'status',
payload_available: 'online',
payload_not_available: 'offline',
unique_id: this.config.id,
device: this.deviceData
}
debugDiscovery('Home Assistant config topic: '+configTopic)
debugDiscovery(discoveryData)
this.publishMqtt(configTopic, JSON.stringify(discoveryData))
}
}
module.exports = SimpleSwitch

716
devices/tuya-device.js Normal file
View File

@@ -0,0 +1,716 @@
const TuyAPI = require('tuyapi')
const { evaluate } = require('mathjs')
const utils = require('../lib/utils')
const debug = require('debug')('tuya-mqtt:tuyapi')
const debugState = require('debug')('tuya-mqtt:state')
const debugCommand = require('debug')('tuya-mqtt:command')
const debugError = require('debug')('tuya-mqtt:error')
class TuyaDevice {
constructor(deviceInfo) {
this.config = deviceInfo.configDevice
this.mqttClient = deviceInfo.mqttClient
this.topic = deviceInfo.topic
// Build TuyAPI device options from device config info
this.options = {
id: this.config.id,
key: this.config.key
}
if (this.config.name) { this.options.name = this.config.name.toLowerCase().replace(/\s|\+|#|\//g,'_') }
if (this.config.ip) {
this.options.ip = this.config.ip
if (this.config.version) {
this.options.version = this.config.version
} else {
this.options.version = '3.1'
}
}
debug(' ############ Config ', JSON.stringify(this.config))
debug(' ############ Options ', JSON.stringify(this.options))
// Set default device data for Home Assistant device registry
// Values may be overridden by individual devices
this.deviceData = {
ids: [ this.config.id ],
name: (this.config.name) ? this.config.name : this.config.id,
mf: 'Tuya'
}
// Initialize properties to hold cached device state data
this.dps = {}
this.cid = {}
this.color = {'h': 0, 's': 0, 'b': 0}
// Device friendly topics
this.deviceTopics = {}
// Missed heartbeat monitor
this.heartbeatsMissed = 0
this.reconnecting = false
// Build the MQTT topic for this device (friendly name or device id)
if (this.options.name) {
this.baseTopic = this.topic + this.options.name + '/'
} else {
this.baseTopic = this.topic + this.options.id + '/'
}
// Create the new Tuya Device
this.device = new TuyAPI(JSON.parse(JSON.stringify(this.options)))
this.device.on('dp-refresh', (data) => {
if (typeof data === 'object') {
if (data.cid) {
debug('Received dp-refresh data from device '+this.options.id+' cid: '+data.cid+' ->', JSON.stringify(data.dps))
} else {
debug('Received dp-refresh data from device '+this.options.id+' ->', JSON.stringify(data.dps))
debug('Received dp-refresh data from device '+this.options.id+' ->', JSON.stringify(data))
}
this.updateState(data)
} else {
if (data !== 'json obj data unvalid') {
debug('Received string data from device '+this.options.id+' ->', data.replace(/[^a-zA-Z0-9 ]/g, ''))
}
}
})
// Listen for device data and call update DPS function if valid
this.device.on('data', (data) => {
if (typeof data === 'object') {
if (data.cid) {
debug('Received JSON data from device '+this.options.id+' cid: '+data.cid+' ->', JSON.stringify(data.dps))
} else {
debug('Received JSON data from device '+this.options.id+' ->', JSON.stringify(data.dps))
debug('Received JSON data from device '+this.options.id+' ->', JSON.stringify(data))
}
this.updateState(data)
} else {
if (data !== 'json obj data unvalid') {
debug('Received string data from device '+this.options.id+' ->', data.replace(/[^a-zA-Z0-9 ]/g, ''))
}
}
})
// Attempt to find/connect to device and start heartbeat monitor
this.connectDevice()
this.monitorHeartbeat()
// On connect perform device specific init
this.device.on('connected', async () => {
// Sometimes TuyAPI reports connection even on socket error
// Wait one second to check if device is really connected before initializing
await utils.sleep(1)
if (this.device.isConnected()) {
debug('Connected to device ' + this.toString())
this.heartbeatsMissed = 0
this.publishMqtt(this.baseTopic+'status', 'online')
this.init()
}
})
// On disconnect perform device specific disconnect
this.device.on('disconnected', async () => {
this.connected = false
this.publishMqtt(this.baseTopic+'status', 'offline')
debug('Disconnected from device ' + this.toString())
await utils.sleep(5)
this.reconnect()
})
// On connect error call reconnect
this.device.on('error', async (err) => {
debugError(err)
await utils.sleep(1)
this.reconnect()
})
// On heartbeat reset heartbeat timer
this.device.on('heartbeat', () => {
this.heartbeatsMissed = 0
})
}
// Get and update cached values of all configured/known dps value for device
async getStates() {
// Suppress topic updates while syncing device state with cached state
this.connected = false
for (let topic in this.deviceTopics) {
const key = this.deviceTopics[topic].key
if (!this.dps[key]) { this.dps[key] = {} }
try {
this.dps[key].val = await this.device.get({"dps": key})
this.dps[key].updated = true
} catch {
debugError('Could not get value for device DPS key '+key)
}
}
this.connected = true
// Force topic update now that all states are fully syncronized
this.publishTopics()
}
// Update cached DPS values on data updates
updateState(data) {
if (typeof data.dps != 'undefined') {
// Update cached device state data
for (let key in data.dps) {
// Only update if the received value is different from previous value
if (this.dps[key] !== data.dps[key]) {
this.dps[key] = {
'val': data.dps[key],
'updated': true
}
}
if (this.isRgbtwLight) {
if (this.config.hasOwnProperty('dpsColor') && this.config.dpsColor == key) {
this.updateColorState(data.dps[key])
} else if (this.config.hasOwnProperty('dpsMode') && this.config.dpsMode == key) {
// If color/white mode is changing, force sending color state
// Allows overriding saturation value to 0% for white mode for the HSB device topics
this.dps[this.config.dpsColor].updated = true
}
}
}
let cid = data.cid
this.cid = cid
// Had to comment this out 2024.02.19
// New Tuya multimode gateway (zigbee & ble) don't send connected state from BLE-devices
// if (this.connected) {
// this.publishTopics()
// }
this.publishTopics()
} else {
debug('Could not updateState ' + JSON.stringify(data))
}
}
// Publish device specific state topics
publishTopics() {
// Don't publish if device is not connected
// Had to comment it out 2024.02.19
// New Tuya multimode gateway (zigbee & ble) don't send connected state from BLE-devices
// if (!this.connected) return
// Loop through and publish all device specific topics
for (let topic in this.deviceTopics) {
const deviceTopic = this.deviceTopics[topic]
const key = deviceTopic.key
// Only publish values if different from previous value
if (this.dps[key] && this.dps[key].updated) {
const state = this.getTopicState(deviceTopic, this.dps[key].val)
if (state) {
if (this.cid) {
// this.publishMqtt(this.baseTopic + cid + '/' + topic, state, true)
// Change cid to cidname
let cidname = this.config.subDevices.find(el => el.cid === this.cid);
this.publishMqtt(this.baseTopic + cidname.name + '/' + topic, state, true)
} else {
this.publishMqtt(this.baseTopic + topic, state, true)
}
}
}
}
// Publish Generic Dps Topics
this.publishDpsTopics()
}
// Publish all dps-values to topic
publishDpsTopics() {
try {
if (!Object.keys(this.dps).length) { return }
let dpsTopic
if (this.cid) {
// dpsTopic = this.baseTopic + this.cid + '/dps'
// Change cid to cidname
let cidname = this.config.subDevices.find(el => el.cid === this.cid);
dpsTopic = this.baseTopic + cidname.name + '/dps'
} else {
dpsTopic = this.baseTopic + 'dps'
}
// const dpsTopic = this.baseTopic + 'dps'
// Publish DPS JSON data if not empty
let data = {}
for (let key in this.dps) {
// Only publish values if different from previous value
if (this.dps[key].updated) {
data[key] = this.dps[key].val
}
}
data = JSON.stringify(data)
const dpsStateTopic = dpsTopic + '/state'
debugState('MQTT DPS JSON: ' + dpsStateTopic + ' -> ', data)
this.publishMqtt(dpsStateTopic, data, false)
// Publish dps/<#>/state value for each device DPS
// or cid/dps/<#>/state if cid exists
for (let key in this.dps) {
// Only publish values if different from previous value
if (this.dps[key].updated) {
const dpsKeyTopic = dpsTopic + '/' + key + '/state'
const data = this.dps.hasOwnProperty(key) ? this.dps[key].val.toString() : 'None'
debugState('MQTT DPS'+key+': '+dpsKeyTopic+' -> ', data)
this.publishMqtt(dpsKeyTopic, data, false)
this.dps[key].updated = false
}
}
} catch (e) {
debugError(e);
}
}
// Get the friendly topic state based on configured DPS value type
getTopicState(deviceTopic, value) {
let state
switch (deviceTopic.type) {
case 'bool':
state = value ? 'ON' : 'OFF'
break;
case 'int':
case 'float':
state = this.parseNumberState(value, deviceTopic)
break;
case 'hsb':
case 'hsbhex':
// Return comma separate array of component values for specific topic
state = new Array()
const components = deviceTopic.components.split(',')
for (let i in components) {
// If light is in white mode always report saturation 0%, otherwise report actual value
state.push((components[i] === 's' && this.dps[this.config.dpsMode].val === 'white') ? 0 : this.color[components[i]])
}
state = (state.join(','))
break;
case 'str':
state = value ? value : ''
break;
}
return state
}
// Parse the received state numeric value based on deviceTopic rules
parseNumberState(value, deviceTopic) {
// Check if it's a number and it's not outside of defined range
if (isNaN(value)) {
return ''
}
// Perform any required math transforms before returing command value
switch (deviceTopic.type) {
case 'int':
value = (deviceTopic.stateMath) ? parseInt(Math.round(evaluate(value+deviceTopic.stateMath))) : parseInt(value)
break;
case 'float':
value = (deviceTopic.stateMath) ? parseFloat(evaluate(value+deviceTopic.stateMath)) : parseFloat(value)
break;
}
return value.toString()
}
// Initial processing of MQTT commands for all command topics
processCommand(message, commandTopic) {
let command
if (utils.isJsonString(message)) {
debugCommand('Received MQTT command message is a JSON string')
command = JSON.parse(message);
} else {
debugCommand('Received MQTT command message is a text string')
command = message.toLowerCase()
}
// If get-states command, then updates all states and re-publish topics
if (commandTopic === 'command' && command === 'get-states') {
// Handle "get-states" command to update device state
debugCommand('Received command: ', command)
this.getStates()
} else {
// Call device specific command topic handler
this.processDeviceCommand(command, commandTopic)
}
}
// Process MQTT commands for all device command topics
processDeviceCommand(command, commandTopic) {
// Determine state topic from command topic to find proper template
const stateTopic = commandTopic.replace('command', 'state')
const deviceTopic = this.deviceTopics.hasOwnProperty(stateTopic) ? this.deviceTopics[stateTopic] : ''
if (deviceTopic) {
// debugCommand('Device '+this.options.id+' received command topic: '+commandTopic+', message: '+command)
let commandResult = this.sendTuyaCommand(command, deviceTopic)
if (!commandResult) {
debugCommand('Command topic '+this.baseTopic+commandTopic+' received invalid value: '+command)
}
} else {
debugCommand('Invalid command topic '+this.baseTopic+commandTopic+' for device id: '+this.config.id)
return
}
}
// Process Tuya JSON commands via DPS command topic
processDpsCommand(message) {
if (utils.isJsonString(message)) {
const command = JSON.parse(message)
debugCommand('Parsed Tuya JSON command: '+JSON.stringify(command))
this.set(command)
} else {
debugCommand('DPS command topic requires Tuya style JSON value')
}
}
// Process text based Tuya commands via DPS key command topics
processDpsKeyCommand(message, dpsKey) {
if (utils.isJsonString(message)) {
debugCommand('Individual DPS command topics do not accept JSON values')
} else {
const dpsMessage = this.parseDpsMessage(message)
debugCommand('Received command for DPS'+dpsKey+': ', message)
const command = {
dps: dpsKey,
set: dpsMessage
}
this.set(command)
}
}
processDpsKeyWcidNameCommand(message, subDevDpsKey, cidName) {
if (utils.isJsonString(message)) {
debugCommand('Individual DPS command topics do not accept JSON values')
} else {
const dpsMessage = this.parseDpsMessage(message)
let subdev = this.config.subDevices.find(el => el.name === cidName);
const sdcid = subdev.cid
debugCommand('Received command for '+sdcid+' DPS'+subDevDpsKey+': ', message)
const command = {
dps: subDevDpsKey,
set: dpsMessage,
cid: sdcid
}
this.set(command)
}
}
// Parse string message into boolean and number types
parseDpsMessage(message) {
if (typeof message === 'boolean' ) {
return message;
} else if (message === 'true' || message === 'false') {
return (message === 'true') ? true : false
} else if (!isNaN(message)) {
return Number(message)
} else {
return message
}
}
// Set state based on command topic
sendTuyaCommand(message, deviceTopic) {
let command = message.toLowerCase()
const tuyaCommand = new Object()
tuyaCommand.dps = deviceTopic.key
switch (deviceTopic.type) {
case 'bool':
if (command === 'toggle') {
tuyaCommand.set = !this.dps[tuyaCommand.dps].val
} else {
command = this.parseBoolCommand(command)
if (typeof command.set === 'boolean') {
tuyaCommand.set = command.set
} else {
tuyaCommand.set = '!!!INVALID!!!'
}
}
break;
case 'int':
case 'float':
tuyaCommand.set = this.parseNumberCommand(command, deviceTopic)
break;
case 'hsb':
this.updateCommandColor(command, deviceTopic.components)
tuyaCommand.set = this.parseTuyaHsbColor()
break;
case 'hsbhex':
this.updateCommandColor(command, deviceTopic.components)
tuyaCommand.set = this.parseTuyaHsbHexColor()
break;
default:
// If type is not one of the above just use the raw string as is
tuyaCommand.set = message
}
if (tuyaCommand.set === '!!!INVALID!!!') {
return false
} else {
if (this.isRgbtwLight) {
this.setLight(deviceTopic, tuyaCommand)
} else {
this.set(tuyaCommand)
}
return true
}
}
// Convert simple bool commands to true/false
parseBoolCommand(command) {
switch(command) {
case 'on':
case 'off':
case '0':
case '1':
case 'true':
case 'false':
return {
set: (command === 'on' || command === '1' || command === 'true' || command === 1) ? true : false
}
default:
return command
}
}
// Validate/transform set interger values
parseNumberCommand(command, deviceTopic) {
let value = undefined
const invalid = '!!!INVALID!!!'
// Check if it's a number and it's not outside of defined range
if (isNaN(command)) {
return invalid
} else if (deviceTopic.hasOwnProperty('topicMin') && command < deviceTopic.topicMin) {
debugError('Received command value "'+command+'" that is less than the configured minimum value')
debugError('Overriding command with minimum value '+deviceTopic.topicMin)
command = deviceTopic.topicMin
} else if (deviceTopic.hasOwnProperty('topicMax') && command > deviceTopic.topicMax) {
debugError('Received command value "'+command+'" that is greater than the configured maximum value')
debugError('Overriding command with maximum value: '+deviceTopic.topicMax)
command = deviceTopic.topicMax
}
// Perform any required math transforms before returing command value
switch (deviceTopic.type) {
case 'int':
if (deviceTopic.commandMath) {
value = parseInt(Math.round(evaluate(command+deviceTopic.commandMath)))
} else {
value = parseInt(command)
}
break;
case 'float':
if (deviceTopic.commandMath) {
value = parseFloat(evaluate(command+deviceTopic.commandMath))
} else {
value = parseFloat(command)
}
break;
}
return value
}
// Takes Tuya color value in HSB or HSBHEX format and updates cached HSB color state for device
// Credit homebridge-tuya project for HSB/HSBHEX conversion code
updateColorState(value) {
let h, s, b
if (this.config.colorType === 'hsbhex') {
[, h, s, b] = (value || '0000000000ffff').match(/^.{6}([0-9a-f]{4})([0-9a-f]{2})([0-9a-f]{2})$/i) || [0, '0', 'ff', 'ff'];
this.color.h = parseInt(h, 16)
this.color.s = Math.round(parseInt(s, 16) / 2.55) // Convert saturation to 100 scale
this.color.b = Math.round(parseInt(b, 16) / 2.55) // Convert brightness to 100 scale
} else {
[, h, s, b] = (value || '000003e803e8').match(/^([0-9a-f]{4})([0-9a-f]{4})([0-9a-f]{4})$/i) || [0, '0', '3e8', '3e8']
// Convert from Hex to Decimal and cache values
this.color.h = parseInt(h, 16)
this.color.s = Math.round(parseInt(s, 16) / 10) // Convert saturation to 100 Scale
this.color.b = Math.round(parseInt(b, 16) / 10) // Convert brightness to 100 scale
}
// Initialize the command color values with existing color state
if (!this.hasOwnProperty('cmdColor')) {
this.cmdColor = {
'h': this.color.h,
's': this.color.s,
'b': this.color.b
}
}
}
// Caches color updates when HSB components have separate device topics
// cmdColor property always contains the desired HSB color state based on received
// command topic messages vs actual device color state, which may be pending
updateCommandColor(value, components) {
// Update any HSB component with a changed value
components = components.split(',')
const values = value.split(',')
for (let i in components) {
this.cmdColor[components[i]] = Math.round(values[i])
}
}
// Returns Tuya HSB format value from current cmdColor HSB values
// Credit homebridge-tuya project for HSB conversion code
parseTuyaHsbColor() {
let {h, s, b} = this.cmdColor
const hexColor = h.toString(16).padStart(4, '0') + (10 * s).toString(16).padStart(4, '0') + (10 * b).toString(16).padStart(4, '0')
return hexColor
}
// Returns Tuya HSBHEX format value from current cmdColor HSB values
// Credit homebridge-tuya project for HSBHEX conversion code
parseTuyaHsbHexColor() {
let {h, s, b} = this.cmdColor
const hsb = h.toString(16).padStart(4, '0') + Math.round(2.55 * s).toString(16).padStart(2, '0') + Math.round(2.55 * b).toString(16).padStart(2, '0');
h /= 60;
s /= 100;
b *= 2.55;
const
i = Math.floor(h),
f = h - i,
p = b * (1 - s),
q = b * (1 - s * f),
t = b * (1 - s * (1 - f)),
rgb = (() => {
switch (i % 6) {
case 0:
return [b, t, p];
case 1:
return [q, b, p];
case 2:
return [p, b, t];
case 3:
return [p, q, b];
case 4:
return [t, p, b];
case 5:
return [b, p, q];
}
})().map(c => Math.round(c).toString(16).padStart(2, '0')),
hex = rgb.join('');
return hex + hsb;
}
// Set white/colour mode based on received commands
async setLight(topic, command) {
let targetMode = undefined
if (topic.key === this.config.dpsWhiteValue || topic.key === this.config.dpsColorTemp) {
// If setting white level or color temperature, light should be in white mode
targetMode = 'white'
} else if (topic.key === this.config.dpsColor) {
// Split device topic HSB components into array
const components = topic.components.split(',')
// If device topic inlucdes saturation check for changes
if (components.includes('s')) {
if (this.cmdColor.s < 10) {
// Saturation changed to < 10% = white mode
targetMode = 'white'
} else {
// Saturation changed to >= 10% = color mode
targetMode = 'colour'
}
} else {
// For other cases stay in existing mode
targetMode = this.dps[this.config.dpsMode].val
}
}
// Send the issued command
this.set(command)
// Make sure the bulb stays in the correct mode
if (targetMode) {
command = {
dps: this.config.dpsMode,
set: targetMode
}
this.set(command)
}
}
// Simple function to help debug output
toString() {
return this.config.name+' (' +(this.options.ip ? this.options.ip+', ' : '')+this.options.id+', '+this.options.key+')'
}
set(command) {
debug('Set device '+this.options.id+' -> '+JSON.stringify(command))
return new Promise((resolve, reject) => {
this.device.set(command).then((result) => {
resolve(result)
})
})
}
// Search for and connect to device
connectDevice() {
// Find device on network
debug('Search for device id '+this.options.id)
this.device.find().then(() => {
debug('Found device id '+this.options.id)
// Attempt connection to device
this.device.connect().catch((error) => {
debugError(error.message)
this.reconnect()
})
}).catch(async (error) => {
debugError(error.message)
debugError('Will attempt to find device again in 60 seconds')
await utils.sleep(60)
this.connectDevice()
})
}
// Retry connection every 10 seconds if unable to connect
async reconnect() {
if (!this.reconnecting) {
this.reconnecting = true
debugError('Error connecting to device id '+this.options.id+'...retry in 10 seconds.')
await utils.sleep(10)
this.connectDevice()
this.reconnecting = false
}
}
// Republish device discovery/state data (used for Home Assistant state topic)
async republish() {
const status = (this.device.isConnected()) ? 'online' : 'offline'
this.publishMqtt(this.baseTopic+'status', status)
await utils.sleep(1)
this.init()
}
// Simple function to monitor heartbeats to determine if
monitorHeartbeat() {
setInterval(async () => {
if (this.connected) {
if (this.heartbeatsMissed > 3) {
debugError('Device id '+this.options.id+' not responding to heartbeats...disconnecting')
this.device.disconnect()
await utils.sleep(1)
this.connectDevice()
} else if (this.heartbeatsMissed > 0) {
const errMessage = this.heartbeatsMissed > 1 ? " heartbeats" : " heartbeat"
debugError('Device id '+this.options.id+' has missed '+this.heartbeatsMissed+errMessage)
}
this.heartbeatsMissed++
}
}, 10000)
}
// Publish MQTT
publishMqtt(topic, message, isDebug) {
if (isDebug) { debugState(topic, message) }
this.mqttClient.publish(topic, message, { qos: 1 });
}
}
module.exports = TuyaDevice

213
docs/DEVICES.md Normal file
View File

@@ -0,0 +1,213 @@
# tuya-mqtt - Devices
The most powerful feature in tuya-mqtt is the ability to configure devices to use friendly topics. For some devices there exist pre-defined device templates which makes using those devices quite easy, simply add the type information to the devices.conf file and tuya-mqtt automatically creates friendly topics for that device.
Friendly topics make it easy to communicate with the device in a standard way and thus integrating into various Home Automation platforms. The topic style generally follows that used by the Home Assistant MQTT integration components and the pre-defined devices automatically send Home Assistant style MQTT discovery messages during startup to make integration with Home Assistant, or other platforms which understand Home Assistant MQTT discovery, even easier.
If the device does not have a pre-defined device template, it's possible to create a template using the [generic device template](#generic-device-templates) feature.
## Pre-defined Device Templates
Pre-defined device templates (except for the Generic Device) will always expose friendly topics for the given device in a consistent manner. Currently the following pre-defined device templates are available:
| Device Type | Descrition |
| --- | --- |
| SimpleSwitch | Supports simple on/off devices |
| SimpleDimmer | Supports simple devices with on/on and brightness |
| RGBTWLight | Supports color/white lights with optional color temerature support |
| GenericDevice | Allows defining a custom template for any device |
To use a device template, simply add the "type" option to the devices.conf similar to the following example:
```
[
{
name: 'Tuya Device 1',
id: '86435357d8b123456789',
key: '8b2a69c9876543210',
type: 'RGBTWLight'
}
]
```
Once the device type is defined tuya-mqtt will attempt to create friendly topics for that device type on connection to the device. Each device type defines specific defaults for DPS values which are typical for common Tuya devices and some, like RGBTWLight, have logic to attempt to detect different variation by querying the device. The goal is that, in most cases, simply adding the type is all that is needed, however, in many cases it is also possible to override the manual settings for the device. The device friendly topics and options for each device are documented below.
### SimpleSwitch
Simple devices that support only on/off.
| Topic | Description | Values |
| --- | --- | --- |
| state | Power state | on/off |
| command | Set power state | on/off, 0/1, true/false |
Manual configuration options:
| Option | Description | Default |
| --- | --- | --- |
| dpsPower | DPS key for power state | 1 |
### SimpleDimmer
Simple device with on/off and brightness functions (dimmer switches or lights)
| Topic | Description | Values |
| --- | --- | --- |
| state | Power state | on/off |
| command | Set power state | on/off, 0/1, true/false |
| brightness_state | Brightness in % | 0-100 |
| brightness_command | set brightness in % | 0-100 |
Manual configuration options:
| Option | Description | Default |
| --- | --- | --- |
| dpsPower | DPS key for power state | 1 |
| dpsBrightness | DPS key for brightness state | 2 |
| brightnessScale | Scale for brightness DPS value | 255 |
### RGBTWLight
The RGBTWLight device support Tuya color lights (bulbs and LEDs). Tuya lights operate in either white or color mode. The RGBTWLight device automatically switches between modes on certain conditions as documented below:
| Condition | Mode |
| --- | --- |
| Changes white brightness | white |
| Changes to color temperature (for device with color temp support) | white |
| Saturation < 10 % | white |
| Saturation >= 10 % | color |
| All other changes | current mode |
This means changing the hue of the light will only switch to color mode if saturation is also >= 10%. Some lights automatically attempt to switch to color mode when any HSB value is updated however, if the saturation setting remains < 10%, tuya-mqtt will force the light back to white mode in this case. This can cause a very quick flicker when chaning hue or color brightness while the saturation remains below the 10% threshold. I expect this not to be a common issue and implemented this in an attempt to make all tuya lights behave in a consistent way.
When the bulb is in white mode, saturation values in the friendly topics are always reported as 0%. This is true even if the mode is toggled manually from color to white mode using the mode_command topic or the Tuya/SmartLife app. When the light is toggled back to color mode, saturation will be reported at the correct level. This is done primarly as a means to indicate color state to automation platforms that don't have a concept of white/color mode, otherwise a light in white mode may still be represented with a color icon in the platforms UI.
Not all devices support color temperature and the script attempts to detect this capability and enables the color temperature topics only when found. Color temperature topics report in Mireds (commonly used by automation tools) and the default range supports roughly 2500K-6500K. This works reasonably well for most available Tuya devices, even if they are not exactly in this range, but, if you know a devices specific color range, the limits can be manually specified to more accurately reflect the exact color temperature.
Tuya bulbs store their HSB color value in a single DPS key using a custom format. Some bulbs use a 14 character format, referred to as HSBHEX, which represents the saturation and brightness values from 0-255 as 2 character hex, while the others use a 12 character format, referred to as HSB, which still uses hex values, but stores saturation and brightness values from 0-1000 as 4 character hex. The code attempts to autodetect the format used by the bulb and perform the proper conversion in all cases, but this can be overridden for cases where the dection method fails.
| Topic | Description | Values |
| --- | --- | --- |
| state | Power state | on/off |
| command | Set power state | on/off, 0/1, true/false |
| white_brightness_state | White mode brightness in % | 0-100 |
| white_brightness_command | Set white mode brightness in % | 0-100 |
| color_brightness_state | Color mode brightness in % | 0-100 |
| color_brightness_command | Set white mode brightness in % | 0-100 |
| hs_state | Hue, saturation % | H,S (Hue 0-360, Saturation 0-100) |
| hs_command | Set hue, saturation % | H,S (Hue 0-360, Saturation 0-100) |
| hsb_state | Hue, saturation %, brightness % | H,S,B (Hue 0-360, Saturation 0-100, Brightness 0-100) |
| hsb_command | Set hue, saturation %, brightness % | H,S,B (Hue 0-360, Saturation 0-100, Brightness 0-100) |
| mode_state | White/Color mode | 'white', 'colour' (some devices also support scenes here) |
| mode_command | Set white/color mode | 'white', 'colour' (some devices also support scenes here) |
| color_temp_state | Color temperature in mireds (only available if device support color temp) | 154-400 (defult range, can be overridden) |
| color_temp_command | Set color temperature in mireds (only available if device support color temp) | 154-400 (defult range, can be overridden) |
Manual configuration options:
| Option | Description | Default (common detected values) |
| --- | --- | --- |
| dpsPower | DPS key for power state | Auto Detect (1,20) |
| dpsMode | DPS key for white/color mode state | Auto Detect (2,21) |
| dpsWhiteValue | DPS key for white mode brightness | Auto Detect (3,22) |
| whiteValueScale | White mode brightness DPS scale | Auto Detect (255, 1000) |
| dpsColorTemp | DPS key for color temperature | Auto Detect (4,23) |
| minColorTemp | Min color temperature in Mireds | 154 (~6500K) |
| maxColorTemp | Max color temperature in Mireds | 400 (~2500K) |
| colorTempScale | Color temperature DPS key scale | Auto Detect (255, 1000) |
| dpsColor | DPS key for HSB color values | Auto Detect (5,24) |
| colorType | Tuya color format for color DPS key | Auto Detect (hsb, hsbhex) |
To use the manual configuration options simply add them to device.conf file after defining the device type like the following example:
```
[
{
name: 'Tuya Device 1',
id: '86435357d8b123456789',
key: '8b2a69c9876543210',
type: 'RGBTWLight',
dpsPower: 31,
dpsMode: 32,
dpsWhiteValue: 33,
whiteValueScale: 255,
dpsColorTemp: 34,
minColorTemp: 165,
maxColorTemp: 385,
colorTempScale: 255,
dpsColor: 34,
colorType: 'hsbhex'
}
]
```
## Generic Device Templates
If a pre-defined device tempate does not exist for the device, or does not expose all capabilities of the device, there are still mulitple options available to control the devices. One method is to use the DPS topics directly to control the device using either native Tuya JSON commands or via the DPS key values by using the DPS key topics (see [DPS Topics](TOPICS.md#dps-topics)). The second method is to create a template for your device to map DPS key values to friendly topics. The GenericDevice type allows you to manually create a template for any device using the same templating engine as the pre-defined device templates. Once you've created a tempalte for your device, it can be re-used with other, similar devices and you can submit your template to the tuya-mqtt project for other to use, or even for inclusion at a pre-defined device template in the future.
Creating a device template is relatively straightforward, but first you must know what DPS keys your devices uses. The GenericDevice attempts to query all device DPS states on startup, but some devices to not respond to this command, however, the generic device will ALWAYS report any DPS topics from which it receives upated. The easiest way to determine how your device uses it's DPS topics is to connect to the MQTT broker via a tool like MQTT Explorer or mosquitto_sub, and watch the topics as you manipulate the device with the Tuya/Smartlife app.
Once you have a reasonable idea of how the device uses it's DPS key values, you can create a template. A simple template for a dimmer looks something like this:
```
[
{
name: 'Tuya Device 1',
id: '86435357d8b123456789',
key: '8b2a69c9876543210',
template: {
state: {
key: 1,
type: 'bool'
},
brightness_state: {
key: 2,
type: 'int',
topicMin: 1,
topicMax: 100,
stateMath: '/2.55',
commandMath: '*2.55'
}
}
}
]
```
The template above defines two topics "state" and "brightness_state", and the template engine automatically creates the corresponding command topics, in this case specifically "command" and "brightness_command".
The "state" topic maps to DPS key 1, and uses a bool (true/false) value in the DPS key. Now you will be able to see "on/off" state in the state topic instead of having to read the true/false value from the DPS/1 topic
The the "brightness_state" topic maps to DPS key 2, and this value defines the brightness using an integer in the 1-255 scale. We define the value as an integer (type: 'int') and the stateMath and commandMath values allow transforming the raw DPS value into a more friendly value that will be presented in the topic. In this case the raw DPS value will be divided by 2.55 before being published to the state, and and received commands will be mulitpled by that same value, converting the 1-255 to a simple 1-100 scale. Note that the topicMin and topicMax values set the minimum and maximum values that the state topic will report and that the command topic will accept. These values are "post-math" for state topics, and "pre-math" for command topics.
The following tables define the available template value types and their options:
### Boolean values
| option | value |
| --- | --- |
| type | 'bool' |
| key | DPS key of the value |
### Integer values
| option | value |
| --- | --- |
| type | 'int' |
| key | DPS key of the value |
| topicMin | Minumum value allowed for the command topic |
| topicMax | Maximum value allowed for the command topic |
| stateMath | Simple math applied to the DPS key value before being published to state topic |
| commandMath | Simple math applied to command value before being set to DPS key |
### Floating point values
| option | value |
| --- | --- |
| type | 'float' |
| key | DPS key of the value |
| topicMin | Minumum value allowed for the command topic |
| topicMax | Maximum value allowed for the command topic |
| stateMath | Simple math applied to the DPS key value before being published to state topic |
| commandMath | Simple math applied to command value before being set to DPS key |
### String values
| option | value |
| --- | --- |
| type | 'str' |
| key | DPS key of the value |
### Tuya HSB values (newer style Tuya, 12 character color value)
| option | value |
| --- | --- |
| type | 'hsb' |
| key | DPS key of the value |
| components | Comma separated list of HSB components that should be included in this topic |
### Tuya HSBHEX values (older style Tuya 14 character color value)
| option | value |
| --- | --- |
| type | 'hsbhex' |
| key | DPS key of the value |
| components | Comma separated list of HSB components that should be included in this topic |
Using these value types you can define templates for a wide range of devices. Additional types and options are likely to be included in future versions of tuya-mqtt.

217
docs/openHAB.md Normal file
View File

@@ -0,0 +1,217 @@
## Example items for OpenHAB 3.x Bindings
### Tuya Smart Thermostat Radiator Valve behind Tuya Gateway
### Things channels (configured via web):
#### Thermostat mode:
Channel identifier:
```
mode
```
State:
```
tuya/zgw1/1a24fkfffe6b4e24/dsp/4/state
```
Example output: {"4":"auto"}
Possible values: auto/temp_auto/holiday/manual/comfort/eco/BOOST
Command:
```
tuya/zgw1/dps/command
```
Outgoing Value Format:
```
{"dps": 4, "set": "%s", "cid": "1a24fkfffe6b4e24"}
```
#### Temperature Setpoint
Channel identifier:
```
setpoint
```
State:
```
tuya/zgw1/1a24fkfffe6b4e24/dsp/2/state
```
Example output: {"2": 220}
Command:
```
tuya/zgw1/dps/command
```
Incoming Value Transformations:
```
JS:tuya-in.js
```
Outgoing Value Transformation:
```
JS:tuya-out.js
```
Outgoing Value Format:
```
{"dps": 2, "set": "%s", "cid": "1a24fkfffe6b4e24"}
```
#### Current Temperature
Channel identifier:
```
temperature
```
State:
```
tuya/zgw1/1a24fkfffe6b4e24/dsp/3/state
```
Incoming Value Transformations:
```
JS:tuya-in.js
```
#### Valve percent
Channel identifier:
```
valve_percent
```
State:
```
tuya/zgw1/1a24fkfffe6b4e24/dsp/109/state
```
Command:
```
tuya/zgw1/dps/command
```
Outgoing Value Format:
```
{"dps": 109, "set": %s, "cid": "1a24fkfffe6b4e24"}
```
### Transformations
tuya-in.js:
```
(function(i) {
return (i / 10)
})(input)
```
tuya-out.js:
```
(function(i) {
return (i * 10)
})(input)
```
### items/thermostat.items
```
String Radiator_Mode "Mode" <radiator> { channel="mqtt:topic:home:zgw1dev1:mode" }
Number Radiator_Setpoint "Temperature setpoint [%.1f °C]" <radiator> { channel="mqtt:topic:home:zgw1dev1:setpoint" }
Number Radiator_Temperature "Current temperature [%.1f °C]" <temperature> { channel="mqtt:topic:home:zgw1dev1:temperature" }
Number Radiator_Valve_Percent "Valve percent [%d %%]" { channel="mqtt:topic:home:zgw1dev1:valve_percent" }
```
### sitemaps/home.sitemap
```
Frame label="Heating" {
Setpoint item=Radiator_Setpoint minValue=15 maxValue=30 step=0.5
Selection item=Radiator_Mode mappings=[auto='Auto', temp_auto='Auto temp', manual='Manual', comfort='Comfort']
Text item=Radiator_Setpoint
Text item=Radiator_Temperature
Text item=Radiator_Valve_Percent
}
```
### Simple on/off switch with power measurement capability
### Things channels (configured via web):
#### Power switch
Channel identifier:
```
power
```
State:
```
tuya/tuya_device_1/state
```
Example output: {"4":"auto"}
Possible values: auto/temp_auto/holiday/manual/comfort/eco/BOOST
Command:
```
tuya/tuya_device_1/command
```
Custom On/Open Value:
```
ON
```
Custom Off/Closed Value:
```
OFF
```
#### Power consumption watts
Channel identifier:
```
w
```
State:
```
tuya/tuya_device_1/dps/19/state
```
Incoming Value Transformations:
JS:tuya-energy.js
#### Power consumption volts
Channel identifier:
```
v
```
State:
```
tuya/tuya_device_1/dps/20/state
```
Incoming Value Transformations:
JS:tuya-energy.js
### transform/tuya-energy.js
```
(function(i) {
return Math.ceil(i / 10)
})(input)
```
### items/socket.items
Switch Socket_Power "Socket" { channel="mqtt:topic:socket:power" }
Number Socket_W "Power (W)" { channel="mqtt:topic:socket:w" }
Number Socket_Vt "Power (V) [%s]" { channel="mqtt:topic:socket:v" }
### sitemaps/home.sitempa
Switch item=Socket_Power
Text item=Socket_W
Text item=Socket_Vt

28
lib/utils.js Normal file
View File

@@ -0,0 +1,28 @@
class Utils
{
// Check if data is JSON or not
isJsonString(data) {
try {
const parsedData = JSON.parse(data)
if (parsedData && typeof parsedData === "object") {
return parsedData
}
}
catch (e) { }
return false
}
// Simple sleep function for various required delays
sleep(sec) {
return new Promise(res => setTimeout(res, sec*1000))
}
msSleep(ms) {
return new Promise(res => setTimeout(res, ms))
}
}
module.exports = new Utils()

116
merge-devices.js Normal file
View File

@@ -0,0 +1,116 @@
#!/usr/bin/env node
const fs = require('fs');
const json5 = require('json5');
const utils = require('./lib/utils');
// Setup Exit Handlers
process.on('exit', processExit.bind(0));
process.on('SIGINT', processExit.bind(0));
process.on('SIGTERM', processExit.bind(0));
process.on('uncaughtException', processExit.bind(1));
async function processExit(exitCode) {
if (exitCode || exitCode === 0)
console.error('Exit code: ' + exitCode)
process.exit();
}
// Main code function
const main = async() => {
const date = new Date();
const dateTimeStr = date.getFullYear() + '-' + ("0" + (date.getMonth() + 1)).slice(-2) + '-' + ("0" + date.getDate()).slice(-2) + '_' + ("0" + date.getHours()).slice(-2) + ("0" + date.getMinutes()).slice(-2) + ("0" + date.getSeconds()).slice(-2);
const configDevicesFilename = 'devices.conf';
const configDevicesBackupFilename = `${configDevicesFilename}_${dateTimeStr}.bak`;
const configNewDevicesFilename = `new-${configDevicesFilename}`;
let configDevices;
let configNewDevices;
try {
console.log(`Loading ${configNewDevicesFilename}...`);
configNewDevices = fs.readFileSync(`./${configNewDevicesFilename}`, 'utf8');
configNewDevices = json5.parse(configNewDevices);
}
catch (e) {
console.error(`Could not parse new devices config file [${configNewDevicesFilename}]!`);
console.error(e);
process.exit(1);
}
try {
console.log(`Loading ${configDevicesFilename}...`);
configDevices = fs.readFileSync(`./${configDevicesFilename}`, 'utf8');
configDevices = json5.parse(configDevices);
}
catch (e) {
console.error(`Could not parse devices config file [${configDevicesFilename}]!`);
console.error(e);
process.exit(1);
}
try {
console.log(`Backing up devices config file [${configDevicesFilename}] to [${configDevicesBackupFilename}]...`);
fs.copyFileSync(`./${configDevicesFilename}`, `./${configDevicesBackupFilename}`);
}
catch (e) {
console.error(`Could not make backup of devices config file [${configDevicesFilename}] to [${configDevicesBackupFilename}].`);
console.error(e);
process.exit(1);
}
console.log('Indexing devices...');
// Create a dictionary for faster lookups with many devices
const configDevicesDictionary = {};
for (let configDevice of configDevices) {
configDevicesDictionary[configDevice.id] = configDevice;
}
console.log('Merging devices...');
// Add new devices and update existing devices
for (let configNewDevice of configNewDevices) {
let configDevice = configDevicesDictionary[configNewDevice.id];
if (configDevice == null) {
// Add new device
console.log(`Adding device: ${configNewDevice.name} (id: ${configNewDevice.id})...`);
configDevices.push(configNewDevice);
configDevicesDictionary[configNewDevice.id] = configNewDevice;
continue;
}
if (configDevice.allowMerge === false)
continue; // No merge updates allowed for this device
// Update existing device
if (configDevice.name !== configNewDevice.name) {
console.log(`Updating device name: '${configDevice.name}' to '${configNewDevice.name}' (device id: ${configDevice.key})...`);
configDevice.name = configNewDevice.name;
}
if (configDevice.key !== configNewDevice.key) {
console.log(`Updating device key for: '${configDevice.name}' (device id: ${configDevice.key})...`);
configDevice.key = configNewDevice.key;
}
}
// Sort the devices by name
configDevices.sort((a, b) => a.name < b.name ? -1 : 1);
const configDevicesJson = json5.stringify(configDevices, { space: ' ', quote: '"' });
//console.log(configDevicesJson);
try {
console.log(`Saving devices config file [${configDevicesFilename}]...`);
fs.writeFileSync(`./${configDevicesFilename}`, configDevicesJson, { encoding: 'utf8' });
}
catch (e) {
console.error(`Could not write devices config file [${configDevicesFilename}]!`)
console.error(e)
process.exit(1)
}
console.log('Done!');
}
// Call the main code
main();

2287
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +1,29 @@
{
"name": "tuya-api",
"version": "1.0.0",
"description": "",
"main": "tuya.js",
"name": "tuya-mqtt",
"version": "3.1.4",
"description": "Control Tuya devices locally via MQTT",
"homepage": "https://github.com/lehanspb/tuya-mqtt#readme",
"main": "tuya-mqtt.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"author": {
"name": "TheAgentK",
"email": "lulattsch22@googlemail.com"
},
"license": "ISC",
"dependencies": {
"color-convert": "^1.9.3",
"debug": "^3.2.6",
"mqtt": "^2.18.8",
"tuyapi": "^3.2.3"
"@tuyapi/cli": "^1.15.0",
"color-convert": "^2.0.1",
"debug": "^4.3.1",
"json5": "^2.1.3",
"mqtt": "^4.2.6",
"supports-color": "^8.1.0",
"tuyapi": "^7.5.1",
"mathjs": "8.1.1"
},
"repository": {
"type": "git",
"url": "git://github.com/TheAgentK/tuya-mqtt.git"
"url": "git://github.com/lehanspb/tuya-mqtt.git"
}
}

View File

@@ -1,247 +0,0 @@
const convert = require('color-convert');
const debug = require('debug')('TuyaColor');
/**
* Class to calculate settings for Tuya colors
*/
function TuyaColorLight() {
this.colorMode = 'white'; // or 'colour'
this.brightness = 100; // percentage value use _convertValToPercentage functions below.
this.color = {
H: 130,
S: 100,
L: 50
};
this.hue = this.color.H;
this.saturation = this.color.S;
this.lightness = this.color.L;
this.colorTemperature = 255;
this.colorTempMin = 153;
this.colorTempMax = 500;
this.dps = {};
}
/**
* calculate color value from given percentage
* @param {Integer} percentage 0-100 percentage value
* @returns {Integer} color value from 0-255
*/
TuyaColorLight.prototype._convertPercentageToVal = function (percentage) {
var tmp = Math.round(255 * (percentage / 100));
debug('Converted ' + percentage + ' to: ' + tmp);
return tmp;
};
/**
* calculate percentage from color value
* @param {Integer} val 0-255 color value
* @returns {Integer} HK-Value
*/
TuyaColorLight.prototype._convertValToPercentage = function (val) {
var tmp = Math.round((val / 255) * 100);
debug('Converted ' + val + ' to: ' + tmp);
return tmp;
};
/**
* converts color value to color temperature
* @param {Integer} val
* @returns {Integer} percentage from 0-100
*/
TuyaColorLight.prototype._convertColorTemperature = function (val) {
var tmpRange = this.colorTempMax - this.colorTempMin;
var tmpCalc = Math.round((val / this.colorTempMax) * 100);
debug('HK colorTemp Value: ' + val);
debug('HK colorTemp scale min : ' + this.colorTempMin);
debug('HK colorTemp scale max : ' + this.colorTempMax);
debug('HK colorTemp range (tmpRange): ' + tmpRange);
debug('HK colorTemp % tmpCalc: ' + tmpCalc);
var tuyaColorTemp = this._convertPercentageToVal(tmpCalc);
debug('HK tuyaColorTemp: ' + tuyaColorTemp);
return tuyaColorTemp;
};
/**
* Convert color temperature to HK
* @param {Integer} val
* @returns {Integer} HK-Value
*/
TuyaColorLight.prototype._convertColorTemperatureToHK = function (val) {
var tuyaColorTempPercent = this._convertValToPercentage(this.colorTemperature);
var tmpRange = this.colorTempMax - this.colorTempMin;
var tmpCalc = Math.round((tmpRange * (tuyaColorTempPercent / 100)) + this.colorTempMin);
var hkValue = Math.round(tmpCalc);
debug('Tuya color Temperature : ' + val);
debug('Tuya color temp Percent of 255: ' + tuyaColorTempPercent + '%');
debug('HK colorTemp scale min : ' + this.colorTempMin);
debug('HK colorTemp scale max : ' + this.colorTempMax);
debug('HK Color Temp Range: ' + tmpRange);
debug('HK range %: ' + tuyaColorTempPercent);
debug('HK Value: ' + hkValue);
return hkValue;
};
/**
* check if given String is HEX
* @param {String} h
* @returns {boolean}
*/
TuyaColorLight.prototype._ValIsHex = function (h) {
debug("Check if value is hex", h);
return /(^#[0-9A-F]{6}$)|(^#[0-9A-F]{3}$)/i.test(h)
};
/**
* get AlphaHex from percentage brightness
* @param {Integer} brightness
* @return {string} brightness as HEX value
*/
TuyaColorLight.prototype._getAlphaHex = function (brightness) {
var i = brightness / 100;
var alpha = Math.round(i * 255);
var hex = (alpha + 0x10000).toString(16).substr(-2);
var perc = Math.round(i * 100);
debug('alpha percent: ' + perc + '% hex: ' + hex + ' alpha: ' + alpha);
return hex;
};
/**
* Set saturation from value
* @param {Integer} value
*/
TuyaColorLight.prototype.setSaturation = function (value) {
this.color.S = value;
this.saturation = value;
this.colorMode = 'colour';
debug('SET SATURATION: ' + value);
};
/**
* Set Brightness
* @param {Integer} value
*/
TuyaColorLight.prototype.setBrightness = function (value) {
this.brightness = value;
var newValue = this._convertPercentageToVal(value);
debug("BRIGHTNESS from UI: " + value + ' Converted from 100 to 255 scale: ' + newValue);
}
/**
* @param {} value
*/
TuyaColorLight.prototype.setHue = function (value) {
debug('SET HUE: ' + value);
debug('Saturation Value: ' + this.color.S);
this.color.H = value;
//check color and set colormode if necessary
debug("colormode", value, this.color.S);
if (value === 0 && this.color.S === 0) {
this.colorMode = 'white';
debug('SET Color Mode: \'white\'');
} else {
this.colorMode = 'colour';
debug('SET Color Mode: \'colour\' -- dahhhhhh british spelling \'coulour\' really is annoying... why you gotta be special?');
}
return {
color: this.color,
colorMode: this.colorMode,
hue: this.color.H,
saturation: this.saturation
};
};
/**
* Set HSL color
* @param {Integer} hue
* @param {Integer} saturation
* @param {Integer} brightness
*/
TuyaColorLight.prototype.setHSL = function (hue, saturation, brightness) {
this.setSaturation(saturation);
this.setBrightness(brightness);
this.setHue(hue);
}
/**
* Set color from given string
* @param {String} colorValue could be HEX or HSL color type
* @returns {Object} dps settings for given color
*/
TuyaColorLight.prototype.setColor = function (colorValue) {
debug("Recieved color", colorValue);
if (this._ValIsHex(colorValue)) {
debug("Color is Hex");
var color = convert.hex.hsl(colorValue);
} else {
debug("Color is HSL");
var color = colorValue.split(",");
// convert strings to numbers
color.forEach(function (element, key) {
color[key] = parseInt(element, 10);
});
}
debug("Converted color as HSL", {
0: color[0] + " - " + typeof color[0],
1: color[1] + " - " + typeof color[1],
2: color[2] + " - " + typeof color[2]
})
this.setHSL(color[0], color[1], color[2]);
return this.getDps();
}
/**
* get dps settings for current color
* @returns {Object} dps settings
*/
TuyaColorLight.prototype.getDps = function () {
var color = this.color;
var lightness = Math.round(this.brightness / 2);
var brightness = this.brightness;
var apiBrightness = this._convertPercentageToVal(brightness);
var alphaBrightness = this._getAlphaHex(brightness);
var hexColor1 = convert.hsl.hex(color.H, color.S, lightness);
var hexColor2 = convert.hsl.hex(0, 0, lightness);
var colorTemperature = this.colorTemperature;
var lightColor = (hexColor1 + hexColor2 + alphaBrightness).toLowerCase();
var temperature = (this.colorMode === 'colour') ? 255 : this._convertColorTemperature(colorTemperature);
dpsTmp = {
'1': true,
'2': this.colorMode,
'3': apiBrightness,
'4': temperature,
'5': lightColor
// '6' : hexColor + hexColor + 'ff'
};
debug("dps", dpsTmp);
return dpsTmp;
}
module.exports = TuyaColorLight;

View File

@@ -1,229 +0,0 @@
const TuyAPI = require('tuyapi');
const TuyColor = require('./tuya-color');
const debug = require('debug')('TuyAPI-device');
const debugColor = require('debug')('TuyAPI-device-color');
const debugTimer = require('debug')('TuyAPI-device-timer');
/**
*
var steckdose = new TuyaDevice({
id: '03200240600194781244',
key: 'b8bdebab418f5b55',
ip: '192.168.178.45',
type: "socket"
});
*/
var TuyaDevice = (function () {
var devices = [];
var events = {};
var autoTimeout = undefined;
function checkExisiting(id) {
var existing = false;
// Check for existing instance
devices.forEach(device => {
if (device.hasOwnProperty("options")) {
if (id === device.options.id) {
existing = device;
}
}
});
return existing;
}
function resetTimer() {
return;
debugTimer("Reset timer for auto disconnect all devices");
clearTimeout(autoTimeout);
autoTimeout = setTimeout(() => {
debugTimer("Auto disconnect all devices");
TuyaDevice.disconnectAll();
}, 10000);
}
function TuyaDevice(options) {
var device = this;
// Check for existing instance
if (existing = checkExisiting(options.id)) {
return existing;
}
if (!(this instanceof TuyaDevice)) {
return new TuyaDevice(options);
}
options.type = options.type || "socket";
options.persistentConnection = true;
Object.defineProperty(this, 'type', {
value: options.type
});
Object.defineProperty(this, 'options', {
value: options
});
Object.defineProperty(this, 'device', {
value: new TuyAPI(this.options)
});
this.device.on('connected', () => {
debug('Connected to device.');
device.triggerAll('connected');
});
this.device.on('disconnected', () => {
debug('Disconnected from device.');
device.triggerAll('disconnected');
});
this.device.on('data', data => {
debug('Data from device:', data);
device.triggerAll('data', data);
});
this.device.on('error', (err) => {
debug('Error: ' + err);
device.triggerAll('error', err);
});
this.device.connect();
devices.push(this);
resetTimer();
}
TuyaDevice.prototype.triggerAll = function (name, argument) {
var device = this;
var e = events[name] || [];
e.forEach(event => {
event.call(device, argument);
});
}
TuyaDevice.prototype.on = function (name, callback) {
var device = this;
this.device.on(name, function () {
callback.apply(device, arguments);
});
}
TuyaDevice.prototype.get = function (options) {
resetTimer();
return this.device.get(options);
}
TuyaDevice.prototype.set = function (options, callback) {
var device = this;
debug('Setting status:', options);
return this.device.set(options).then(result => {
device.get().then(status => {
debug('Result of setting status to', status);
if (callback != undefined) {
callback.call(device, status);
}
return;
});
});
resetTimer();
}
TuyaDevice.prototype.switch = function (newStatus, callback) {
newStatus = newStatus.toLowerCase();
debug("switch: " + newStatus);
if (newStatus == "on") {
this.switchOn(callback);
}
if (newStatus == "off") {
this.switchOff(callback);
}
if (newStatus == "toggle") {
this.toggle(callback);
}
}
TuyaDevice.prototype.switchOn = function (callback) {
var device = this;
debug("switch -> ON");
device.get().then(status => {
device.set({
set: true
}, callback);
});
}
TuyaDevice.prototype.switchOff = function (callback) {
var device = this;
debug("switch -> OFF");
device.get().then(status => {
device.set({
set: false
}, callback);
});
}
TuyaDevice.prototype.toggle = function (callback) {
var device = this;
device.get().then(status => {
device.set({
set: !status
}, callback);
});
}
TuyaDevice.prototype.setColor = function (hexColor, callback) {
debugColor("Set color to: ", hexColor);
var device = this;
var tuya = this.device;
var color = new TuyColor(tuya);
var dps = color.setColor(hexColor);
debugColor("dps values:", dps);
device.get().then(status => {
device.set({
multiple: true,
data: dps
}, callback);
});
resetTimer();
}
TuyaDevice.prototype.connect = function (callback) {
this.device.connect(callback);
}
TuyaDevice.prototype.disconnect = function (callback) {
this.device.disconnect(callback);
}
Object.defineProperty(TuyaDevice, 'devices', {
value: devices
});
TuyaDevice.connectAll = function () {
devices.forEach(device => {
device.connect();
});
}
TuyaDevice.disconnectAll = function () {
devices.forEach(device => {
device.disconnect();
});
}
TuyaDevice.onAll = function (name, callback) {
if (events[name] == undefined) {
events[name] = [];
}
events[name].push(callback);
devices.forEach(device => {
device.triggerAll(name);
});
}
return TuyaDevice;
}());
module.exports = TuyaDevice;

View File

@@ -1,248 +1,182 @@
const mqtt = require('mqtt');
const TuyaDevice = require('./tuya-device');
const debug = require('debug')('tuya-mqtt');
const debugColor = require('debug')('color');
const debugMqtt = require('debug')('mqtt');
const debugTuya = require('debug')('tuyAPI-Events');
const debugError = require('debug')('error');
var cleanup = require('./cleanup').Cleanup(onExit);
function bmap(istate) {
return istate ? 'ON' : "OFF";
}
function boolToString(istate) {
return istate ? 'true' : "false";
}
var connected = undefined;
var CONFIG = undefined;
try {
CONFIG = require("./config");
} catch (e) {
console.error("Configuration file not found")
debugError(e)
process.exit(1)
}
if (typeof CONFIG.qos == "undefined") {
CONFIG.qos = 2;
}
if (typeof CONFIG.retain == "undefined") {
CONFIG.retain = false;
}
const mqtt_client = mqtt.connect({
host: CONFIG.host,
port: CONFIG.port,
username: CONFIG.mqtt_user,
password: CONFIG.mqtt_pass,
});
mqtt_client.on('connect', function (err) {
debugMqtt("Verbindung mit MQTT-Server hergestellt");
connected = true;
var topic = CONFIG.topic + '#';
mqtt_client.subscribe(topic, {
retain: CONFIG.retain,
qos: CONFIG.qos
});
});
mqtt_client.on("reconnect", function (error) {
if (connected) {
debugMqtt("Verbindung mit MQTT-Server wurde unterbrochen. Erneuter Verbindungsversuch!");
} else {
debugMqtt("Verbindung mit MQTT-Server konnte nicht herrgestellt werden.");
}
connected = false;
});
mqtt_client.on("error", function (error) {
debugMqtt("Verbindung mit MQTT-Server konnte nicht herrgestellt werden.", error);
connected = false;
});
/**
* execute function on topic message
*/
function boolToString(istate) {
return istate == 1 ? 'on' : "off";
}
function convertMessage(message) {
var status = message.toString();
status = boolToString(status);
status = status.toLowerCase();
return status;
}
mqtt_client.on('message', function (topic, message) {
try {
var cMessage = convertMessage(message);
var topic = topic.split("/");
var options = {
type: topic[1],
id: topic[2],
key: topic[3],
ip: topic[4],
};
var exec = topic[5];
if (options.type == "socket" || options.type == "lightbulb") {
debug("device", options);
debug("message", cMessage);
var device = new TuyaDevice(options);
if (exec == "command") {
var status = topic[6];
if (status == null) {
device.switch(cMessage);
} else {
device.switch(status);
}
}
if (exec == "color") {
var color = message.toString();
color = color.toLowerCase();
debugColor("topic: ", topic);
debugColor("onColor: ", color);
device.setColor(color);
}
}
} catch (e) {
debugError(e);
}
});
/**
* Publish current TuyaDevice state to MQTT-Topic
* @param {TuyaDevice} device
* @param {boolean} status
*/
function publishStatus(device, status) {
if (mqtt_client.connected == true) {
try {
var type = device.type;
var tuyaID = device.options.id;
var tuyaKey = device.options.key;
var tuyaIP = device.options.ip;
if (typeof tuyaID != "undefined" && typeof tuyaKey != "undefined" && typeof tuyaIP != "undefined") {
var topic = CONFIG.topic + type + "/" + tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/state";
mqtt_client.publish(topic, status, {
retain: CONFIG.retain,
qos: CONFIG.qos
});
debugTuya("mqtt status updated to:" + topic + " -> " + status);
} else {
debugTuya("mqtt status not updated");
}
} catch (e) {
debugError(e);
}
}
}
function publishColorState(device, state) {
}
/**
* publish all dps-values to topic
* @param {TuyaDevice} device
* @param {Object} dps
*/
function publishDPS(device, dps) {
if (mqtt_client.connected == true) {
try {
var type = device.type;
var tuyaID = device.options.id;
var tuyaKey = device.options.key;
var tuyaIP = device.options.ip;
if (typeof tuyaID != "undefined" && typeof tuyaKey != "undefined" && typeof tuyaIP != "undefined") {
var topic = CONFIG.topic + type + "/" + tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/dps";
var data = JSON.stringify(dps);
debugTuya("mqtt dps updated to:" + topic + " -> ", data);
mqtt_client.publish(topic, data, {
retain: CONFIG.retain,
qos: CONFIG.qos
});
Object.keys(dps).forEach(function (key) {
var topic = CONFIG.topic + type + "/" + tuyaID + "/" + tuyaKey + "/" + tuyaIP + "/dps/" + key;
var data = JSON.stringify(dps[key]);
debugTuya("mqtt dps updated to:" + topic + " -> dps[" + key + "]", data);
mqtt_client.publish(topic, data, {
retain: CONFIG.retain,
qos: CONFIG.qos
});
});
} else {
debugTuya("mqtt dps not updated");
}
} catch (e) {
debugError(e);
}
}
}
/**
* event fires if TuyaDevice sends data
* @see TuyAPI (https://github.com/codetheweb/tuyapi)
*/
TuyaDevice.onAll('data', function (data) {
try {
debugTuya('Data from device ' + this.type + ' :', data);
var status = data.dps['1'];
if (typeof status != "undefined") {
publishStatus(this, bmap(status));
}
publishDPS(this, data.dps);
} catch (e) {
debugError(e);
}
});
/**
* MQTT connection tester
*/
function MQTT_Tester() {
this.interval = null;
function mqttConnectionTest() {
if (mqtt_client.connected != connected) {
connected = mqtt_client.connected;
if (connected) {
debugMqtt('MQTT-Server verbunden.');
} else {
debugMqtt('MQTT-Server nicht verbunden.');
}
}
}
this.destroy = function () {
clearInterval(this.interval);
this.interval = undefined;
}
this.connect = function () {
this.interval = setInterval(mqttConnectionTest, 1500);
mqttConnectionTest();
}
var constructor = (function (that) {
that.connect.call(that);
})(this);
}
var tester = new MQTT_Tester();
/**
* Function call on script exit
*/
function onExit() {
TuyaDevice.disconnectAll();
if (tester) tester.destroy();
};
#!/usr/bin/env node
const fs = require('fs')
const mqtt = require('mqtt')
const json5 = require('json5')
const debug = require('debug')('tuya-mqtt:info')
const debugCommand = require('debug')('tuya-mqtt:command')
const debugError = require('debug')('tuya-mqtt:error')
const SimpleSwitch = require('./devices/simple-switch')
const SimpleDimmer = require('./devices/simple-dimmer')
const RGBTWLight = require('./devices/rgbtw-light')
const GenericDevice = require('./devices/generic-device')
const utils = require('./lib/utils')
var CONFIG = undefined
var tuyaDevices = new Array()
// Setup Exit Handlers
process.on('exit', processExit.bind(0))
process.on('SIGINT', processExit.bind(0))
process.on('SIGTERM', processExit.bind(0))
process.on('uncaughtException', processExit.bind(1))
// Disconnect from and publish offline status for all devices on exit
async function processExit(exitCode) {
for (let tuyaDevice of tuyaDevices) {
tuyaDevice.device.disconnect()
}
if (exitCode || exitCode === 0) debug('Exit code: '+exitCode)
await utils.sleep(1)
process.exit()
}
// Get new deivce based on configured type
function getDevice(configDevice, mqttClient) {
const deviceInfo = {
configDevice: configDevice,
mqttClient: mqttClient,
topic: CONFIG.topic
}
switch (configDevice.type) {
case 'SimpleSwitch':
return new SimpleSwitch(deviceInfo)
break;
case 'SimpleDimmer':
return new SimpleDimmer(deviceInfo)
break;
case 'RGBTWLight':
return new RGBTWLight(deviceInfo)
break;
}
return new GenericDevice(deviceInfo)
}
function initDevices(configDevices, mqttClient) {
for (let configDevice of configDevices) {
const newDevice = getDevice(configDevice, mqttClient)
tuyaDevices.push(newDevice)
}
}
// Republish devices 2x with 30 seconds sleep if restart of HA is detected
async function republishDevices() {
for (let i = 0; i < 2; i++) {
debug('Resending device config/state in 30 seconds')
await utils.sleep(30)
for (let device of tuyaDevices) {
device.republish()
}
await utils.sleep(2)
}
}
// Main code function
const main = async() => {
let configDevices
let mqttClient
try {
CONFIG = require('./config')
} catch (e) {
console.error('Configuration file not found!')
debugError(e)
process.exit(1)
}
if (typeof CONFIG.qos == 'undefined') {
CONFIG.qos = 1
}
if (typeof CONFIG.retain == 'undefined') {
CONFIG.retain = false
}
try {
configDevices = fs.readFileSync('./devices.conf', 'utf8')
configDevices = json5.parse(configDevices)
} catch (e) {
console.error('Devices file not found!')
debugError(e)
process.exit(1)
}
if (!configDevices.length) {
console.error('No devices found in devices file!')
process.exit(1)
}
mqttClient = mqtt.connect({
host: CONFIG.host,
port: CONFIG.port,
username: CONFIG.mqtt_user,
password: CONFIG.mqtt_pass,
})
mqttClient.on('connect', function (err) {
debug('Connection established to MQTT server')
let topic = CONFIG.topic + '#'
mqttClient.subscribe(topic)
mqttClient.subscribe('homeassistant/status')
mqttClient.subscribe('hass/status')
initDevices(configDevices, mqttClient)
})
mqttClient.on('reconnect', function (error) {
if (mqttClient.connected) {
debug('Connection to MQTT server lost. Attempting to reconnect...')
} else {
debug('Unable to connect to MQTT server')
}
})
mqttClient.on('error', function (error) {
debug('Unable to connect to MQTT server', error)
})
mqttClient.on('message', function (topic, message) {
try {
message = message.toString()
const splitTopic = topic.split('/')
const topicLength = splitTopic.length
const commandTopic = splitTopic[topicLength - 1]
const deviceTopicLevel = splitTopic[1]
if (topic === 'homeassistant/status' || topic === 'hass/status' ) {
debug('Home Assistant state topic '+topic+' received message: '+message)
if (message === 'online') {
republishDevices()
}
} else if (commandTopic.includes('command')) {
// If it looks like a valid command topic try to process it
debugCommand('Received MQTT message -> ', JSON.stringify({
topic: topic,
message: message
}))
// Use device topic level to find matching device
const device = tuyaDevices.find(d => d.options.name === deviceTopicLevel || d.options.id === deviceTopicLevel)
switch (topicLength) {
case 3:
device.processCommand(message, commandTopic)
break;
case 4:
device.processDpsCommand(message)
break;
case 5:
const dpsKey = splitTopic[topicLength-2]
device.processDpsKeyCommand(message, dpsKey)
break;
case 6:
const subDevDpsKey = splitTopic[topicLength-2]
const cidName = splitTopic[topicLength-4]
device.processDpsKeyWcidNameCommand(message, subDevDpsKey, cidName)
break;
}
}
} catch (e) {
debugError(e)
}
})
}
// Call the main code
main()