├── README.md ├── ArduinoTuya.h ├── library.properties ├── library.json ├── LICENSE ├── examples └── ESP8266TuyaPlugToggle │ └── ESP8266TuyaPlugToggle.ino └── src ├── ArduinoTuya.h └── ArduinoTuya.cpp /README.md: -------------------------------------------------------------------------------- 1 | # ArduinoTuya 2 | This library allows direct control of Tuya smart plugs from embedded/Arduino devices 3 | -------------------------------------------------------------------------------- /ArduinoTuya.h: -------------------------------------------------------------------------------- 1 | // ArduinoTuya 2 | // Copyright Alex Cortelyou 2018 3 | // MIT License 4 | 5 | #include "src/ArduinoTuya.h" 6 | -------------------------------------------------------------------------------- /library.properties: -------------------------------------------------------------------------------- 1 | name=ArduinoTuya 2 | version=1.0.1 3 | author=Alex Cortelyou 4 | maintainer=Alex Cortelyou 5 | sentence=Allows direct control of Tuya smart plugs from embedded/Arduino devices. 6 | paragraph= 7 | category=Device Control 8 | url=https://github.com/acortelyou/ArduinoTuya 9 | architectures=* 10 | repository=https://github.com/acortelyou/ArduinoTuya.git 11 | license=MIT 12 | depends=tiny-AES-c,ArduinoJson 13 | -------------------------------------------------------------------------------- /library.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ArduinoTuya", 3 | "keywords": "tuya, iot, smarthome, plug", 4 | "description": "Allows direct control of Tuya smart plugs from embedded/Arduino devices", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/acortelyou/ArduinoTuya.git" 8 | }, 9 | "version": "1.0.1", 10 | "authors": { 11 | "name": "Alex Cortelyou" 12 | }, 13 | "frameworks": "arduino", 14 | "platforms": "*", 15 | "dependencies": [ 16 | { 17 | "name": "tiny-AES-c", 18 | "owner": "kokke" 19 | }, 20 | { 21 | "name": "ArduinoJson", 22 | "owner": "bblanchon" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alex Cortelyou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/ESP8266TuyaPlugToggle/ESP8266TuyaPlugToggle.ino: -------------------------------------------------------------------------------- 1 | #include 2 | ESP8266WiFiMulti wiFiMulti; 3 | 4 | #include 5 | TuyaDevice plug("01200885ecfabc87b0f9", "fd351bcdb819492f", "192.168.1.159"); 6 | 7 | void setup() { 8 | 9 | // Serial 10 | Serial.begin(115200); 11 | delay(1000); 12 | Serial.println(); 13 | Serial.println(); 14 | 15 | // WiFi 16 | WiFi.mode(WIFI_STA); 17 | wiFiMulti.addAP("WIFI", "thepasswordispassword"); 18 | Serial.print("Waiting for connection..."); 19 | while (wiFiMulti.run() != WL_CONNECTED) { 20 | Serial.print("."); 21 | delay(1000); 22 | } 23 | Serial.println(" ready."); 24 | delay(1000); 25 | 26 | // Set plug state 27 | Serial.println(); 28 | plug.set(TUYA_OFF); 29 | Serial.print("SET Plug: "); 30 | printPlugState(); 31 | delay(5000); 32 | 33 | } 34 | 35 | void loop() { 36 | 37 | // Get plug state 38 | Serial.println(); 39 | plug.get(); 40 | Serial.print("GET Plug: "); 41 | printPlugState(); 42 | delay(5000); 43 | 44 | // Toggle plug state 45 | Serial.println(); 46 | plug.toggle(); 47 | Serial.print("SET Plug: "); 48 | printPlugState(); 49 | delay(5000); 50 | 51 | } 52 | 53 | 54 | void printPlugState() { 55 | auto error = plug.error(); 56 | if (!error) { 57 | Serial.print(plug.state() ? "ON" : "OFF"); 58 | Serial.println(" (OK)"); 59 | } else { 60 | Serial.print("ERROR "); 61 | Serial.println(error); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/ArduinoTuya.h: -------------------------------------------------------------------------------- 1 | // ArduinoTuya 2 | // Copyright Alex Cortelyou 2018 3 | // MIT License 4 | 5 | #ifndef TUYA_H 6 | #define TUYA_H 7 | 8 | #ifndef TUYA_PORT_DEFAULT 9 | #define TUYA_PORT_DEFAULT 6668 10 | #endif 11 | 12 | #ifndef TUYA_VERSION_DEFAULT 13 | #define TUYA_VERSION_DEFAULT "3.1" 14 | #endif 15 | 16 | #ifndef TUYA_TIMEOUT 17 | #define TUYA_TIMEOUT 10000 18 | #endif 19 | 20 | #ifndef TUYA_RETRY_DELAY 21 | #define TUYA_RETRY_DELAY 5000 22 | #endif 23 | 24 | #ifndef TUYA_RETRY_COUNT 25 | #define TUYA_RETRY_COUNT 4 26 | #endif 27 | 28 | #ifdef TUYA_DEBUG 29 | #define DEBUG_PRINT(x) Serial.print (x) 30 | #define DEBUG_PRINTDEC(x) Serial.print (x, DEC) 31 | #define DEBUG_PRINTHEX(x) Serial.print (x, HEX) 32 | #define DEBUG_PRINTLN(x) Serial.println (x) 33 | #define DEBUG_PRINTJSON(x) serializeJson(doc, Serial);Serial.println() 34 | #else 35 | #define DEBUG_PRINT(x) 36 | #define DEBUG_PRINTDEC(x) 37 | #define DEBUG_PRINTHEX(x) 38 | #define DEBUG_PRINTLN(x) 39 | #define DEBUG_PRINTJSON(x) 40 | #endif 41 | 42 | #define TUYA_BLOCK_LENGTH 16 43 | #define TUYA_PREFIX_LENGTH 16 44 | #define TUYA_SUFFIX_LENGTH 8 45 | 46 | #define ECB 1 47 | #define CBC 0 48 | #define CTR 0 49 | 50 | #ifndef ARDUINO 51 | #include 52 | #elif ARDUINO >= 100 53 | #include "Arduino.h" 54 | #include "Print.h" 55 | #else 56 | #include "WProgram.h" 57 | #endif 58 | 59 | #include 60 | #include 61 | #include 62 | #include 63 | #include 64 | 65 | typedef enum 66 | { 67 | TUYA_OFF = (0), 68 | TUYA_ON = (1) 69 | } tuya_state_t; 70 | 71 | typedef enum 72 | { 73 | TUYA_OK = (0), 74 | TUYA_ERROR_UNINIT = (1), 75 | TUYA_ERROR_SOCKET = (2), 76 | TUYA_ERROR_PREFIX = (3), 77 | TUYA_ERROR_LENGTH = (4), 78 | TUYA_ERROR_SUFFIX = (5), 79 | TUYA_ERROR_PARSE = (6), 80 | TUYA_ERROR_ARGS = (7) 81 | } tuya_error_t; 82 | 83 | class TuyaDevice { 84 | 85 | protected: 86 | 87 | AES_ctx _aes; 88 | MD5Builder _md5; 89 | WiFiClient _client; 90 | 91 | const byte prefix[16] = { 0, 0, 85, 170, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; 92 | const byte suffix[8] = { 0, 0, 0, 0, 0, 0, 170, 85 }; 93 | 94 | String _id; 95 | String _key; 96 | const char* _host; 97 | uint16_t _port; 98 | String _version; 99 | 100 | tuya_state_t _state = TUYA_OFF; 101 | tuya_error_t _error = TUYA_ERROR_UNINIT; 102 | 103 | void initGetRequest(JsonDocument &jsonRequest); 104 | void initSetRequest(JsonDocument &jsonRequest); 105 | String createPayload(JsonDocument &jsonRequest, bool encrypt = true); 106 | String sendCommand(String &payload, byte command); 107 | 108 | public: 109 | 110 | inline TuyaDevice(const char* id, const char* key, const char* host = NULL, uint16_t port = TUYA_PORT_DEFAULT, const char* version = TUYA_VERSION_DEFAULT) { 111 | _id = String(id); 112 | _key = String(key); 113 | _host = host; 114 | _port = port; 115 | _version = String(version); 116 | AES_init_ctx(&_aes, (uint8_t*) key); 117 | } 118 | 119 | inline tuya_state_t state() { return _state; } 120 | inline tuya_error_t error() { return _error; } 121 | 122 | tuya_error_t get(); 123 | tuya_error_t set(bool state); 124 | tuya_error_t toggle(); 125 | 126 | }; 127 | 128 | class TuyaPlug : public TuyaDevice { 129 | 130 | public: 131 | 132 | TuyaPlug(const char* id, const char* key, const char* host = NULL, uint16_t port = TUYA_PORT_DEFAULT, const char* version = TUYA_VERSION_DEFAULT) : TuyaDevice(id,key,host,port,version) {} 133 | 134 | }; 135 | 136 | class TuyaBulb : public TuyaDevice { 137 | 138 | public: 139 | 140 | TuyaBulb(const char* id, const char* key, const char* host = NULL, uint16_t port = TUYA_PORT_DEFAULT, const char* version = TUYA_VERSION_DEFAULT) : TuyaDevice(id,key,host,port,version) {} 141 | 142 | tuya_error_t setColorRGB(byte r, byte g, byte b); 143 | tuya_error_t setColorHSV(byte h, byte s, byte v); 144 | tuya_error_t setWhite(byte brightness, byte temp); 145 | 146 | private: 147 | 148 | inline byte asByte(float n) { return n <= 0.0 ? 0 : floor(n >= 1.0 ? 255 : n * 256.f); } 149 | inline float asFloat(byte n) { return n * (1.f/255.f); } 150 | inline float step(float e, float x) { return x < e ? 0.0 : 1.0; } 151 | inline float mix(float a, float b, float t) { return a + (b - a) * t; } 152 | 153 | }; 154 | 155 | #endif 156 | -------------------------------------------------------------------------------- /src/ArduinoTuya.cpp: -------------------------------------------------------------------------------- 1 | // ArduinoTuya 2 | // Copyright Alex Cortelyou 2018 3 | // MIT License 4 | 5 | #include "ArduinoTuya.h" 6 | 7 | void TuyaDevice::initGetRequest(JsonDocument &jsonRequest) { 8 | jsonRequest["gwId"] = _id; //device id 9 | jsonRequest["devId"] = _id; //device id 10 | } 11 | 12 | void TuyaDevice::initSetRequest(JsonDocument &jsonRequest) { 13 | jsonRequest["t"] = 0; //epoch time (required but value doesn't appear to be used) 14 | jsonRequest["devId"] = _id; //device id 15 | jsonRequest.createNestedObject("dps"); 16 | jsonRequest["uid"] = ""; //user id (required but value doesn't appear to be used) 17 | } 18 | 19 | String TuyaDevice::createPayload(JsonDocument &jsonRequest, bool encrypt) { 20 | 21 | // Serialize json request 22 | String jsonString; 23 | serializeJson(jsonRequest, jsonString); 24 | 25 | DEBUG_PRINT("REQUEST "); 26 | DEBUG_PRINTLN(jsonString); 27 | 28 | if (!encrypt) return jsonString; 29 | 30 | // Determine lengths and padding 31 | const int jsonLength = jsonString.length(); 32 | const int cipherPadding = TUYA_BLOCK_LENGTH - jsonLength % TUYA_BLOCK_LENGTH; 33 | const int cipherLength = jsonLength + cipherPadding; 34 | 35 | // Allocate encrypted data buffer 36 | byte cipherData[cipherLength]; 37 | 38 | // Use PKCS7 padding mode 39 | memcpy(cipherData, jsonString.c_str(), jsonLength); 40 | memset(&cipherData[jsonLength], cipherPadding, cipherPadding); 41 | 42 | // AES ECB encrypt each block 43 | for (int i = 0; i < cipherLength; i += TUYA_BLOCK_LENGTH) { 44 | AES_ECB_encrypt(&_aes, &cipherData[i]); 45 | } 46 | 47 | // Base64 encode encrypted data 48 | String base64Data = base64::encode(cipherData, cipherLength, false); 49 | 50 | // Calculate MD5 hash signature 51 | _md5.begin(); 52 | _md5.add("data="); 53 | _md5.add(base64Data); 54 | _md5.add("||lpv="); 55 | _md5.add(_version); 56 | _md5.add("||"); 57 | _md5.add(_key); 58 | _md5.calculate(); 59 | String md5 = _md5.toString().substring(8, 24); 60 | 61 | // Create signed payload 62 | String payload = String(_version + md5 + base64Data); 63 | 64 | DEBUG_PRINT("PAYLOAD "); 65 | DEBUG_PRINTLN(payload); 66 | 67 | return payload; 68 | } 69 | 70 | String TuyaDevice::sendCommand(String &payload, byte command) { 71 | 72 | // Attempt to send command at least once 73 | int tries = 0; 74 | while (tries++ <= TUYA_RETRY_COUNT) { 75 | 76 | // Determine lengths and offsets 77 | const int payloadLength = payload.length(); 78 | const int bodyOffset = TUYA_PREFIX_LENGTH; 79 | const int bodyLength = payloadLength + TUYA_SUFFIX_LENGTH; 80 | const int suffixOffset = TUYA_PREFIX_LENGTH + payloadLength; 81 | const int requestLength = TUYA_PREFIX_LENGTH + payloadLength + TUYA_SUFFIX_LENGTH; 82 | 83 | // Assemble request buffer 84 | byte request[requestLength]; 85 | memcpy(request, prefix, 11); 86 | request[11] = command; 87 | request[12] = (byte) ((bodyLength>>24) & 0xFF); 88 | request[13] = (byte) ((bodyLength>>16) & 0xFF); 89 | request[14] = (byte) ((bodyLength>> 8) & 0xFF); 90 | request[15] = (byte) ((bodyLength>> 0) & 0xFF); 91 | memcpy(&request[bodyOffset], payload.c_str(), payloadLength); 92 | memcpy(&request[suffixOffset], suffix, TUYA_SUFFIX_LENGTH); 93 | 94 | // Connect to device 95 | _client.setTimeout(TUYA_TIMEOUT); 96 | if (!_client.connect(_host, _port)) { 97 | DEBUG_PRINTLN("TUYA SOCKET ERROR"); 98 | _error = TUYA_ERROR_SOCKET; 99 | delay(TUYA_RETRY_DELAY); 100 | continue; 101 | } 102 | 103 | // Wait for socket to be ready for write 104 | while (_client.connected() && _client.availableForWrite() < requestLength) delay(10); 105 | 106 | // Write request to device 107 | _client.write(request, requestLength); 108 | 109 | // Wait for socket to be ready for read 110 | while (_client.connected() && _client.available() < 11) delay(10); 111 | 112 | // Read response prefix (bytes 1 to 11) 113 | byte buffer[11]; 114 | _client.read(buffer, 11); 115 | 116 | // Check prefix match 117 | if (memcmp(prefix, buffer, 11) != 0) { 118 | DEBUG_PRINTLN("TUYA PREFIX MISMATCH"); 119 | _error = TUYA_ERROR_PREFIX; 120 | _client.stop(); 121 | delay(TUYA_RETRY_DELAY); 122 | continue; 123 | } 124 | 125 | // Read response command (byte 12) (ignored) 126 | _client.read(buffer, 1); 127 | 128 | // Read response length (bytes 13 to 16) 129 | _client.read(buffer, 4); 130 | 131 | // Assemble big-endian response length 132 | size_t length = (buffer[0]<<24)|(buffer[1]<<16)|(buffer[2]<<8)|(buffer[3])-12; 133 | 134 | // Read response unknown (bytes 17 to 20) (ignored) 135 | _client.read(buffer, 4); 136 | 137 | // Allocate response buffer 138 | byte response[length+1]; 139 | memset(response, 0, length+1); 140 | 141 | // Read response (bytes 21 to N-8) 142 | _client.read(response, length); 143 | 144 | // Read response suffix (bytes N-7 to N) 145 | _client.read(buffer, 8); 146 | 147 | // Check last four bytes of suffix match 148 | if (memcmp(&suffix[4], &buffer[4], 4) != 0) { 149 | DEBUG_PRINTLN("TUYA SUFFIX MISMATCH"); 150 | _error = TUYA_ERROR_SUFFIX; 151 | _client.stop(); 152 | delay(TUYA_RETRY_DELAY); 153 | continue; 154 | } 155 | 156 | // Check length match 157 | if (_client.available() > 0) { 158 | DEBUG_PRINTLN("TUYA LENGTH MISMATCH"); 159 | _error = TUYA_ERROR_LENGTH; 160 | _client.stop(); 161 | delay(TUYA_RETRY_DELAY); 162 | continue; 163 | } 164 | 165 | // Close connection 166 | _client.stop(); 167 | 168 | if (length > 0) { 169 | DEBUG_PRINT("RESPONSE "); 170 | DEBUG_PRINTLN((const char*)response); 171 | } 172 | 173 | _error = TUYA_OK; 174 | return String((const char*)response); 175 | } 176 | 177 | return String(""); 178 | } 179 | 180 | tuya_error_t TuyaDevice::get() { 181 | 182 | // Allocate json objects 183 | StaticJsonDocument<512> jsonRequest; 184 | StaticJsonDocument<512> jsonResponse; 185 | 186 | // Build request 187 | initGetRequest(jsonRequest); 188 | 189 | String payload = createPayload(jsonRequest, false); 190 | 191 | String response = sendCommand(payload, 10); 192 | 193 | // Check for errors 194 | if (_error != TUYA_OK) return _error; 195 | 196 | // Deserialize json response 197 | auto error = deserializeJson(jsonResponse, response); 198 | if (error) return _error = TUYA_ERROR_PARSE; 199 | 200 | // Check response 201 | JsonVariant state = jsonResponse["dps"]["1"]; 202 | if (state.isNull()) return _error = TUYA_ERROR_PARSE; 203 | 204 | _state = state.as() ? TUYA_ON : TUYA_OFF; 205 | return _error = TUYA_OK; 206 | } 207 | 208 | tuya_error_t TuyaDevice::set(bool state) { 209 | 210 | // Allocate json object 211 | StaticJsonDocument<512> jsonRequest; 212 | 213 | // Build request 214 | initSetRequest(jsonRequest); 215 | jsonRequest["dps"]["1"] = state; //state 216 | jsonRequest["dps"]["2"] = 0; //delay 217 | 218 | String payload = createPayload(jsonRequest); 219 | 220 | String response = sendCommand(payload, 7); 221 | 222 | // Check for errors 223 | if (_error != TUYA_OK) return _error; 224 | if (response.length() != 0) return _error = TUYA_ERROR_LENGTH; 225 | 226 | _state = state ? TUYA_ON : TUYA_OFF; 227 | 228 | return _error = TUYA_OK; 229 | } 230 | 231 | tuya_error_t TuyaDevice::toggle() { 232 | return set(!_state); 233 | } 234 | 235 | tuya_error_t TuyaBulb::setColorRGB(byte r, byte g, byte b) { 236 | //https://gist.github.com/postspectacular/2a4a8db092011c6743a7 237 | float R = asFloat(r); 238 | float G = asFloat(g); 239 | float B = asFloat(b); 240 | float s = step(B, G); 241 | float px = mix(B, G, s); 242 | float py = mix(G, B, s); 243 | float pz = mix(-1.0, 0.0, s); 244 | float pw = mix(0.6666666, -0.3333333, s); 245 | s = step(px, R); 246 | float qx = mix(px, R, s); 247 | float qz = mix(pw, pz, s); 248 | float qw = mix(R, px, s); 249 | float d = qx - min(qw, py); 250 | float H = abs(qz + (qw - py) / (6.0 * d + 1e-10)); 251 | float S = d / (qx + 1e-10); 252 | float V = qx; 253 | 254 | return setColorHSV(asByte(H), asByte(S), asByte(V)); 255 | } 256 | 257 | tuya_error_t TuyaBulb::setColorHSV(byte h, byte s, byte v) { 258 | 259 | // Format color as hex string 260 | char hexColor[7]; 261 | sprintf(hexColor, "%02x%02x%02x", h, s, v); 262 | 263 | // Allocate json object 264 | StaticJsonDocument<512> jsonRequest; 265 | 266 | // Build request 267 | initSetRequest(jsonRequest); 268 | jsonRequest["dps"]["5"] = hexColor; 269 | jsonRequest["dps"]["2"] = "colour"; 270 | 271 | String payload = createPayload(jsonRequest); 272 | 273 | String response = sendCommand(payload, 7); 274 | 275 | return _error; 276 | } 277 | 278 | tuya_error_t TuyaBulb::setWhite(byte brightness, byte temp) { 279 | 280 | if (brightness < 25 || brightness > 255) { 281 | DEBUG_PRINTLN("BRIGHTNESS MUST BE BETWEEN 25 AND 255"); 282 | return _error = TUYA_ERROR_ARGS; 283 | } 284 | 285 | // Allocate json object 286 | StaticJsonDocument<512> jsonRequest; 287 | 288 | // Build request 289 | initSetRequest(jsonRequest); 290 | jsonRequest["dps"]["2"] = "white"; 291 | jsonRequest["dps"]["3"] = brightness; 292 | jsonRequest["dps"]["4"] = temp; 293 | 294 | String payload = createPayload(jsonRequest); 295 | 296 | String response = sendCommand(payload, 7); 297 | 298 | return _error; 299 | } 300 | --------------------------------------------------------------------------------