В продолжении статей о построении «умного дома»…



Кондиционер Haier серии Lightera имеет на своем борту модуль WiFi для управления им через приложение на телефоне, которое работает через неведомый китайский облачный сервис. Для старых моделей модуль был опцией и приобретался отдельно, подключается к плате управления во внутреннем блоке. На новых моделях разъем выведен под декоративную накладку и в серии Lightera модуль уже установлен. Таким образом, данное устройство применимо ко многим кондиционерам марки Haier.

Для управления кондиционером через родной 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. Подключив параллельно линии RX/TX переходник UART/USB и управляя кондиционером из приложения и с пульта, прочитал все данные.

Фото платы родного модуля.

Плата родного модуля WiFi Haier


На плате видно DC/DC преобразователь на 3.3 В и преобразователи логических уровней. Экран снимать не стал, что под ним неизвестно.

Плата родного модуля WiFi Haier


Это мой первый опыт реверса протокола, но на мой взгляд протокол оказался очень простой.
Скорость обмена составляет 9600/8-N-1. Модуль WiFi каждые 2 секунды отправляет запрос (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 модуля.
Печатная плата

Печатная плата

На фото ниже тестовая плата.

Код написан в среде Arduino. Актуальная версия доступна на 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 руб.


Первая часть — Умный дом, начало.
Часть вторая — Счетчик посетителей ванной комнаты
Поделиться с друзьями
-->

Комментарии (14)


  1. beho1der
    03.07.2016 14:30

    Может выложите принципиальную схему и разведенную плату на github?


    1. instalator
      03.07.2016 17:18

      Могу, только в проекте используются свои библиотеки. Может гербер выложить?


      1. beho1der
        03.07.2016 18:50

        Да поди можно и гербер или вообще ПДФ для ЛУТа


        1. instalator
          03.07.2016 19:47

          выложил герберы и сверловку


  1. memtew
    03.07.2016 16:19

    У кондиционеров Mitsubishi Electric тоже есть возможность управления через wi-fi, и схема работы такая же отвратная — через своё облако. Но главная проблема в том, что модуль wi-fi продается отдельно и стоит довольно дорого (1/3 стоимости кондиционера).


  1. claymen
    03.07.2016 18:11

    Ой как же вы вовремя всё написали, а случаем нет распиновки для модуля относильльно старых моделей
    http://www.haierspares.eu/en/SPHA00138920/electronic-board-Wi-Fi-module
    А то везде большими буквами было написано вайфай, а по факту оказалась опция за 70 EUR…
    Спасибо за проделанную работу.


    1. instalator
      03.07.2016 19:54

      На плате должен быть свободный 4 пиновый разъем +5 В, земля, RX и TX


  1. KonstantinSoloviov
    03.07.2016 19:59
    +2

    То есть от USB только разъем, а на самом деле там 5-вольтвый UART?
    Типа, что бы никто не догадался )


    1. instalator
      03.07.2016 20:01
      +3

      На новых моделях USB разьем выведен наружу, на старых он внутри на плате управления. Да там обычный UART


    1. aivs
      04.07.2016 11:16

      Как к современному компьютеру подключить uart устройство (которых на рынке миллион)? USB самый удобный способ, COM портов уже почти ни у кого нет.


      1. nafikovr
        05.07.2016 10:36

        в том то и дело что здесь USB и не пахнет, и работать воткнутое в комп оно не будет.


  1. Keroro
    04.07.2016 11:52

    А код веб-странички можно где-нибудь посмотреть?


    1. instalator
      04.07.2016 16:17

      Страничка сделана из готовых виджетов драйвера vis


      1. Bluefox
        04.07.2016 21:14

        Или точнее здесь можно посмотреть:
        https://iobroker.biz:8080/