التحكم في مكيفات الهواء من هاير على أساس ESP8266

استمرارًا للمقالات المتعلقة ببناء "منزل ذكي" ...



يحتوي مكيف الهواء من سلسلة Haier Lightera على لوحته على وحدة WiFi للتحكم فيه من خلال تطبيق على الهاتف يعمل من خلال خدمة سحابية صينية غير معروفة. بالنسبة للموديلات القديمة ، كانت الوحدة خيارًا وتم شراؤها بشكل منفصل ؛ وهي متصلة بلوحة التحكم في الوحدة الداخلية. في الموديلات الجديدة ، يوجد الموصل أسفل زخرفة زخرفية وفي سلسلة Lightera تم تثبيت الوحدة بالفعل. وبالتالي ، فإن هذه الوحدة قابلة للتطبيق على العديد من مكيفات هواء ماركة هاير.

WiFi /, , / Wi-Fi. 30 , , Haier-uAC, . . , , (). DHCP WiFi SSID ( ) IP . , WiFi . WiFi , , , , IP , .

ظهور وحدة WiFi الأصلية.

ظهور وحدة WiFi Haier الأصلية


للاختبار ، ما زلت توصيله من خلال جهاز توجيه آخر. تعمل الإدارة من خلال التطبيق ، ولكن لا توجد طريقة للتحكم في تكييف الهواء بدون التطبيق ، من خلال الخدمة السحابية التي لا تتضح ، لا يوجد حساب شخصي. ونتيجة لذلك ، أنشأت شركة Haier ، مثل العديد من الشركات المصنعة للمعدات ، أجهزتها الخاصة مع تطبيقاتها دون إمكانية التكامل مع أنظمة الأتمتة الأخرى (بدون وحدات ومعدات خاصة). في النهاية ، قررت أن أجعل وحدة WiFi الخاصة بي مع جميع خصائص الشخصية المعروفة.

تم أخذ الأساس ESP8266 12F ، والذي سيعمل مباشرة مع الخادم الخاص بي باستخدام بروتوكول MQTT. يتم تثبيت IOBroker على الخادم ، والذي يعمل أيضًا كخادم MQTT.

بقي لفهم بروتوكول التبادل مع مكيف الهواء نفسه. بعد دراسة الوحدة الأصلية ودوائر وحدة التحكم للنماذج السابقة ، أصبح من الواضح أن وحدة WiFi تتواصل مع مكيف الهواء من خلال UART العادي بمستويات TTL. بعد توصيل محول UART / USB بالتوازي مع خط RX / TX والتحكم في مكيف الهواء من التطبيق وجهاز التحكم عن بعد ، قرأت جميع البيانات.

صور اللوحة الأم اللوحة الأم.

لوحة هاير واي فاي الأصلية


يُظهر اللوح محولًا بجهد 3.3 فولت DC / DC ومحولات مستوى منطقي. الشاشة لم تبدأ بالتصوير ، ما هو غير معروف تحتها.

لوحة هاير واي فاي الأصلية


هذه هي تجربتي الأولى مع عكس البروتوكول ، ولكن في رأيي تبين أن البروتوكول بسيط للغاية.
سعر الصرف هو 9600/8-N-1. ترسل وحدة WiFi طلبًا كل 13 ثانية (13 بايت) ، حيث يصدر مكيف الهواء حزمة (37 بايت) مع جميع البيانات. تحت المفسد ، قائمة البايت التي تبين أنها غير مكشوفة.

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 — . .

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

:
FF FF 22 00 00 00 00 00 01 01 4D 5F 00 00 00 00 00 00 00 00 00 00 00 01 00 02 00 00 00 01 00 00 00 00 00 04 D8 — 20 .

. 5 , ESP8266 — 3.3 , LM1117(AMS1117) . R1, Q1, R3 R2, R3 RXD TXD ESP8266 5 . ESP U2 U3 .
.
مخطط


. WiFi .
لوحة الدوائر

لوحة الدوائر

.

الرمز مكتوب في بيئة اردوينو. الإصدار الحالي متاح على GitHub .
كود
#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)); // 
  }
}


بعد وميض ESP8266 ، نضع الوحدة في مكيف الهواء. يتم إنشاء المواضيع تلقائيًا على خادم MQTT:
لوحة الدوائر


لوحة تحكم تكييف الهواء على صفحة ويب.
لوحة الدوائر

بالإضافة إلى الإدارة من صفحة ويب ، يتم تنظيم إدارة الأوامر الصوتية ، وكذلك من خلال برنامج تشغيل Telegram لـ IOBroker.
بتكلفة ، تكلف الوحدة الجديدة حوالي 200 روبل.


الجزء الأول - البداية الذكية ، البداية.
الجزء الثاني - عداد زوار الحمام

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


All Articles