Commande de climatisation Haier basée sur ESP8266

Dans la suite des articles sur la construction d'une «maison intelligente» ... le



climatiseur de la série Haier Lightera a à son bord un module WiFi pour le contrôler via une application sur le téléphone qui fonctionne via un service cloud chinois inconnu. Pour les modèles plus anciens, le module était une option et a été acheté séparément; il est connecté à la carte de commande de l'unité intérieure. Sur les nouveaux modèles, le connecteur est sous une garniture décorative et dans la série Lightera, le module est déjà installé. Ainsi, cette unité est applicable à de nombreux climatiseurs de marque Haier.

Pour contrôler le climatiseur via le module WiFi natif, vous devez télécharger l'application sur votre smartphone / tablette, vous y inscrire, connecter votre smartphone / tablette au routeur via Wi-Fi. Allumez le climatiseur en mode refroidissement de 30 degrés avec la vitesse minimale du ventilateur, assurez-vous que le réseau Haier-uAC est apparu et lancez le programme de recherche d'appareils et de réseaux. Le programme trouve votre climatisation et les réseaux disponibles. Vous enregistrez votre réseau en le sélectionnant dans la liste et procédez à l'enregistrement de votre modèle d'équipement (climatiseur). Dans mon réseau domestique, le serveur DHCP est désactivé sur le routeur et afin de se connecter à mon réseau WiFi sur l'appareil connecté, vous devez créer une nouvelle connexion et vous y inscrire en plus du SSID (car il est masqué) et le mot de passe également une adresse IP statique.C'est pour cette raison que je n'ai pas pu ajouter mon climatiseur à l'application, car lors de l'ajout d'un climatiseur il me demande de ne sélectionner que le point d'accès WiFi et le mot de passe. L'application envoie les données saisies au module WiFi du climatiseur et, à l'aide de ces données, elle essaie de se connecter à votre point d'accès, en espérant qu'on lui donnera une adresse IP, mais mon routeur brise tous ses espoirs.

Apparence du module WiFi natif.

Apparence du module natif WiFi Haier


Pour le test, je l'ai toujours connecté via un autre routeur. La gestion via l'application fonctionne, mais il n'y a aucun moyen de contrôler la climatisation sans l'application, à travers laquelle le service cloud n'est pas clair, il n'y a pas de compte personnel. En conséquence, Haier, comme de nombreux fabricants d'équipements, a créé son propre matériel avec leur application sans possibilité d'intégration avec d'autres systèmes d'automatisation (sans modules et équipements spéciaux). Au final, j'ai décidé de réaliser mon module WiFi avec toutes les caractéristiques d'un personnage bien connu.

La base a été prise ESP8266 12F, qui fonctionnera directement avec mon serveur en utilisant le protocole MQTT. IOBroker est installé sur le serveur , qui fait également office de serveur MQTT.

Restait à comprendre le protocole d'échange avec le climatiseur lui-même. Après avoir étudié les circuits du module natif et de l'unité de contrôle des modèles précédents, il est devenu clair que le module WiFi communique avec le climatiseur via un UART régulier avec des niveaux TTL. Après avoir connecté l'adaptateur UART / USB en parallèle avec la ligne RX / TX et contrôlé le climatiseur depuis l'application et la télécommande, j'ai lu toutes les données.

Carte mère photo mère.

Carte de module WiFi natif Haier


La carte montre un convertisseur 3,3 V CC / CC et des convertisseurs de niveau logique. L'écran n'a pas commencé à tirer, ce qui est inconnu en dessous.

Carte de module WiFi natif Haier


C'est ma première expérience avec l'inversion de protocole, mais à mon avis, le protocole s'est avéré être très simple.
Le taux de change est 9600/8-N-1. Le module WiFi envoie une requête toutes les 13 secondes (13 octets), à laquelle le climatiseur émet un paquet (37 octets) avec toutes les données. Sous le spoiler, une liste d'octets qui se sont révélés démêlés.

