p-a-h-a
Member
Приветствую, возникла необходимость управлять розеткой Tuya по событию. Подобного не нашел.
Реализовано только для розетки только внутри wi-fi сети с фиксированным ip розетки.
Буду рад если кто-то расширит функционал, например расшифрует ответ, научится запрашивать статус, добавит новые устройств. Мне сейчас достаточно сделанного. LOCAL_KEY и devId брал на оф. сайте разработчиков туя, в ютубе смотрел инструкцию.
main.cpp:
Tuya.h:
platformio.ini:
Разбирался как все устроенно при помощи пайтон скрипта, который по сути делает тоже самое (вкл/откл) с подробным логом.
На всякий случай tuya.py:
Реализовано только для розетки только внутри wi-fi сети с фиксированным ip розетки.
Буду рад если кто-то расширит функционал, например расшифрует ответ, научится запрашивать статус, добавит новые устройств. Мне сейчас достаточно сделанного. LOCAL_KEY и devId брал на оф. сайте разработчиков туя, в ютубе смотрел инструкцию.
main.cpp:
C++:
// Код подключается к умной розетке Tuya local по протоколу 3.3
#include <Arduino.h>
#include <WiFi.h>
#include "Tuya.h"
const char *ssid = "xxx"; // Имя вашей Wi-Fi сети
const char *password = "xxx"; // Пароль от вашей Wi-Fi сети
const char *devId = "xxx"; // Id розетки. В роутере нужно прописать постоянный IP для розетки
const char *LOCAL_KEY = "xxxxxxxxxx"; // Ваш ключ AES (16 байтов) Ищите на ютубе как его получить на сайте разработчика
const char *tuya_ip = "xxx.xxx.xxx.xxx"; // IP розетки. В роутере нужно прописать постоянный IP для розетки
Tuya Rozetka(tuya_ip, devId, LOCAL_KEY);
void setup(){
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED)
configTime(0, 0, "pool.ntp.org", "time.nist.gov");
}
void loop(){
Rozetka.on();
delay(3000);
Rozetka.off();
delay(3000);
// static byte i;
// Rozetka.relay(++i % 2);
// delay(3000);
}
C++:
#pragma once
#include <Arduino.h>
#include <mbedtls/aes.h>
#include <WiFi.h>
#include <lwip/inet.h>
#include <time.h>
#define headerSize 31 // Фиксированный размер заголовка
#define TailSize 8 // Фиксированный размер хвоста (crc32 и фиксированный код 0xAA 0x55)
class Tuya
{
private:
const char *devId;
const char *LOCAL_KEY; // Ваш ключ AES (16 байтов)
const char *tuya_ip;
WiFiClient client;
void payloadConfigure(char *input_string, const uint8_t len, const char *devId, const char *dpsValue);//Создает json запрос с командой
void aesEncrypt(uint8_t *output_data, size_t output_data_len, const char *input_string, size_t input_string_len, const char *LOCAL_KEY);//Кодирует запрос
uint32_t crc32(const uint8_t *data, size_t length); // CRC32
bool sendCommand(uint8_t *payload, size_t size, const char *tuya_ip); // передает данные
public:
Tuya() = delete;
Tuya(const char *tuya_ip, const char *devId, const char *LOCAL_KEY)
: tuya_ip(tuya_ip), devId(devId), LOCAL_KEY(LOCAL_KEY) {}
bool relay(bool state);
bool on() { return relay(LOW); }
bool off() { return relay(HIGH); }
};
bool Tuya::relay(bool state)
{
const size_t input_string_len = 97; // Длинна сообщения фиксирована для вкл/откл розетки
char input_string[input_string_len] = {};
const char *dpsValue = nullptr; // команда включения отключения в запросе (true/false)
state ? dpsValue = "true" : dpsValue = "false";
payloadConfigure(input_string, input_string_len, devId, dpsValue);
const int padded_len = ((input_string_len / 16) + 1) * 16;
constexpr size_t output_data_len = headerSize + padded_len + TailSize;
uint8_t output_data[output_data_len] = {};
aesEncrypt(output_data, output_data_len, input_string, input_string_len, LOCAL_KEY);
return sendCommand(output_data, output_data_len, tuya_ip); // Отправка данных
}
void Tuya::aesEncrypt(uint8_t *output_data, size_t output_data_len, const char *input_string, size_t input_string_len, const char *LOCAL_KEY)
{
{
//Заголовок
const uint8_t header[] = {
0x00, 0x00, 0x55, 0xAA,//Фиксированный заголовок
0x01, 0x00, 0x00, 0x00, //счетчик, Идентификатор сообщения. Такой же возвращается в ответ.
0x00, 0x00, 0x00, 0x07, //Команда 0x00..0x0E
0x00, 0x00, 0x00, 0x00, // Длина зашифрованных данных плюс 8 байт (CRC32 и суффикс)
0x33, 0x2E, 0x33, 0x00, //Версия 3.3
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 //ноли
};
memcpy(output_data, header, sizeof(header)); // Копируем заголовок в массив байтов
}
uint32_t *p_comandCounter = (uint32_t *)(output_data + 4); // Создаем ссылку на адресс масива где хранится номер команды
static uint32_t counter = 0;
*p_comandCounter = htonl(counter++);
// на потом, команды сообщения в 11м байте
// enum Comands {
// HEART_BEAT = 0x00, //Пинг / проверка связи
// PRODUCT_INFO = 0x01, //Информация об устройстве
// WORK_MODE = 0x02, //Рабочий режим
// WIFI_STATE = 0x03, //Состояние Wi-Fi
// WIFI_RESET = 0x04, //Сброс Wi-Fi
// WIFI_MODE = 0x05, //Режим Wi-Fi
// DATA = 0x06, //Передача команд управления (вкл/выкл и др)
// STATE = 0x07, //Получение состояния устройства
// STATE_RESPONSE = 0x08,//Ответ на запрос состояния
// DP_QUERY = 0x09, //Запрос значения DP (data point)
// DP_QUERY_RESPONSE = 0x0A,//Ответ на DP_QUERY
// CONTROL = 0x0B, //Управление устройством (часто тоже 0x07)
// STATUS_REPORT = 0x0C, //Отчет об изменении состояния
// STATUS_REPORT_RESPONSE = 0x0D,//Ответ на отчет
// RESET_FACTORY = 0x0E //Сброс на заводские
// };
// output_data[11] = DATA;
const size_t padded_len = ((input_string_len / 16) + 1) * 16; // Делаем длину строки кратной 16
uint8_t* input_data = new uint8_t[padded_len]{};
memcpy(input_data, input_string, input_string_len); // Копируем строку в массив байтов
// Добавляем padding (PKCS7)
const size_t block_size = 16;
size_t pad_len = block_size - (input_string_len % block_size);
for (size_t i = input_string_len; i < input_string_len + pad_len; i++)
{
input_data[i] = pad_len; // Заполнение байтами с значением pad_len
}
uint32_t *total_payload_len = (uint32_t *)(output_data + 12); // Создаем ссылку на адресс масива где хранится длина сообщения
*total_payload_len = htonl(padded_len + headerSize - (const int)8); // Расчет длинны сообщения, перевод байт в big-endian и сразу запись в массив
// Инициализация AES контекста
mbedtls_aes_context aes_ctx;
mbedtls_aes_init(&aes_ctx);
// Инициализация шифратора
mbedtls_aes_setkey_enc(&aes_ctx, (const unsigned char *)LOCAL_KEY, 128);
for (size_t i = 0; i < padded_len; i += 16)
{
mbedtls_aes_crypt_ecb(&aes_ctx, MBEDTLS_AES_ENCRYPT, input_data + i, output_data + i + headerSize);
}
mbedtls_aes_free(&aes_ctx); // Очистка контекста
delete[] input_data;
uint32_t crc = htonl(crc32(output_data, padded_len + headerSize));
memcpy(&output_data[padded_len + headerSize], &crc, sizeof(crc)); // Пишем контрольную сумму в хвост
memcpy(&output_data[padded_len + headerSize + TailSize - 2], "\xAA\x55", 2); // фиксированный хвост
}
void Tuya::payloadConfigure(char *input_string, const uint8_t len, const char *devId, const char *dpsValue)
{
struct tm timeinfo;
getLocalTime(&timeinfo); // Получаем системное время
sprintf(input_string, "{\"devId\":\"%s\",\"uid\":\"%s\",\"t\":\"%lu\",\"dps\":{\"1\":%s}}", devId, devId, mktime(&timeinfo), dpsValue);
}
uint32_t Tuya::crc32(const uint8_t *data, size_t length)
{
uint32_t crc = 0xFFFFFFFF;
for (size_t i = 0; i < length; i++)
{
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++)
{
if (crc & 1)
crc = (crc >> 1) ^ 0xEDB88320;
else
crc >>= 1;
}
}
return ~crc;
}
bool Tuya::sendCommand(uint8_t *payload, size_t size, const char *tuya_ip)
{
if (client.connect(tuya_ip, 6668))
{
delay(50);
Serial.printf("Соединение с Tuya успешно!\n");
client.write(payload, size);
client.flush();
// Чтение ответа
delay(200);
uint32_t start = millis();
while (millis() - start < 700) // ждём до 1 сек
{
if (client.available())
{
uint8_t b = client.read();
Serial.printf("%02X ", b);
start = millis(); // сброс таймера, если получили байт
}
else
delay(10); // подождать немного, если данных нет
}
client.stop();
Serial.println("\nСоединение закрыто.");
return true;
}
else
{
Serial.println("Ошибка подключения к розетке!");
return false;
}
}
Код:
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
board_build.filesystem = littlefs
build_unflags = -std=gnu++11
board_upload.wait_for_upload_port = yes
board_upload.use_1200bps_touch = yes
lib_deps =
На всякий случай tuya.py:
Python:
import logging
import tinytuya
import time
#скрипт выводит в консоль зашифрованные пакеты для вкл и выкл
# Данные устройства
DEVICE_ID = "xxxxxxxxxxxxxxx"
DEVICE_IP = "xxx.xxx.xxx.xxx"
LOCAL_KEY = "xxxxxxxxxxxxxxx"
# Настройка логирования
logging.basicConfig(level=logging.DEBUG)
# Инициализация устройства
device = tinytuya.OutletDevice(DEVICE_ID, DEVICE_IP, LOCAL_KEY)
device.set_version(3.3) # важно для новых устройств
# Проверка статуса
status = device.status()
print("Текущий статус:", status)
# Включение розетки
print("Включаем розетку...")
#device.set_status(True, 1) # True = ВКЛ, False = ВЫКЛ, 1 = DP ID (обычно для розеток)
#time.sleep(3)
device.turn_on()
# Выключение розетки
print("Выключаем розетку...")
device.set_status(False, 1)