Controle de ar condicionado Haier baseado em ESP8266

Na continuação dos artigos sobre a construção de uma “casa inteligente” ... o



ar condicionado da série Haier Lightera tem em seu quadro um módulo WiFi para controlá-lo através de um aplicativo no telefone que funciona através de um serviço em nuvem chinês desconhecido. Para modelos mais antigos, o módulo era uma opção e foi adquirido separadamente; ele é conectado à placa de controle na unidade interna. Em novos modelos, o conector está embaixo de uma guarnição decorativa e, na série Lightera, o módulo já está instalado. Portanto, esta unidade é aplicável a muitos condicionadores de ar da marca Haier.

Para controlar o ar condicionado através do módulo WiFi nativo, é necessário fazer o download do aplicativo no seu smartphone / tablet, registrar-se nele, conectar seu smartphone / tablet ao roteador via Wi-Fi. Ligue o ar condicionado no modo de resfriamento em 30 graus com a velocidade mínima do ventilador, verifique se a rede Haier-uAC apareceu e inicie o programa para pesquisar dispositivos e redes. O programa encontra o seu ar condicionado e as redes disponíveis. Você registra sua rede selecionando-a na lista e continua registrando seu modelo de equipamento (ar condicionado). Na minha rede doméstica, o servidor DHCP está desativado no roteador e, para conectar-se à minha rede Wi-Fi no dispositivo conectado, é necessário criar uma nova conexão e registrar-se além do SSID (já que está oculto) e a senha também é um endereço IP estático.É por esse motivo que não pude adicionar meu ar condicionado ao aplicativo, pois, ao adicionar um ar condicionado, ele solicita que eu selecione apenas o ponto de acesso e a senha do WiFi. O aplicativo envia os dados inseridos para o módulo WiFi do ar condicionado e, usando esses dados, ele tenta se conectar ao seu ponto de acesso, esperando receber um endereço IP, mas meu roteador quebra todas as suas esperanças.

Aparência do módulo WiFi nativo.

Aparência do módulo WiFi Haier nativo


Para o teste, eu ainda o conectei através de outro roteador. O gerenciamento por meio do aplicativo funciona, mas não há como controlar o ar condicionado sem o aplicativo, por meio do qual o serviço em nuvem não é claro, não há conta pessoal. Como resultado, a Haier, como muitos fabricantes de equipamentos, criou seu próprio hardware com sua aplicação, sem a possibilidade de integração com outros sistemas de automação (sem módulos e equipamentos especiais). No final, eu decidi fazer meu módulo WiFi com todas as características de um personagem bem conhecido.

A base foi tomada ESP8266 12F, que funcionará diretamente com meu servidor usando o protocolo MQTT. O IOBroker está instalado no servidor , que também atua como um servidor MQTT.

Faltava entender o protocolo de troca com o próprio ar condicionado. Tendo estudado os circuitos do módulo nativo e da unidade de controle dos modelos anteriores, ficou claro que o módulo WiFi se comunica com o ar condicionado através de um UART regular com níveis TTL. Depois de conectar o adaptador UART / USB em paralelo à linha RX / TX e controlar o ar condicionado do aplicativo e do controle remoto, li todos os dados.

Foto placa-mãe placa-mãe.

Placa do módulo WiFi nativo da Haier


A placa mostra um conversor de 3,3 V DC / DC e conversores de nível lógico. A tela não começou a filmar, o que é desconhecido sob ela.

Placa do módulo WiFi nativo da Haier


Esta é a minha primeira experiência com a reversão de protocolo, mas, na minha opinião, o protocolo acabou sendo muito simples.
A taxa de câmbio é 9600/8-N-1. O módulo WiFi envia uma solicitação a cada 13 segundos (13 bytes), para a qual o ar condicionado emite um pacote (37 bytes) com todos os dados. Sob o spoiler, uma lista de bytes que acabou sendo desvendada.

Protocolo de troca
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 — . .

Equipas curtas
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

Por exemplo, para definir a temperatura, é necessário enviar:
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 00 04 04 D8 - definido para 20 graus.

Nós desenhamos o diagrama de circuito. O circuito é alimentado por 5 volts do ar condicionado e, como a tensão de alimentação do ESP8266 é de 3,3 volts, existe um estabilizador linear LM1117 (AMS1117) para a tensão de saída correspondente. Os conversores de nível lógico são montados nos elementos R1, Q1, R3 e R2, R3, pois o RXD TXD do módulo ESP8266 não é tolerante a 5 V. Para programar o ESP, os contatos U2 U3 devem ser fechados juntos.
Diagrama de circuito
Esquema


Criamos uma placa de circuito impresso. O layout da placa é feito para instalação na caixa a partir do módulo WiFi nativo.
Placa de circuito

Placa de circuito

Na foto abaixo, a placa de teste.

O código é escrito em um ambiente Arduino. A versão atual está disponível no GitHub .
código
#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)); // 
  }
}


Depois de piscar o ESP8266, colocamos o módulo no ar condicionado. Os tópicos são criados automaticamente no servidor MQTT:
Placa de circuito


Painel de controle do ar condicionado em uma página da web.
Placa de circuito

Além de gerenciar a partir de uma página da web, o gerenciamento de comandos de voz é organizado, bem como por meio do driver Telegram para IOBroker.
A um custo, o novo módulo custa cerca de 200 rublos.


A primeira parte - casa inteligente, o começo.
Parte Dois - Contador de Visitantes do Banheiro

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


All Articles