Protocole d'échange
1 — FF c
2 — FF c
3 — 22
4 — 00
5 — 00
6 — 00
7 — 00
8 — 00
9 — 01
10 — 01 — , 02 —
11 — 4D — , 6D —
12 — 5F —
13 — 00
14 — 1A — 26 , 1B — 27,
15 — 00
16 — 00
17 — 00
18 — 00 — , 7F-
19 — 00
20 — 00
21 — 00
22 — 00
23 — 00
24 — 00 — smart, 01 — cool, 02 — heat, 03 — , 04 — DRY,
25 — 00
26 — 00 — max, 01 — mid, 02 — min, 03 — auto — FanSpeed
27 — 00
28 — 00 — ., 01 — . 02 — / . 03 —
29 — 00 — , 80 .
30 — 00 — power off, x1 — power on, (1x ) — ? x9 — QUIET
31 — 00
32 — 00 — fresh off, 01 — fresh on
33 — 00
34 — 00
35 — 00
36 — 00 — 16 , 01 — 17 0E — 30 .
37 — . .

Équipes courtes
FF FF 0A 00 00 00 00 00 01 01 4D 02 5B
FF FF 0A 00 00 00 00 00 01 01 4D 03 5C
FF FF 0A 00 00 00 00 00 01 03 00 00 0E
FF FF 0A 00 00 00 00 00 01 01 4D 01 5A

Par exemple, pour régler la température, il faut envoyer:
FF FF 22 00 00 00 00 00 01 01 4D 5F 00 00 00 00 00 00 00 00 00 00 00 00 01 00 02 00 00 00 01 00 00 00 00 00 04 04 D8 - réglé à 20 degrés.

Nous dessinons le schéma du circuit. Le circuit est alimenté par 5 volts à partir du climatiseur, et puisque la tension d'alimentation de l'ESP8266 est de 3,3 volts, il existe alors un stabilisateur linéaire LM1117 (AMS1117) pour la tension de sortie correspondante. Les convertisseurs de niveau logique sont montés sur les éléments R1, Q1, R3 et R2, R3 car le RXD TXD du module ESP8266 n'est pas tolérant à 5 V. Pour programmer l'ESP, les contacts U2 U3 doivent être fermés ensemble.
Schéma du circuit.
Schéma


Nous élevons une carte de circuit imprimé. La disposition de la carte est faite pour une installation dans le boîtier à partir du module WiFi natif.
Circuit imprimé

Circuit imprimé

Sur la photo ci-dessous, le tableau de test.

Le code est écrit dans un environnement Arduino. La version actuelle est disponible sur GitHub .
code
#include <ESP8266WiFi.h>
#include <PubSubClient.h>

const char* ssid = "...";
const char* password = "...";
const char* mqtt_server = "xx.xx.xx.xx"; // MQTT

IPAddress ip(xx,xx,xx,x); //IP 
IPAddress gateway(xx,xx,xx,xx); // 
IPAddress subnet(xx,xx,xx,xx); // 

WiFiClient espClient;
PubSubClient client(espClient);

#define ID_CONNECT "myhome-Conditioner"
#define LED     12
#define LEN_B   37

#define B_CUR_TMP   13  // 
#define B_CMD       17  // 00- 7F- ???
#define B_MODE      23  //04 - DRY, 01 - cool, 02 - heat, 00 - smart 03 - 
#define B_FAN_SPD   25  // 02 - min, 01 - mid, 00 - max, 03 - auto
#define B_SWING     27  //01 -     . 00 - . 02 - / . 03 -  
#define B_LOCK_REM  28  //80  . 00 -  
#define B_POWER     29  //on/off 01 - on, 00 - off (10, 11)-??? 09 - QUIET
#define B_FRESH     31  //fresh 00 - off, 01 - on
#define B_SET_TMP   35  // 

int fresh;
int power;
int swing;
int lock_rem;
int cur_tmp;
int set_tmp;
int fan_spd;
int Mode;
long prev = 0;
byte inCheck = 0;
byte qstn[] = {255,255,10,0,0,0,0,0,1,1,77,1,90}; //  
//byte start[] = {255,255};
byte data[37] = {}; // 
byte on[]   = {255,255,10,0,0,0,0,0,1,1,77,2,91}; //  
byte off[]  = {255,255,10,0,0,0,0,0,1,1,77,3,92}; //  
byte lock[] = {255,255,10,0,0,0,0,0,1,3,0,0,14};  //  
//byte buf[10];

void setup_wifi() {
  delay(10);
  WiFi.begin(ssid, password);
  WiFi.config(ip, gateway, subnet);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    digitalWrite(LED, !digitalRead(LED));
  }
  digitalWrite(LED, HIGH);
}

void reconnect() {
  digitalWrite(LED, !digitalRead(LED));
  while (!client.connected()) {
    if (client.connect(ID_CONNECT)) {
      client.publish("myhome/Conditioner/connection", "true");
      client.publish("myhome/Conditioner/RAW", "");
      client.subscribe("myhome/Conditioner/#");
      digitalWrite(LED, HIGH);
    } else {
      delay(5000);
    }
  }
}

void InsertData(byte data[], size_t size){
    set_tmp = data[B_SET_TMP]+16;
    cur_tmp = data[B_CUR_TMP];
    Mode = data[B_MODE];
    fan_spd = data[B_FAN_SPD];
    swing = data[B_SWING];
    power = data[B_POWER];
    lock_rem = data[B_LOCK_REM];
    fresh = data[B_FRESH];
  /////////////////////////////////
  if (fresh == 0x00){
      client.publish("myhome/Conditioner/Fresh", "off");
  }
  if (fresh == 0x01){
      client.publish("myhome/Conditioner/Fresh", "on");
  }
  /////////////////////////////////
  if (lock_rem == 0x80){
      client.publish("myhome/Conditioner/Lock_Remote", "true");
  }
  if (lock_rem == 0x00){
      client.publish("myhome/Conditioner/Lock_Remote", "false");
  }
  /////////////////////////////////
  if (power == 0x01 || power == 0x11){
      client.publish("myhome/Conditioner/Power", "on");
  }
  if (power == 0x00 || power == 0x10){
      client.publish("myhome/Conditioner/Power", "off");
  }
  if (power == 0x09){
      client.publish("myhome/Conditioner/Power", "quiet");
  }
  if (power == 0x11 || power == 0x10){
      client.publish("myhome/Conditioner/Compressor", "on");
  } else {
    client.publish("myhome/Conditioner/Compressor", "off");
  }
  /////////////////////////////////
  if (swing == 0x00){
      client.publish("myhome/Conditioner/Swing", "off");
  }
  if (swing == 0x01){
      client.publish("myhome/Conditioner/Swing", "ud");
  }
  if (swing == 0x02){
      client.publish("myhome/Conditioner/Swing", "lr");
  }
  if (swing == 0x03){
      client.publish("myhome/Conditioner/Swing", "all");
  }
  /////////////////////////////////  
  if (fan_spd == 0x00){
      client.publish("myhome/Conditioner/Fan_Speed", "max");
  }
  if (fan_spd == 0x01){
      client.publish("myhome/Conditioner/Fan_Speed", "mid");
  }
  if (fan_spd == 0x02){
      client.publish("myhome/Conditioner/Fan_Speed", "min");
  }
  if (fan_spd == 0x03){
      client.publish("myhome/Conditioner/Fan_Speed", "auto");
  }
  /////////////////////////////////
  char b[5]; 
  String char_set_tmp = String(set_tmp);
  char_set_tmp.toCharArray(b,5);
  client.publish("myhome/Conditioner/Set_Temp", b);
  ////////////////////////////////////
  String char_cur_tmp = String(cur_tmp);
  char_cur_tmp.toCharArray(b,5);
  client.publish("myhome/Conditioner/Current_Temp", b);
  ////////////////////////////////////
  if (Mode == 0x00){
      client.publish("myhome/Conditioner/Mode", "smart");
  }
  if (Mode == 0x01){
      client.publish("myhome/Conditioner/Mode", "cool");
  }
  if (Mode == 0x02){
      client.publish("myhome/Conditioner/Mode", "heat");
  }
  if (Mode == 0x03){
      client.publish("myhome/Conditioner/Mode", "vent");
  }
  if (Mode == 0x04){
      client.publish("myhome/Conditioner/Mode", "dry");
  }
  
  String raw_str;
  char raw[75];
  for (int i=0; i < 37; i++){
     if (data[i] < 10){
       raw_str += "0";
       raw_str += String(data[i], HEX);
     } else {
      raw_str += String(data[i], HEX);
     }    
  }
  raw_str.toUpperCase();
  raw_str.toCharArray(raw,75);
  client.publish("myhome/Conditioner/RAW", raw);
  
///////////////////////////////////
}

byte getCRC(byte req[], size_t size){
  byte crc = 0;
  for (int i=2; i < size; i++){
      crc += req[i];
  }
  return crc;
}

void SendData(byte req[], size_t size){
  //Serial.write(start, 2);
  Serial.write(req, size - 1);
  Serial.write(getCRC(req, size-1));
}

inline unsigned char toHex( char ch ){
   return ( ( ch >= 'A' ) ? ( ch - 'A' + 0xA ) : ( ch - '0' ) ) & 0x0F;
}

