Управление подсветкой Xiaomi Gateway (без node-red)

После того как удалось начать управлять подсветкой Xiaomi Gateway, находящегося в другом сетевом сегменте, о чем я писал тут и тут, логичным было бы продолжить и избавиться от node-red, как от необязательной “прокладки”. Благо уже есть esp8266, читающая сетевые сообщения от шлюза и даже запрашивающая статус. Значит осталось дело за малым – научить ее саму отправлять команды на включение и выключение подсветки шлюза, при получении соответствующей команды через MQTT топик.

Сказать это оказалось чуть легче чем сделать, но тем не менее сегодня данная задача была успешно решена и charon project стал чуть более полезен. Теперь он получает значение CO2 от сенсора SenseAir S8-0053, ловит мультикаст пакеты от шлюза, вытаскивая из них сессионный токен и текущее состояние устройства, пишет это в MQTT брокер, читает MQTT топики в ожидании команды на включение или выключение света и соответсвенно отправляет нужную команду на шлюз. Самым сложным было разобраться с использованием библиотеки для работы с AES-CBC (напомню, что она используется для шифрования ключа и сессионного токена шлюза при отправке на него управляющей команды).

Используется библиотека для работы с AES-CBC suculent/AESLib@^2.2.1
Версия кода на момент написания данной статьи (да, кстати, я переехал на PlatformIO):

Из того, что нужно править под себя, кроме кредов на вайфай и MQTT брокер:

  • aes_key – это HEX от вашего ключа от шлюза (после перевода его в режим разработчика)
  • 04cf8cf2ee25 – это SID вашего шлюза, разбросан по коду в нескольких местах
  • 838795264 – это значение обозначает цвет и яркость на которую нужно установить подсветку
  • ну и вырезать всю часть для работы с сенсором CO2
