Kontrola klimatyzacji przez MQTT
Sterowanie klimatyzatorem przez Wi-Fi jest bardzo przydatną funkcją. Pozwala chociażby na ustawienie odpowiedniej temperatury przed wejściem do pomieszczenia, nawet będąc poza domem. Umożliwia też tworzenie zaawansowanych automatyzacji, zapewniających bardziej stabilną temperaturę lub różnego rodzaju uzależnienie pracy od panujących warunków. Automatyzacja taka może na przykład latem preferować uruchamianie klimatyzatora w nocy, gdy nie tylko zużycie prądu w wielu domach jest liczone po niższej stawce, ale też temperatura na zewnątrz jest najniższa, co sprawia, że klimatyzator sam w sobie zużywa mniej energii elektrycznej.
Tyle w teorii, bo w praktyce sposób, w jaki producenci zapewniają zazwyczaj swoim urządzeniom łączność z siecią, jest absolutnie nieakceptowalny. Komunikacja często jest możliwa wyłącznie przez smartphona i wyłącznie przez aplikację producenta. Oprogramowanie jest zaprojektowane byle jak i zazwyczaj niemożliwe do jakiejkolwiek konfiguracji; dostosowania do własnych preferencji lub sposobu, w jaki ma być używane. I co najgorsze, wszystko się opiera na infrastrukturze producenta, co oznacza, że powierzamy mu pełną kontrolę nad urządzeniem. Wynika z tego wiele problemów. Jeśli producent stwierdzi, że nie opłaca się mu dłużej utrzymywać serwerów lub aktualizować oprogramowania, to nasze urządzenie, a przynajmniej jego sieciowe funkcje, przestaną działać. Może też on nasze urządzenie w każdej chwili po prostu zablokować, co już się zdarzało . Nie są to oczywiście wszystkie problemy; można też poruszać chociażby kwestię bezpieczeństwa lub faktu, że można po prostu nie chcieć w domu urządzenia pracującego na własnościowym oprogramowaniu.
Dlatego też, gdy jakiś czas temu zainstalowałem w warsztacie klimatyzator, to nawet nie brałem pod uwagę korzystania ze sterownika Wi-Fi producenta, tylko od razu postanowiłem zrobić własny.
Spis treści:
Pełen proces
Założenia były proste. Mikrokontroler musi się komunikować z brokerem MQTT oraz z klimatyzatorem. Po otrzymaniu polecenia z brokera powinien wysłać odpowiednią komendę do klimatyzatora, a po otrzymaniu statusu od klimatyzatora, wysłać aktualizację do brokera. Tylko tyle. Nie chcę implementować żadnego OTA ani jakiegoś trybu konfiguracji, bo korzyści z tego byłby w moim przypadku znikome, a tylko by to zwiększało ilość potencjalnych problemów z bezpieczeństwem.
No to teraz trudna część. Jak się komunikować z klimatyzatorem? O jakiejkolwiek dokumentacji można pomarzyć, więc trzeba będzie się trochę pomęczyć. W zestawie z klimatyzatorem był już załączony sterownik. Po zajrzeniu do środka wygląda na to, że to zwykłe ESP32. W dodatku komunikacja odbywa się przez UART. Nie może być to zbyt skomplikowane.


Pewnie ktoś już nawet doszedł do tego, jak to działa. Szybka konsultacja z internetem pokazuje, że owszem, istnieje już taki projekt . Najwyraźniej Haier ma 2 rodzaje sterowników. Jeden wykorzystuje port USB, drugi, tak jak mój, JST SM04B-GHS-TB. Protokół jednak w obydwu przypadkach powinien być ten sam. Projekt wykorzystuje ESPHome zamiast MQTT, ale nic nie szkodzi. Wystarczy, że skopiuję potrzebne wiadomości do komunikacji z klimatyzatorem. Nasuwa się jednak pytanie, czy nie mógłbym sobie dać spokoju z tym MQTT i po prostu użyć gotowego kodu? Mógłbym.
Tak więc skopiowałem wymagane wiadomości i uporządkowałem je w osobną klasę. Teraz czas na MQTT. Na szczęście nie jest to pierwszy raz, gdy go używam, więc skopiowałem obsługę ze swoich poprzednich projektów. Jeszcze kilka modyfikacji et voila, kod do sterownika gotowy. Następnym krokiem jest zajęcie się innymi sprawami i ciągłe odkładanie dokończenia tego sterownika na później.
Rok później
Czas wreszcie przetestować ten kod. Złącza już dawno dotarły, więc wystarczy je przylutować i można podłączać. Przylutowanie przewodów do tak małego złącza SMD nie jest najprostszym zadaniem, szczególnie bez dużego doświadczenia, ale w końcu się udało. Podłączyłem więc sterownik do klimatyzatora… i nie działa. Można było się tego spodziewać. Czas na debugowanie. Pierwszym problemem wydaje się fakt, że klimatyzator się komunikuje przez 5 V, a nie 3,3 V. Powinienem był to sprawdzić wcześniej. Mikrokontroler na szczęście nie ucierpiał, a ja już kiedyś wykorzystywałem konwertery poziomów logicznych, więc chyba mam kilka w zapasie. Szybka modyfikacja obwodu, no i się przewód oderwał od złącza JST. A myślałem, że mam to już za sobą…
Sterownik podłączony, tym razem ma poprawne napięcie, klimatyzator zaczął już wydawać jakieś dźwięki, ale dalej nie działa. No ale cóż, od czego mam oscyloskop. Ściągam sterownik i mierzę, co on na ten UART podaje. Pierwsze dwie wiadomości inicjalizacyjne wysyła, dalej cisza. Z brokerem łączy się poprawnie, więc próbuję wysłać przez MQTT jakieś wiadomości do debugowania, żeby zobaczyć, co tam się dzieje. Po długich godzinach dochodzenia o co chodzi, trafiłem w odmętach GitHuba na komentarz sugerujący, że biblioteka, której używam, PubSubClient , może sprawiać problemy, jeśli klient jest umieszczony w osobnej klasie. Co prawda zawsze jej używałem w taki sposób, ale nigdy nie wykorzystywałem interfejsu UART, więc może rzeczywiście to jest winowajca. Przeniosłem zawartość wszystkich klas związanych z siecią do głównego pliku… i zadziałało! A przynajmniej zaczęło wysyłać wiadomości, bo klimatyzator po podłączeniu dalej nie funkcjonuje poprawnie. Przy okazji, przewód się znowu oderwał.
Wygląda na to, że protokół jednak nie jest taki sam dla wszystkich klimatyzatorów. Pora się temu lepiej przyjrzeć. Najlepiej chyba zacząć od odczytywania obecnego stanu. Klimatyzator włączony pilotem. Reaguje na poll. Dokładna analiza odpowiedzi, bajt po bajcie wskazuje, że wszystkie potrzebne wartości są na miejscach zgodnych z oryginalnym kodem. Może jednak nie będzie tak źle. Na tym etapie, żeby co chwilę nie sięgać do góry, miałem już przewody z klimatyzatora wyprowadzone bezpośrednio na biurko. Jeden z nich się oderwał od wtyczki. Po co ja to robię? Jest tyle lepszych zajęć… Mógłbym zostać ogrodnikiem, dać sobie spokój z całą tą elektroniką… No ale nic. Zlutowałem wszytko z powrotem, zacząłem w tym już nabierać wprawy, podłączyłem i można działać dalej. Czas na wysyłanie poleceń. Wygląda na to, że osobne polecenie do włączenia jest zbędne. Klimatyzator się automatycznie włącza przy ustawianiu trybu. Kilka kolejnych testów, kilka kolejnych modyfikacji i działa już coraz lepiej. Cały czas pozostaje jednak wyraźny problem. Przy każdym poleceniu włącza się “health mode” oraz nie da się ustawić temperatury na 21 stopni. We wspomnianym wcześniej wątku na forum pojawiały się forki projektu oraz komentarze sugerujące, że 7. bit 18. bajtu odpowiada za przełączanie health mode. Jeśli jest ustawiony na jedynkę, to health mode będzie na zmianę się włączał i wyłączał. U mnie to jednak nie działało. Bit ten był Już ustawiony na jedynkę. Gdy go zmieniłem na zero, health mode zaczął się na zmianę włączać i wyłączać, a temperatury nie dało się ustawić na 23 stopnie. Podczas czytania różnych wersji i forków tego projektu w poszukiwaniu rozwiązania zauważyłem, że w jednym z nich 5. i 6. bit 22. bajtu mają wartość 0, zamiast 1. Po odpowiedniej korekcie w moim kodzie problem zniknął.
Pozostała już tylko jedna rzecz, która nie działała poprawnie. Nie dało się włączyć trybu samego wentylatora. Tutaj rozwiązanie okazało się proste. Cały czas miałem włączoną automatyczną prędkość wentylatora. Gdy próbowałem dodatkowo ustawić tryb “tylko wentylator”, klimatyzator nie akceptował takiej pary ustawień, więc ignorował polecenie. Wystarczyło dodać warunek, który dla rzeczonego trybu automatycznie zmienia automatyczną prędkość na niską, no i wreszcie wszystko działa!
Nadal jednak całość jest podłączona przez dwumetrowe przewody. Rozłączyłem wszystko, zlutowałem mikrokontroler i konwerter w bardziej kompaktowym ułożeniu. Oderwał się przewód od złącza, przylutowałem z powrotem i zalałem żywicą epoksydową. Przylutowałem złącze do konwertera, zostawiając sporo zapasu na przewodach i gotowe, całość się nawet mieści w oryginalnej wnęce.



Już tylko ostatni - obudowa. Zmierzyłem wszytko i doszedłem do wniosku, że jeśli dodam obudowę, to pokrywka wnęki w klimatyzatorze nie będzie się domykać. Chyba można więc sobie tę obudowę odpuścić i oficjalnie uznać projekt za skończony.
“Jak mogę tego użyć?”
Projekt był testowany na klimatyzatorze Haier AS35TADHRA-CLC. Jeśli masz taki klimatyzator lub inny klimatyzator firmy Haier i liczysz, że zadziała, co, jak widać w poprzedniej sekcji, wcale nie jest takie oczywiste, to wystarczy, że pobierzesz kod z mojego GitLaba i zmodyfikujesz następujący fragment:
#define AC_NAME "Warsztat klimatyzacja"
#define AC_MANUF "Haier"
#define AC_MODEL "AS35TADHRA-CLC"
#define MQTT_TOPIC_PREFIX "/garaz/warsztat/ac"
#define MQTT_SERVER "mqtt.home"
#define MQTT_PORT 1883
#define MQTT_CLIENT_NAME "W_AC_MQTT"
#define MQTT_USERNAME "user"
#define MQTT_PASS "pass"
#define MQTT_ONLINE_TOPIC "/garaz/warsztat/ac/online"
#define MQTT_ONLINE_MESSAGE "online"
#define MQTT_OFFLINE_MESSAGE "offline"
#define WIFI_SSID "ssid"
#define WIFI_PASS "pass"
Pierwsze 3 wartości: AC_NAME
, AC_MANUF
oraz AC_MODEL
to dane, które zostaną umieszczone we wiadomości konfiguracyjnej “MQTT Discovery” dla Home Assistanta. Wspominałem, że sterownik ma wsparcie dla Home Assistanta? Nie? No to teraz wspominam. Wartości te nie mają żadnego funkcjonalnego znaczenia. Wpływają jedynie na tekst wyświetlany w rejestrze urządzeń. Jeśli nie masz Home Assistanta, to możesz je zignorować.
Kolejny jest MQTT_TOPIC_PREFIX
. Jest to początek tematów, na których będzie się odbywać komunikacja z klimatyzatorem. Jeśli ustawisz tę wartość na /klimatyzator
, to wiadomości będą wysyłane na tematy /klimatyzator/state
, /klimatyzator/mode/set
, /klimatyzator/temp/set
itd.
Dalej MQTT_SERVER
, czyli adres brokera MQTT. Może to być adres IP lub domena, jeśli wykorzystujesz zewnętrzny serwis, albo masz w domu serwer DNS. MQTT_PORT
jest oczywiście portem brokera. Domyślny port to zazwyczaj 1883.
MQTT_USERNAME
i MQTT_PASS
to nazwa użytkownika i hasło do brokera MQTT.
MQTT_ONLINE_TOPIC
to temat wiadomości o dostępności urządzenia. Po połączeniu z brokerem sterownik wyśle na ten temat wiadomość MQTT_ONLINE_MESSAGE
, a gdy utraci połączenie, to zostanie wysłana wiadomość MQTT_OFFLINE_MESSAGE
. Dla Home Assistanta domyślne wartości tych wiadomości to online
i offline
.
Ostatnie 2 parametry, WIFI_SSID
i WIFI_PASS
to nazwa sieci Wi-Fi oraz hasło.
Po dokonaniu wymaganych zmian wystarczy wgrać kod na mikrokontroler. Dla niedoświadczonych użytkowników najprostszą metodą będzie wykorzystanie rozszerzenia PlatformIO w VSCode. Ponieważ cały projekt jest oparty na protokole MQTT, to będzie oczywiście potrzebny broker MQTT. Polecam pakiet Mosquitto zainstalowany na RaspberryPi lub innym SBC.
Jeśli chodzi o hardware, to wykorzystałem płytkę Wemos D1 mini, opartą na ESP8266. Nie ma w niej nic wyjątkowego, niezbędnego dla tego projektu, po prostu ją miałem pod ręką. Projekt powinien być kompatybilny z innymi płytkami ESP8266, a być może nawet ESP32, ale tylko na Wemos D1 mini go testowałem. Oprócz tego będzie też potrzebny konwerter poziomów logicznych, pozwalający na komunikację 5 V i 3,3 V, oraz złącze JST SM04B-GHS-TB. Wszystko należy połączyć według poniższego schematu:

Kolejność przewodów we wtyczce JST, patrząc od przodu to: +5 V, masa, RX i TX (mikrokontrolera).

Jak to działa
Klimatyzator
Komunikacja z klimatyzatorem jest bardzo prosta. Wszystko odbywa się przez UART. Po uruchomieniu mikrokontroler wysyła dwie wiadomości inicjalizacyjne.
byte initialization_1[13] = {0xFF, 0xFF, 0x0A, 0x0, 0x0, 0x0, 0x0,
0x0, 0x00, 0x61, 0x00, 0x07, 0x72};
byte initialization_2[13] = {0xFF, 0xFF, 0x08, 0x40, 0x0, 0x0, 0x0,
0x0, 0x0, 0x70, 0xB8, 0x86, 0x41};
delay(1000);
Serial.write(initialization_1, sizeof(initialization_1));
delay(1000);
Serial.write(initialization_2, sizeof(initialization_2));
Następnie żądanie zmiany ustawień jest wysyłane pojedynczą, dwudziestopięciobajtową komendą, zawierającą wszystkie parametry.
byte command[25] = {0xFF, 0xFF, 0x14, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x60, 0x01, 0x09, 0x08, 0x25, 0x00, 0x02, 0x01,
0x00, 0x06, 0x00, 0x00, 0x03, 0x0B, 0x70};
Lokalizacja parametrów:
- Bajt trzynasty (licząc od 1) to żądana temperatura w stopniach Celsjusza minus 16.
- Bajt czternasty to kierunek nawiewu.
- Pierwsze cztery bity bajtu piętnastego to tryb pracy.
- Ostatnie cztery bity bajtu piętnastego to prędkość wentylatora.
Dostępne tryby pracy:
- 1000 - grzanie
- 0010 - chłodzenie
- 0100 - osuszanie
- 1100 - tylko wentylator
Dostępne prędkości wentylatora:
- 0011 - niska
- 0010 - średnia
- 0001 - wysoka
- 0101 - automatyczna
Dostępne kierunki nawiewu:
- 00001100 - przemiatanie
- 00000110 - środek
- Jakieś tam jeszcze, których nie zaimplementowałem, lol.
Klimatyzator zwraca aktualny stan, w postaci czterdziestosiedmiobajtowej wiadomości, po otrzymaniu żądania poll.
const byte poll[15] = {0xFF, 0xFF, 0x0A, 0x40, 0x00, 0x00, 0x00, 0x00,
0x00, 0x01, 0x4D, 0x01, 0x99, 0xB3, 0xB4};
Pozycje żądanej temperatury, kierunku nawiewu, trybu pracy oraz prędkości wentylatora w otrzymanej odpowiedzi są takie same, jak w żądaniu zmiany ustawień. Oprócz tego dochodzą:
- Aktualna temperatura pomnożona przez 2, na dwudziestym trzecim bajcie.
- Aktualny stan (on/off) na ostatnim bicie osiemnastego bajtu.
Wyłączenie klimatyzatora odbywa się przez wysłanie siedemnastobajtowej wiadomości:
const byte power_command[17] = {0xFF, 0xFF, 0x0C, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x5D, 0x01, 0x00, 0x00, 0xAB, 0x7D, 0x3A};
Wszystkie wiadomości wykorzystują również crc do sprawdzania poprawności danych.
MQTT
Po połączeniu z brokerem MQTT mikrokontroler publikuje informację o dostępności urządzenia. Następnie subskrybuje następujące tematy:
MQTT_TOPIC_PREFIX/fanMode/set
MQTT_TOPIC_PREFIX/mode/set
MQTT_TOPIC_PREFIX/swingMode/set
MQTT_TOPIC_PREFIX/temp/set
I na koniec wysyła wiadomość konfiguracyjną, MQTT Discovery dla Home Assistanta. Wiadomość jest zapisana w formacie JSON i na następującą treść:
"name": AC_NAME,
"uniq_id": nameString + chipId,
"dev": {
"ids": nameString + chipId,
"name": AC_NAME,
"sw": "1.0",
"mdl": AC_MODEL,
"mf": AC_MANUF,
}
"avty_t": MQTT_ONLINE_TOPIC,
"curr_temp_t": MQTT_TOPIC_PREFIX + "/state",
"curr_temp_tpl": "{{ value_json.currTemp }}",
"fan_mode_stat_t": MQTT_TOPIC_PREFIX + "/state",
"fan_mode_stat_tpl": "{{ value_json.fanMode }}",
"fan_mode_cmd_t": MQTT_TOPIC_PREFIX + "/fanMode/set",
"max_temp": 30,
"min_temp": 16,
"mode_stat_t": MQTT_TOPIC_PREFIX + "/state",
"mode_stat_tpl": "{{ value_json.mode }}",
"mode_cmd_t": MQTT_TOPIC_PREFIX + "/mode/set",
"modes": ["off", "heat", "cool", "dry", "fan_only"],
"swing_mode_stat_t": MQTT_TOPIC_PREFIX + "/state",
"swing_mode_stat_tpl": "{{ value_json.swingMode }}",
"swing_modes": ["auto", center"],
"swing_mode_cmd_t": MQTT_TOPIC_PREFIX + "/swingMode/set",
"temp_stat_t": MQTT_TOPIC_PREFIX + "/state",
"temp_stat_tpl": "{{ value_json.temp }}",
"temp_cmd_t": MQTT_TOPIC_PREFIX + "/temp/set",
"temp_step": 1
Wiadomość jest wysyłana na temat "homeassistant/climate/" + nameString + chipId + "/config"
. Dla zapewnienia unikalności identyfikatorów użyłem funkcji ESP.getChipId()
, która zwraca część adresu MAC chipu ESP8266. Zmienna nameString
to AC_NAME
z usuniętymi spacjami.
Po zakończeniu tych zadań mikrokontroler przechodzi do oczekiwania na wiadomości od brokera oraz do okresowego pollowania klimatyzatora. Po otrzymaniu każdej wiadomości wywoływana jest funkcja MQTTcallback
, która analizuje wiadomości, oraz przekazuje odpowiednie polecenia do klasy obsługującej klimatyzator. Po każdym otrzymaniu aktualnego stanu od klimatyzatora mikrokontroler wysyła do brokera wiadomość w następującym formacie:
"currTemp": 23,
"fanMode": "auto",
"mode": "cool",
"swingMode": "auto",
"temp": 22