void callback(char* topic, byte* payload, unsigned int length) {
  payload[length] = '\0';
  String strTopic = String(topic);
  String strPayload = String((char*)payload);
  ///////////
  if (strTopic == "myhome/Conditioner/Set_Temp"){
    set_tmp = strPayload.toInt()-16;
    if (set_tmp >= 0 && set_tmp <= 30){
      data[B_SET_TMP] = set_tmp;      
    }
  }
  //////////
  if (strTopic == "myhome/Conditioner/Mode"){
     if (strPayload == "smart"){
      data[B_MODE] = 0; 
    }
    if (strPayload == "cool"){
        data[B_MODE] = 1;
    }
    if (strPayload == "heat"){
        data[B_MODE] = 2; 
    }
    if (strPayload == "vent"){
        data[B_MODE] = 3;
    }
    if (strPayload == "dry"){
        data[B_MODE] = 4;
    }
  }
  //////////
  if (strTopic == "myhome/Conditioner/Fan_Speed"){
     if (strPayload == "max"){
      data[B_FAN_SPD] = 0; 
    }
    if (strPayload == "mid"){
        data[B_FAN_SPD] = 1;
    }
    if (strPayload == "min"){
        data[B_FAN_SPD] = 2; 
    }
    if (strPayload == "auto"){
        data[B_FAN_SPD] = 3; 
    }
  }
  ////////
  if (strTopic == "myhome/Conditioner/Swing"){
     if (strPayload == "off"){
      data[B_SWING] = 0; 
    }
    if (strPayload == "ud"){
        data[B_SWING] = 1;
    }
    if (strPayload == "lr"){
        data[B_SWING] = 2; 
    }
    if (strPayload == "all"){
        data[B_SWING] = 3; 
    }
  }
  ////////
  if (strTopic == "myhome/Conditioner/Lock_Remote"){
     if (strPayload == "true"){
      data[B_LOCK_REM] = 80;
    }
    if (strPayload == "false"){
        data[B_LOCK_REM] = 0;
    }
  }
  ////////
  if (strTopic == "myhome/Conditioner/Power"){
     if (strPayload == "off" || strPayload == "false" || strPayload == "0"){
      SendData(off, sizeof(off)/sizeof(byte));
      return;
    }
    if (strPayload == "on" || strPayload == "true" || strPayload == "1"){
      SendData(on, sizeof(on)/sizeof(byte));
      return;
    }
    if (strPayload == "quiet"){
        data[B_POWER] = 9;
    }
  }
  ////////
  if (strTopic == "myhome/Conditioner/RAW"){
    char buf[75];
    char hexbyte[3] = {0};
    strPayload.toCharArray(buf, 75);
    int octets[sizeof(buf) / 2] ;
    for (int i=0; i < 76; i += 2){
      hexbyte[0] = buf[i] ;
      hexbyte[1] = buf[i+1] ;
      data[i/2] = (toHex(hexbyte[0]) << 4) | toHex(hexbyte[1]);
    }
    Serial.write(data, 37);
    client.publish("myhome/Conditioner/RAW", buf);
  }
  
  data[B_CMD] = 0;
  data[9] = 1;
  data[10] = 77;
  data[11] = 95;
  SendData(data, sizeof(data)/sizeof(byte));
}

void setup() {
  pinMode(LED, OUTPUT);
  Serial.begin(9600);
  setup_wifi();
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
}

void loop() {
  if(Serial.available() > 0){
    Serial.readBytes(data, 37);
    while(Serial.available()){
      delay(2);
      Serial.read();
    }
    if (data[36] != inCheck){
      inCheck = data[36];
      InsertData(data, 37);
    }
  }
  
  if (!client.connected()){
    reconnect();
  }
  client.loop();

  long now = millis();
  if (now - prev > 5000) {
    prev = now;
    SendData(qstn, sizeof(qstn)/sizeof(byte)); // 
  }
}


Après avoir clignoté l'ESP8266, nous avons mis le module dans le climatiseur. Les rubriques sont créées automatiquement sur le serveur MQTT:
Circuit imprimé


Panneau de commande de climatisation sur une page Web.
Circuit imprimé

En plus de la gestion à partir d'une page Web, la gestion des commandes vocales est organisée, ainsi que via le pilote Telegram pour IOBroker.
À un coût, le nouveau module coûte environ 200 roubles.


La première partie - Smart home, le début.
Deuxième partie - Comptoir des visiteurs de salle de bain

Source: https://habr.com/ru/post/fr395533/


All Articles