#include <Arduino.h>
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <WiFiUdp.h>
#include <ArduinoJson.h>
#include <SoftwareSerial.h>
#include <ArduinoOTA.h>
#include <AESLib.h>
#include <arduino_secrets.h>
const char* ssid 		  = SECRET_SMARTHOME_WIFI_SSID;
const char* password 		  = SECRET_SMARTHOME_WIFI_PASSWORD;
const char* mqttServer	          = SECRET_MQTT_SERVER;
const int   mqttPort 		  = SECRET_MQTT_PORT;
const char* mqttUser 		  = SECRET_MQTT_USER;
const char* mqttPassword 	  = SECRET_MQTT_PASSWORD;
// ==============================================================================
// AES CBC
// ==============================================================================
AESLib aesLib;
byte aes_iv[16]    = { 0x17, 0x99, 0x6d, 0x09, 0x3d, 0x28, 0xdd, 0xb3, 0xba, 0x69, 0x5a, 0x2e, 0x6f, 0x58, 0x56, 0x2e };
byte aes_key[16]   = { 0x37, 0x37, 0x37, 0x39, 0x33, 0x38, 0x44, 0x39, 0x30, 0x46, 0x37, 0x30, 0x34, 0x45, 0x35, 0x42 };
byte enc_iv_to[16] = {};
byte cleartext[16] = {};
char readbuffer[] = "vcT9bEapirfUZNyq";
unsigned char encoded[sizeof(readbuffer) * 2] = "";
char aes_send[32] = "";
// ==============================================================================
// S8 Init zone
// ==============================================================================
#define D7 (13)
#define D8 (15)
#define CO2_INTERVAL 15000
SoftwareSerial s8Serial(D7, D8);
int s8_co2;
int s8_co2_mean;
int s8_co2_mean2;
float smoothing_factor = 0.5;
float smoothing_factor2 = 0.15;
byte cmd_s8[]       = {0xFE, 0x04, 0x00, 0x03, 0x00, 0x01, 0xD5, 0xC5};
byte abc_s8[]       = {0xFE, 0x03, 0x00, 0x1F, 0x00, 0x01, 0xA1, 0xC3};
byte response_s8[7] = {0, 0, 0, 0, 0, 0, 0};
const int r_len = 7;
const int c_len = 8;
long lastCo2Measured = 0;
// ==============================================================================
// End S8 init zone
// ==============================================================================
unsigned int multicast_port = 9898;
unsigned int unicast_port   = 8989;
long lastReconnectAttempt = 0;
IPAddress multicast_ip_addr = IPAddress(224, 0, 0, 50);
char mPacket[255];
char uPacket[255];
WiFiUDP mUdp;
WiFiUDP uUdp;
WiFiClient espClient;
PubSubClient client(espClient);
boolean mqtt_reconnect() {
Serial.print("Connecting to MQTT...");
if(client.connect("charon", mqttUser, mqttPassword, "esp/status/charon", 2, true, "disconnected")) {
Serial.println("connected");
// Online Message
client.publish("esp/status/charon", "online", true);
client.subscribe("esp/04cf8cf2ee25/CMD");
client.subscribe("esp/04cf8cf2ee25/heartbeat");
} else {
Serial.printf("failed with state: %d\n", client.state());
}
return client.connected();
}
boolean wifi_reconnect() {
Serial.printf("Connecting to %s ", ssid);
WiFi.hostname("charonESP");
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
delay(500);
}
Serial.printf("\nConnected to the WiFi network: %s\n", ssid);
mUdp.beginMulticast(WiFi.localIP(), multicast_ip_addr, multicast_port);
uUdp.begin(unicast_port);
Serial.printf("Now listening at IP %s, multicst UDP port %d, local UDP port: %d\n", WiFi.localIP().toString().c_str(), multicast_port, unicast_port);
ArduinoOTA.setPort(8266);
ArduinoOTA.setHostname("charonESP");
// ArduinoOTA.setPassword("admin");
ArduinoOTA.onStart([]() {
String type;
if (ArduinoOTA.getCommand() == U_FLASH) {
type = "sketch";
} else { // U_FS
type = "filesystem";
}
Serial.println("Start updating " + type);
});
ArduinoOTA.onEnd([]() {
Serial.println("\nEnd");
});
ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
Serial.printf("Progress: %u%%\r", (progress / (total / 100)));
});
ArduinoOTA.onError([](ota_error_t error) {
Serial.printf("Error[%u]: ", error);
if (error == OTA_AUTH_ERROR) {
Serial.println("Auth Failed");
} else if (error == OTA_BEGIN_ERROR) {
Serial.println("Begin Failed");
} else if (error == OTA_CONNECT_ERROR) {
Serial.println("Connect Failed");
} else if (error == OTA_RECEIVE_ERROR) {
Serial.println("Receive Failed");
} else if (error == OTA_END_ERROR) {
Serial.println("End Failed");
}
});
ArduinoOTA.begin();
return true;
}
void encrypt_to_ciphertext(char * msg, uint16_t msgLen, byte iv[]) {
aesLib.encrypt((byte*)msg, msgLen, (char*)encoded, aes_key, sizeof(aes_key), iv);
return;
}
void callback(char* topic, byte* payload, unsigned int length) {
char buff_p[length];
for (size_t i = 0; i < length; i++)
{
buff_p[i] = (char)payload[i];
}
buff_p[length] = '\0';
if (strcmp(topic,"esp/04cf8cf2ee25/CMD")==0){
// Сперва получить ключ, вне зависимости от содержания
memset(aes_send, 0, sizeof(aes_send));
sprintf((char*)cleartext, "%s", readbuffer);
memcpy(enc_iv_to, aes_iv, sizeof(aes_iv));
uint16_t msgLen = sizeof(cleartext);
encrypt_to_ciphertext((char*)cleartext, msgLen, enc_iv_to);
for (size_t i = 0; i < sizeof(enc_iv_to); i++)
{
char ch[sizeof(enc_iv_to[i])] = "";
memset(ch, 0, sizeof(ch));
sprintf(ch, "%02X", enc_iv_to[i]);
strcat (aes_send, ch);
}
// Вариант "в лоб", без использования цикла
//sprintf(aes_send, "%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", enc_iv_to[0], enc_iv_to[1],enc_iv_to[2],enc_iv_to[3],enc_iv_to[4],enc_iv_to[5],enc_iv_to[6], enc_iv_to[7], enc_iv_to[8],enc_iv_to[9],enc_iv_to[10],enc_iv_to[11],enc_iv_to[12],enc_iv_to[13],enc_iv_to[14],enc_iv_to[15]);
if(strcmp(buff_p, "ON")==0) {
char udpPayload[150] = "{\"cmd\":\"write\",\"model\":\"gateway\",\"sid\":\"4cf8cf2ee25\",\"data\":\"{\"key\":\"";
strcat(udpPayload, aes_send); 
strcat(udpPayload, "\",\"rgb\":838795264}\"}");
IPAddress gIP(192, 168, 2, 9);
uUdp.beginPacket(gIP, multicast_port);
uUdp.write(udpPayload);
uUdp.endPacket();
memset(udpPayload, 0, sizeof(udpPayload));
strcat(udpPayload, "{\"cmd\":\"read\",\"sid\":\"4cf8cf2ee25\"}");
uUdp.beginPacket(gIP, multicast_port);
uUdp.write(udpPayload);
uUdp.endPacket();
} else if (strcmp(buff_p, "OFF")==0) {
char udpPayload[150] = "{\"cmd\":\"write\",\"model\":\"gateway\",\"sid\":\"4cf8cf2ee25\",\"data\":\"{\"key\":\"";
strcat(udpPayload, aes_send); 
strcat(udpPayload, "\",\"rgb\":0}\"}");
IPAddress gIP(192, 168, 2, 9);
uUdp.beginPacket(gIP, multicast_port);
uUdp.write(udpPayload);
uUdp.endPacket();
memset(udpPayload, 0, sizeof(udpPayload));
strcat(udpPayload, "{\"cmd\":\"read\",\"sid\":\"4cf8cf2ee25\"}");
uUdp.beginPacket(gIP, multicast_port);
uUdp.write(udpPayload);
uUdp.endPacket();
} else {
Serial.print("UNKNOWN CMD");
Serial.println();
}
}
if (strcmp(topic,"esp/04cf8cf2ee25/heartbeat")==0){
memset(readbuffer, 0, sizeof(readbuffer));
StaticJsonDocument<256> doc;
deserializeJson(doc, payload, length);
strlcpy(readbuffer, doc["token"] | "default", sizeof(readbuffer));
}
}
void s8Request(byte cmd[]) { 
s8Serial.begin(9600);
while(!s8Serial.available()) {
s8Serial.write(cmd, c_len); 
delay(50);
}
int timeout=0;
while(s8Serial.available() < r_len ) {
timeout++;
if(timeout > 10) {
while(s8Serial.available()) {
s8Serial.read(); 
break;
}
} 
delay(50); 
} 
for (int i=0; i < r_len; i++) { 
response_s8[i] = s8Serial.read(); 
}
s8Serial.end();
}    
unsigned long s8Replay(byte rc_data[]) { 
int high = rc_data[3];
int low = rc_data[4];
unsigned long val = high*256 + low;
return val; 
}
boolean co2_measure() {
s8Request(cmd_s8);
s8_co2 = s8Replay(response_s8);
if (!s8_co2_mean) s8_co2_mean = s8_co2;
s8_co2_mean = s8_co2_mean - smoothing_factor*(s8_co2_mean - s8_co2);
if (!s8_co2_mean2) s8_co2_mean2 = s8_co2;
s8_co2_mean2 = s8_co2_mean2 - smoothing_factor2*(s8_co2_mean2 - s8_co2);
// Serial.printf("CO2 value: %d, M1Value: %d, M2Value: %d\n", s8_co2, s8_co2_mean, s8_co2_mean2);
char buf[8];
itoa(s8_co2, buf, 10);
client.publish("esp/sensors/charon_co2/current", buf);
itoa(s8_co2_mean, buf, 10);
client.publish("esp/sensors/charon_co2/mean", buf);
itoa(s8_co2_mean2, buf, 10);
client.publish("esp/sensors/charon_co2/mean2", buf);
return true;
}
void get_abc() {
int abc_s8_time;
char buf[8];
s8Request(abc_s8);
abc_s8_time = s8Replay(response_s8);
itoa(abc_s8_time, buf, 10);
Serial.printf("Auto calibration of S8-0053 set to %s hours\n", buf);
client.publish("esp/sensors/charon_co2/stat/abc_hours", buf, true);
return;
}
void setup() {
Serial.begin(115200);
Serial.println();
Serial.println("==================================== USING CHARON BRANCH ====================================");
Serial.println();
if(WiFi.status() != WL_CONNECTED) {
wifi_reconnect();
}
client.setServer(mqttServer, mqttPort);
client.setCallback(callback);
mqtt_reconnect();
get_abc();
}
void loop() {
ArduinoOTA.handle();
if (WiFi.status() != WL_CONNECTED) {
wifi_reconnect();
}
if(!client.connected()) {
long now = millis();
if(now - lastReconnectAttempt > 5000) {
lastReconnectAttempt = now;
if(mqtt_reconnect()) {
lastReconnectAttempt = 0;
}
}
} else {
client.loop();
}
long co2_time = millis();
if(co2_time - lastCo2Measured > CO2_INTERVAL) {
co2_measure();
lastCo2Measured = co2_time;
}
int mPkSize = mUdp.parsePacket();
//unsigned int pub_status = 0;
if (mPkSize) {
int len = mUdp.read(mPacket, 255);
if (len > 0) {
mPacket[len] = 0;
}
char jsonPacket[255];
strcpy(jsonPacket, mPacket);
const size_t capacity = JSON_OBJECT_SIZE(3) + JSON_ARRAY_SIZE(2) + 60;
DynamicJsonDocument doc(capacity);
deserializeJson(doc, jsonPacket);
char topic[50] = "esp/";
strcat (topic, doc["sid"].as<char*>());
strcat (topic, "/");
strcat (topic, doc["cmd"].as<char*>());
char SID[25];
strcpy(SID, doc["sid"].as<char*>());
// Serial.printf("UDP packet [%d bytes] contents: %s\n", mPkSize, mPacket);
client.publish(topic, mPacket, true);
// Request status
char ack_data[50] = "{\"cmd\":\"read\",\"sid\":\"";
strcat(ack_data, SID);
strcat(ack_data, "\"}");
uUdp.beginPacket(mUdp.remoteIP(), multicast_port);
uUdp.write(ack_data);
uUdp.endPacket();
// Read answer
int uPkSize = uUdp.parsePacket();
if (uPkSize) {
len = uUdp.read(uPacket, 255);
if (len > 0) {
uPacket[len] = 0;
}
}
if(uUdp.remoteIP() == mUdp.remoteIP()) {
char topic[50] = "esp/";
strcat (topic, SID);
strcat (topic, "/read_ack");
client.publish(topic, uPacket);
char ujsonPacket[255];
strcpy(ujsonPacket, uPacket);
const size_t ucapacity = JSON_OBJECT_SIZE(3) + JSON_ARRAY_SIZE(2) + 60;
DynamicJsonDocument udoc(ucapacity);
deserializeJson(udoc, ujsonPacket);
memset(topic, 0, sizeof(topic));
strcat (topic, "esp/");
strcat (topic, SID);
strcat (topic, "/status");
client.publish(topic,  udoc["data"], true);
}
}
}

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *