Pull to refresh

Делаем умный кондиционер еще умнее

Level of difficultyEasy
Reading time16 min
Views13K

или как сделать неумный умным.

Привет, Хабр!

Мой домашний кондиционер

После года владения кондиционером Royal Clima fresh full inverter (RCI-RF40HN), обнаружил на пульте кнопку I FEEL, полез читать инструкцию (пультом не пользуюсь, кондиционер управляется умным домом по wifi) и нашел, что в пульт встроен датчик температуры и по этому датчику кондиционер может регулировать более точнее температуру в помещении. Это то что мне не хватало: кондиционер висит в центре квартиры, в гостиной, а на ночь я направлял воздушный поток (через открытую дверь) в спальню.

ночью температура в спальне повышалась
ночью температура в спальне повышалась

Встроенный в корпус кондиционера датчик работает довольно точно, но вот ночью, он охлаждал сам себя намного быстрее и уменьшал холодный поток воздуха и "холод" не доходил до спальной комнаты.

Вариант уносить с собой в спальню пульт на ночь, не подходил, так как нет прямой видимости пульта и кондиционера.

Анализ протокола пульта

Для анализа протокола пульта решил воспользоваться библиотекой IRremoteESP8266, быстро собрал простейшую схему из ESP2866 и IR сенсора KY-022 (удобно, что у него есть встроенные светодиод, который сигнализирует о получении сигнала) или любой другой сенсор IR.

Здесь история как я сначала выбрал неудачную платку ESP32WROOM

Сначала залил прошивку анализатор IRrecvDumpV2.ino на ESP32 и начал анализировать сигналы от пульта, но получал лишь такие данные данные, в которых не определялся протокол:

E (1747402) gptimer: gptimer_start(348): timer is not enabled yet
E (1747408) gptimer: gptimer_start(348): timer is not enabled yet
E (1747414) gptimer: gptimer_start(348): timer is not enabled yet
E (1747420) gptimer: gptimer_start(348): timer is not enabled yet
E (1747426) gptimer: gptimer_start(348): timer is not enabled yet
E (1747432) gptimer: gptimer_start(348): timer is not enabled yet
E (1747437) gptimer: gptimer_start(348): timer is not enabled yet
;  // UNKNOWN EBA17F
Timestamp : 001747.491
Library   : v2.8.6

Protocol  : UNKNOWN
Code      : 0x5B39BFD5 (12 Bits)
uint16_t rawData[23] = {3058, 1620,  468, 5504,  5816, 5812,  5818, 5816,  5816, 5816,  5814, 5818,  5816, 5816,  5816, 5812,  5818, 5814,  5816, 5816,  5816, 5814,  5818};  // UNKNOWN 5B39BFD5

Потратил несколько часов разбираясь какие байты за что могут отвечать, потом загуглил, что же за ошибка gptimer_start и нашел пост, в котором писалось, что эта библиотека иногда может некорректно с платами серии ESP32 и тогда решил запустить анализатор на esp8266 и все сработало, протокол начал определятся.

Добавляем библиотеку IRremoteESP8266 в Arduino IDE, открываем файл IRrecvDumpV2.ino из примеров, прописываем в коде порт к которому подключили сенсор и заливаем на esp8266. Включаем монитор COM порта, нажимаем кнопки на пульте и видим данные, которые передает пульт.

18:22:35.544 -> Library   : v2.8.6
18:22:35.576 -> 
18:22:35.576 -> Protocol  : TCL112AC
18:22:35.576 -> Code      : 0x23CB260200402000830000000008 (112 Bits)
18:22:35.576 -> Mesg Desc.: Model: 1 (TAC09CHSD), Type: 2, Quiet: Off
18:22:35.576 -> uint16_t rawData[227] = {3062, 1620,  468, 1118,  470, 1116,  468, 350,  468, 350,  468, 352,  468, 1140,  446, 350,  468, 352,  468, 1140,  444, 1120,  464, 352,  468, 1120,  466, 352,  468, 350,  468, 1118,  468, 1120,  464, 352,  468, 1118,  468, 1140,  446, 350,  468, 350,  468, 1142,  444, 350,  468, 350,  468, 350,  468, 1140,  444, 352,  468, 350,  468, 350,  468, 350,  468, 352,  468, 350,  468, 350,  468, 350,  468, 352,  468, 350,  468, 352,  468, 352,  468, 352,  466, 352,  468, 352,  468, 350,  468, 350,  468, 352,  468, 352,  466, 352,  466, 1120,  468, 352,  468, 352,  466, 352,  468, 352,  468, 350,  468, 350,  468, 1142,  444, 350,  468, 352,  468, 352,  468, 350,  468, 350,  468, 350,  468, 352,  466, 352,  468, 350,  468, 350,  468, 1118,  468, 1142,  444, 350,  468, 350,  468, 352,  468, 350,  468, 350,  468, 1142,  444, 352,  466, 352,  468, 350,  468, 350,  468, 350,  468, 350,  468, 350,  468, 350,  468, 350,  468, 350,  468, 352,  468, 350,  468, 350,  468, 350,  468, 352,  468, 352,  466, 352,  466, 352,  468, 350,  468, 350,  468, 352,  468, 350,  468, 350,  468, 352,  468, 350,  468, 350,  468, 350,  468, 350,  468, 352,  468, 350,  468, 350,  468, 350,  468, 350,  468, 350,  468, 352,  468, 1120,  466, 350,  468, 350,  468, 352,  468, 350,  468};  // TCL112AC
18:22:35.675 -> uint8_t state[14] = {0x23, 0xCB, 0x26, 0x02, 0x00, 0x40, 0x20, 0x00, 0x83, 0x00, 0x00, 0x00, 0x00, 0x08};
18:22:35.707 -> 
18:22:35.707 -> 
18:22:35.740 -> Library   : v2.8.6
18:22:35.740 -> 
18:22:35.740 -> Protocol  : TCL112AC
18:22:35.772 -> Code      : 0x23CB26010024830500000019805A (112 Bits)
18:22:35.772 -> Mesg Desc.: Model: 1 (TAC09CHSD), Type: 1, Power: On, Mode: 3 (Cool), Temp: 26C, Fan: 0 (Auto), Swing(V): 0 (Auto), Swing(H): Off, Econo: Off, Health: Off, Turbo: Off, Light: On, On Timer: Off, Off Timer: Off
18:22:35.772 -> uint16_t rawData[227] = {3060, 1620,  468, 1142,  444, 1142,  444, 354,  466, 352,  468, 352,  468, 1118,  468, 350,  468, 352,  468, 1118,  468, 1142,  444, 352,  468, 1118,  468, 352,  468, 352,  468, 1142,  444, 1118,  468, 350,  468, 1142,  444, 1118,  466, 350,  468, 350,  468, 1118,  468, 350,  468, 352,  468, 1142,  444, 352,  466, 352,  468, 350,  468, 350,  468, 350,  468, 352,  468, 350,  468, 352,  468, 352,  468, 350,  468, 350,  468, 352,  468, 350,  468, 350,  468, 352,  468, 350,  468, 352,  468, 1118,  468, 350,  468, 352,  468, 1118,  468, 350,  468, 350,  468, 1116,  468, 1116,  470, 350,  468, 352,  468, 352,  468, 350,  468, 352,  468, 1142,  444, 1142,  444, 352,  468, 1142,  444, 350,  468, 350,  468, 352,  468, 352,  468, 350,  468, 350,  468, 352,  466, 352,  468, 350,  468, 350,  468, 350,  468, 350,  468, 352,  468, 350,  468, 350,  468, 350,  468, 350,  468, 350,  468, 350,  468, 350,  468, 352,  468, 352,  466, 352,  468, 350,  468, 350,  468, 352,  468, 352,  468, 350,  468, 352,  466, 1118,  468, 350,  468, 350,  468, 1118,  466, 1142,  444, 352,  468, 350,  468, 350,  468, 352,  468, 350,  466, 352,  468, 352,  468, 350,  468, 352,  468, 350,  468, 1142,  444, 352,  468, 1142,  444, 350,  468, 1142,  444, 1118,  468, 350,  468, 1142,  444, 352,  468};  // TCL112AC
18:22:35.905 -> uint8_t state[14] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x24, 0x83, 0x05, 0x00, 0x00, 0x00, 0x19, 0x80, 0x5A};

Кстати, как видно выше, при однократном нажатии на кнопку пульта моего кондиционера, отправляется последовательно два пакета данных, первый содержит лишь один статус - Тихий режим работы; непонятно зачем выделили целый пакет в 14 байт, ведь в основном пакете (во втором) полно "свободного места". И туже информацию с внешнего датчика (пульта) могли бы в отдельный пакет добавить.

Здесь мы узнаем, что пульт с кондиционером общаются по протоколу TCL112AC, но нажатие на кнопку IFEEL в логе не отображается, хотя сырые данные и меняются. Включил функцию IFEEL и начал ждать (здесь я все еще думал, что температура могла передаваться по Bluetooth) и ровно через 10 минут (и каждые 10 минут), в логе Arduiono появились новые пакеты данных.

Эксперимент с помещением в разную температуру
На столе около кондиционера
20:08:14.829 -> Protocol  : TCL112AC
20:08:14.829 -> Code      : 0x23CB260100048305000000148035 (112 Bits)
20:08:14.829 -> Mesg Desc.: Model: 1 (TAC09CHSD), Type: 1, Power: On, Mode: 3 (Cool), Temp: 26C, Fan: 0 (Auto), Swing(V): 0 (Auto), Swing(H): Off, Econo: Off, Health: Off, Turbo: Off, Light: On, On Timer: Off, Off Timer: Off
20:08:14.861 -> uint16_t rawData[227] = {3056, 1620,  468, 1118,  466, 1120,  466, 352,  466, 352,  466, 352,  466, 1120,  464, 352,  466, 352,  466, 1120,  464, 1142,  442, 352,  466, 1118,  466, 350,  466, 352,  466, 1120,  466, 1120,  466, 352,  466, 1118,  466, 1144,  442, 352,  466, 352,  466, 1120,  466, 352,  466, 352,  466, 1142,  442, 352,  466, 352,  466, 350,  466, 352,  466, 350,  468, 352,  466, 352,  466, 352,  466, 350,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 350,  466, 352,  466, 350,  466, 1118,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 1118,  466, 1120,  464, 352,  466, 352,  466, 352,  466, 352,  466, 352,  468, 1142,  442, 1120,  466, 352,  466, 1118,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 1142,  444, 352,  466, 1142,  442, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 1142,  442, 1142,  442, 350,  466, 1118,  466, 352,  466, 1144,  442, 1118,  466, 352,  466, 352,  466};  // TCL112AC
20:08:14.961 -> uint8_t state[14] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x04, 0x83, 0x05, 0x00, 0x00, 0x00, 0x14, 0x80, 0x35};

Холодильник
20:18:15.241 -> Protocol  : TCL112AC
20:18:15.241 -> Code      : 0x23CB2601000483050000000C802D (112 Bits)
20:18:15.241 -> Mesg Desc.: Model: 1 (TAC09CHSD), Type: 1, Power: On, Mode: 3 (Cool), Temp: 26C, Fan: 0 (Auto), Swing(V): 0 (Auto), Swing(H): Off, Econo: Off, Health: Off, Turbo: Off, Light: On, On Timer: Off, Off Timer: Off
20:18:15.275 -> uint16_t rawData[227] = {3054, 1622,  464, 1142,  442, 1118,  466, 352,  466, 352,  466, 352,  464, 1120,  464, 352,  466, 352,  466, 1118,  466, 1142,  442, 352,  466, 1120,  464, 352,  466, 352,  466, 1116,  468, 1140,  442, 352,  464, 1116,  468, 1142,  442, 350,  468, 350,  466, 1140,  442, 352,  466, 350,  466, 1118,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 350,  466, 352,  466, 352,  464, 352,  466, 352,  466, 352,  466, 1118,  466, 350,  466, 352,  466, 352,  466, 352,  466, 350,  466, 1118,  466, 1142,  442, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 1118,  466, 1140,  444, 352,  466, 1118,  464, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 350,  466, 352,  466, 350,  466, 352,  466, 352,  466, 352,  466, 352,  464, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 350,  466, 352,  466, 352,  466, 350,  466, 352,  466, 1142,  442, 1118,  464, 352,  464, 352,  466, 352,  466, 350,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  464, 1118,  466, 1142,  442, 352,  466, 1116,  468, 1118,  464, 352,  466, 1116,  466, 352,  464, 352,  466};  // TCL112AC
20:18:15.410 -> uint8_t state[14] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x04, 0x83, 0x05, 0x00, 0x00, 0x00, 0x0C, 0x80, 0x2D};


Держал в руках 2 минуты
20:38:16.040 -> Protocol  : TCL112AC
20:38:16.040 -> Code      : 0x23CB2601000483050000001D803E (112 Bits)
20:38:16.040 -> Mesg Desc.: Model: 1 (TAC09CHSD), Type: 1, Power: On, Mode: 3 (Cool), Temp: 26C, Fan: 0 (Auto), Swing(V): 0 (Auto), Swing(H): Off, Econo: Off, Health: Off, Turbo: Off, Light: On, On Timer: Off, Off Timer: Off
20:38:16.072 -> uint16_t rawData[227] = {3060, 1622,  466, 1142,  444, 1144,  442, 352,  468, 352,  466, 354,  466, 1144,  444, 352,  466, 352,  468, 1120,  468, 1116,  468, 352,  468, 1120,  466, 352,  466, 352,  468, 1120,  466, 1120,  466, 352,  466, 1122,  464, 1120,  466, 352,  468, 352,  466, 1120,  466, 352,  466, 352,  468, 1120,  466, 352,  468, 352,  466, 352,  466, 354,  466, 352,  468, 352,  466, 352,  466, 352,  466, 354,  466, 352,  466, 352,  466, 352,  466, 352,  468, 352,  466, 354,  466, 352,  466, 352,  466, 1144,  444, 352,  466, 354,  466, 352,  468, 352,  468, 352,  466, 1120,  466, 1144,  442, 352,  468, 352,  468, 352,  468, 352,  466, 352,  466, 1142,  444, 1142,  444, 352,  466, 1122,  466, 352,  466, 352,  468, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  468, 352,  468, 352,  466, 352,  466, 354,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  468, 352,  466, 352,  466, 354,  466, 352,  466, 1144,  444, 352,  468, 1144,  442, 1122,  466, 1120,  466, 352,  468, 352,  468, 352,  466, 352,  466, 352,  466, 354,  466, 352,  468, 352,  466, 352,  466, 352,  466, 1142,  444, 352,  466, 1144,  444, 1120,  466, 1144,  444, 1120,  464, 1120,  466, 352,  468, 352,  466};  // TCL112AC
20:38:16.208 -> uint8_t state[14] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x04, 0x83, 0x05, 0x00, 0x00, 0x00, 0x1D, 0x80, 0x3E};

Лежит на столе после рук
20:48:16.327 -> Protocol  : TCL112AC
20:48:16.327 -> Code      : 0x23CB2601000483050000001A803B (112 Bits)
20:48:16.327 -> Mesg Desc.: Model: 1 (TAC09CHSD), Type: 1, Power: On, Mode: 3 (Cool), Temp: 26C, Fan: 0 (Auto), Swing(V): 0 (Auto), Swing(H): Off, Econo: Off, Health: Off, Turbo: Off, Light: On, On Timer: Off, Off Timer: Off
20:48:16.358 -> uint16_t rawData[227] = {3058, 1622,  466, 1142,  444, 1120,  466, 356,  464, 352,  466, 356,  462, 1144,  442, 352,  466, 352,  466, 1120,  466, 1144,  442, 352,  466, 1122,  466, 352,  466, 352,  468, 1118,  468, 1120,  464, 352,  468, 1120,  468, 1118,  466, 352,  466, 352,  466, 1122,  464, 352,  466, 352,  466, 1120,  466, 352,  468, 352,  468, 352,  466, 352,  466, 352,  466, 352,  468, 352,  468, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 354,  466, 352,  466, 352,  466, 1144,  442, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 1120,  466, 1120,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  468, 1142,  444, 1118,  468, 352,  466, 1120,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  468, 352,  468, 352,  468, 350,  468, 352,  466, 352,  466, 352,  468, 352,  466, 352,  466, 352,  466, 352,  468, 352,  466, 352,  466, 352,  468, 352,  466, 352,  466, 352,  466, 352,  466, 352,  468, 352,  466, 352,  466, 352,  466, 352,  468, 352,  466, 352,  466, 1144,  442, 354,  466, 1120,  468, 1120,  466, 352,  466, 352,  466, 352,  466, 352,  468, 352,  466, 352,  466, 352,  468, 352,  466, 352,  466, 352,  466, 1142,  444, 1142,  444, 1144,  444, 352,  466, 1120,  466, 1120,  466, 1120,  466, 352,  466, 352,  468};  // TCL112AC
20:48:16.466 -> uint8_t state[14] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x04, 0x83, 0x05, 0x00, 0x00, 0x00, 0x1A, 0x80, 0x3B};

Вынес на улицу
21:08:16.888 -> Protocol  : TCL112AC
21:08:16.888 -> Code      : 0x23CB260100048305000000178038 (112 Bits)
21:08:16.921 -> Mesg Desc.: Model: 1 (TAC09CHSD), Type: 1, Power: On, Mode: 3 (Cool), Temp: 26C, Fan: 0 (Auto), Swing(V): 0 (Auto), Swing(H): Off, Econo: Off, Health: Off, Turbo: Off, Light: On, On Timer: Off, Off Timer: Off
21:08:16.921 -> uint16_t rawData[227] = {3058, 1620,  468, 1120,  466, 1120,  466, 352,  468, 352,  466, 352,  466, 1120,  466, 352,  466, 352,  468, 1120,  466, 1118,  468, 352,  466, 1118,  466, 352,  466, 352,  466, 1120,  466, 1118,  468, 352,  466, 1144,  442, 1120,  466, 352,  468, 352,  466, 1120,  466, 354,  466, 352,  466, 1142,  444, 352,  466, 352,  468, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 354,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 356,  464, 1120,  466, 352,  466, 352,  466, 352,  466, 352,  466, 354,  466, 1142,  444, 1142,  442, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 1120,  466, 1120,  466, 352,  466, 1142,  442, 352,  466, 354,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 352,  466, 1142,  442, 1144,  442, 1142,  444, 352,  468, 1142,  444, 352,  466, 352,  466, 352,  466, 352,  468, 350,  468, 352,  466, 352,  466, 352,  466, 352,  466, 352,  468, 1118,  466, 352,  466, 352,  466, 352,  466, 1142,  444, 1120,  466, 1118,  466, 352,  466, 352,  466};  // TCL112AC
21:08:17.052 -> uint8_t state[14] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x04, 0x83, 0x05, 0x00, 0x00, 0x00, 0x17, 0x80, 0x38};

Прокрутите текст правее, все интересное там

Я нашел изменяющиеся байты нужные мне, но пока еще не обнаруживающимися анализатором библиотеки IRremoteESP8266.

{0x23, 0xCB, 0x26, 0x01, 0x00, 0x24, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x80, 0xC1}

{0x23, 0xCB, 0x26, 0x01, 0x00, 0x04, 0x83, 0x05, 0x00, 0x00, 0x00, 0x14, 0x80, 0x35}

Меняющиеся байты это 11ый (считаем от нуля) байт 0x00 -> 0x14 и 6ой байт 0x03 -> 0x83.

11 байт подозрительно похож на температуру в 16ом исчисление и, действительно, если перевести все числа полученные в логе под спойлером выше, то получается температура окружающей среды (пульта).

Результаты конвертации

На столе около кондиционера 20°, Холодильник 12°, Держал в руках 2 минуты 29°, Лежит на столе после рук 26°, Вынес на улицу 23°

Идем в исходной код библиотеки и код протокола TCL и видим что 11 байт не используется совсем, а 6ый не учитывает кнопку IFEEL.

6ой (считаем от нуля) байт 0x03 это 0000 0011, а 0x83 это 1000 0011 этот бит не использовался в библиотеке.

Быстренько дописываем нужные нам новые параметры и отправляем PR разработчику.

Патченный код библиотеки
/// Native representation of a TCL 112 A/C message.
union Tcl112Protocol{
  uint8_t raw[kTcl112AcStateLength];  ///< The State in IR code form.
  struct {
    // Byte 0~2
    uint8_t                 :8;
    uint8_t                 :8;
    uint8_t                 :8;
    // Byte 3
    uint8_t MsgType         :2;
    uint8_t                 :6;
    // Byte 4
    uint8_t                 :8;
    // Byte 5
    uint8_t                 :2;
    uint8_t Power           :1;
    uint8_t OffTimerEnabled :1;
    uint8_t OnTimerEnabled  :1;
    uint8_t Quiet           :1;
    uint8_t Light           :1;
    uint8_t Econo           :1;
    // Byte 6
    uint8_t Mode            :4;
    uint8_t Health          :1;
    uint8_t Turbo           :1;
    uint8_t                 :1;
    uint8_t IFeel           :1;  // <---- изменил тут
    // Byte 7
    uint8_t Temp            :4;
    uint8_t                 :4;
    // Byte 8
    uint8_t Fan             :3;
    uint8_t SwingV          :3;
    uint8_t TimerIndicator  :1;
    uint8_t                 :1;
    // Byte 9
    uint8_t                 :1;  // 0
    uint8_t OffTimer        :6;
    uint8_t                 :1;  // 0
    // Byte 10
    uint8_t                 :1;  // 0
    uint8_t OnTimer         :6;
    uint8_t                 :1;  // 0
    // Byte 11
    uint8_t CurrentTemp     :8;  // <---- изменил тут
    // Byte 12
    uint8_t                 :3;
    uint8_t SwingH          :1;
    uint8_t                 :1;
    uint8_t HalfDegree      :1;
    uint8_t                 :1;
    uint8_t isTcl           :1;
    // Byte 13
    uint8_t Sum             :8;
  };
};

Плюс написал методы для получение новых статусов

и вывод в лог

Теперь заливаем еще раз прошивку, но с измененными библиотекой и в логе видим правильное отображение нажатия кнопки IFEEL и температуры помещения где находится пульт - IFeel: On, Sensor Temp: 23C

23:01:52.168 -> Code      : 0x23CB260100048306000000178039 (112 Bits)
23:01:52.168 -> Mesg Desc.: Model: 1 (TAC09CHSD), Type: 1, Power: On, Mode: 3 (Cool), Temp: 25C, Fan: 0 (Auto), Swing(V): 0 (Auto), Swing(H): Off, Econo: Off, Health: Off, Turbo: Off, Light: On, IFeel: On, Sensor Temp: 23C, On Timer: Off, Off Timer: Off
23:01:52.201 -> uint16_t rawData[227] = {3002, 1676,  412, 1176,  434, 1152,  408, 410,  408, 412,  410, 384,  434, 1174,  412, 408,  412, 408,  410, 1174,  412, 1178,  408, 408,  408, 1178,  410, 410,  412, 408,  410, 1174,  410, 1178,  408, 388,  432, 1176,  410, 1176,  410, 410,  410, 408,  410, 1176,  410, 408,  410, 410,  408, 1174,  412, 410,  410, 408,  410, 410,  410, 408,  410, 408,  412, 408,  410, 410,  408, 410,  408, 408,  410, 410,  408, 412,  410, 406,  412, 384,  436, 406,  412, 408,  408, 410,  410, 408,  412, 1174,  410, 408,  410, 434,  384, 412,  410, 406,  414, 406,  410, 1174,  412, 1176,  410, 408,  410, 408,  410, 410,  408, 410,  410, 408,  408, 1176,  410, 406,  412, 1176,  410, 1174,  414, 408,  410, 408,  410, 408,  410, 410,  410, 408,  412, 410,  408, 406,  412, 410,  410, 408,  410, 408,  410, 408,  412, 410,  408, 408,  410, 410,  408, 410,  408, 408,  410, 408,  410, 408,  410, 410,  410, 408,  384, 434,  412, 410,  410, 384,  434, 408,  410, 410,  410, 408,  412, 408,  410, 408,  412, 408,  410, 1176,  412, 1174,  410, 1176,  410, 410,  408, 1176,  412, 408,  410, 408,  410, 410,  410, 410,  406, 412,  408, 410,  408, 408,  412, 408,  412, 410,  410, 408,  410, 1176,  410, 1152,  436, 408,  410, 410,  410, 1176,  408, 1176,  410, 1174,  412, 408,  412, 408,  410};  // TEKNOPOINT
23:01:52.302 -> uint8_t state[14] = {0x23, 0xCB, 0x26, 0x01, 0x00, 0x04, 0x83, 0x06, 0x00, 0x00, 0x00, 0x17, 0x80, 0x39};

Вы наверно заметили, что менялся еще один байт 5ый (0x24 при нажатии и 0x04 при передачи температуры пультом), а конкретнее 3ий бит, в протоколе он отвечает за статус Quiet, и как можно догадаться, он отвечает за писк (подтверждение получения сигнала кондиционером) при нажатии на пульт и отсутствии звука при отправке температуры (иначе кондиционер пищал бы каждые 10 минут при включенной функции IFEEL).

Управление кондиционером и отправка температуры

После того как мы узнали протокол пульта (для управления кондиционером) и научились отправлять нужную температуру, необходимо сделать передатчик IR и настроить нужную автоматизацию для умного дома.

Схема опять таки очень простая: esp8266 и любой IR светодиод (я откусил от нонейм китайского пульта управления светодиодной ленты).

Умный дом у меня построен на Home Assistant, а DIY устройства используют прошивку и интеграцию ESPHome, но у него поддержка TCL протокола еще скуднее, а код намного сложнее чем у IRremoteESP8266, а управление происходит через сущность Climate для HA, а мне нужно было только отправлять температуру, а управление кондиционером, напомню, у меня уже настроено через интеграцию Tuya Local по wifi.

Если у вас другой кондиционер и не нужно отправлять спец данные (как у меня внешнюю температуре), то подойдет или эта интеграция IR Remote Climate, или эта Remote Transmitter, которая позволяет отправлять по IR даже сырые данные, которые вы получили на предыдущем шаге. Мне уж очень не хотелось собирать сырой пакет с нуля для отправки, тем более код в библиотеке IRremoteESP8266 уже был написан и работал.

Решил, написанный ранее код, подключить к ESPHome это позволяют сделать функционал Lambda - внедрение сишного кода прямо в yaml код прошивки.

Теперь перешиваем нашу платку esp8266 на прошивку "пустышку" ESPHome через web и добавляем его в HA.

Так как мой PR еще не приняли в основную ветку, придется залить модифицированную версию библиотеки IRremoteESP8266 в HA. Я скопировал его сюда в HomeAssistant /config/esphome/lib/IRremoteESP8266 и написал код для ESPHome, который берет данные с основной сущности Climate из HomeAssitant, устанавливает нужные флаги, температуру, режим работы и тд, обогащает данными с внешнего сенсора температуры (тоже из HA) и отправляет по IR с периодичностью (тут можно увеличить период обновления температуры с 10 минут, например, на 3).

Основной код прошивки отправки IR сигналов
esphome:
  name: 
  friendly_name: AC IR temp sender
  libraries:
    - IRremoteESP8266 # подключаем внешнюю библиотеку для работы с IR
  includes:  
    - lib/IRremoteESP8266/src/ir_Tcl.h # подменяем своей патченной локальной версией  

esp8266:
  board: d1_mini

# Enable logging
logger:

# Enable Home Assistant API
api:
  encryption:
    key: "HIDDEN"

ota:
  - platform: esphome
    password: "HIDDEN"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Test Fallback Hotspot"
    password: "HIDDEN"

captive_portal:


# Text sensor для получения режима работы
text_sensor:
  - platform: homeassistant
    entity_id: climate.air_conditioner_none  # сущность кондиционера
    id: ha_climate_mode  # Режим работы (cool, heat, fan, dry)
    attribute: hvac_mode  # Получаем режим работы
# Text sensor для получения скорости вентилятора
  - platform: homeassistant
    entity_id: climate.air_conditioner_none  # сущность кондиционера
    id: ha_climate_fan_mode  # Режим вентилятора
    attribute: fan_mode  # Получаем скорость вентилятора
# Switch для управления UV-стерилизацией
binary_sensor:
  - platform: homeassistant
    entity_id: switch.air_conditioner_uv_sterilization  # сущность выключатель UV стериализации
    id: ha_uv_sterilization  # Для управления функцией UV-стерилизации
# Switch для управления экраном
  - platform: homeassistant
    entity_id: light.air_conditioner_display  # сущность выключатель дисплея 
    id: ha_conditioner_display  # Для управления функцией UV-стерилизации    

# Получение данных с сенсора Home Assistant
sensor:
  - platform: homeassistant
    name: "Средняя температура в гостиной"
    id: temp_sensor1
    entity_id: sensor.sredniaia_temperatura_v_gostinnoi
  - platform: homeassistant
    name: "Средняя температура в спальне"
    id: temp_sensor2
    entity_id: sensor.sredniaia_temperatura_v_spalne
  - platform: homeassistant
    entity_id: climate.air_conditioner_none  # сущность кондиционера
    attribute: temperature  # Атрибут целевой температуры
    id: ha_climate_target_temperature  # Целевая температура  
  
# Select Интерфейс для выбора сенсора    
select:
  - platform: template
    icon: "mdi:snowflake-thermometer"
    name: "Внешний сенсор"
    id: select_temp_sensor
    options:
      - "Средняя температура в гостиной"
      - "Средняя температура в спальне"
    optimistic: True

button:
  - platform: template
    name: "Send TCL AC Cool Command"
    on_press:
      - lambda: |-
          #include <IRremoteESP8266.h>
          #include <IRsend.h>
          #include <ir_Tcl.h>
          const uint8_t first_packet[14] = 
                                      {0x23, 0xCB, 0x26, 0x02, 0x00, 
                                       0x40, 0x20, 0x00, 0x83, 0x00, 
                                       0x00, 0x00, 0x00, 0x08};
          uint8_t default_state[14] = {0x23, 0xCB, 0x26, 0x01, 0x00,
                                       0x04, 0x03, 0x07, 0x40, 0x00,
                                       0x00, 0x00, 0x80, 0x03};
          // Получаем значение температуры с выбранного сенсора
          float external_temperature = NAN;
          if (id(select_temp_sensor).state == "Средняя температура в гостиной") {
              external_temperature = id(temp_sensor1).state;
          } else if (id(select_temp_sensor).state == "Средняя температура в спальне") {
              external_temperature = id(temp_sensor2).state;
          }    

          ESP_LOGD("main", "Selected temperature: %.2f", external_temperature);
          // Ограничиваем температуру от внешнего датчика 0 до 35
          if (external_temperature < 1) {
            external_temperature = 0;
          } else if (external_temperature > 35) {
            external_temperature = 35;
          }
          // Получаем значение температуры целевой температуры с кондиционера          
          float set_temperature = id(ha_climate_target_temperature).state;
          // Получаем состояние UV-стерилизации (health mode)
          ESP_LOGD("main", "Starting IR transmission");
          bool uv_sterilization = id(ha_uv_sterilization).state;
          bool display = id(ha_conditioner_display).state;

          IRTcl112Ac ac(5); // GPIO5
          ac.begin();

          ac.setRaw(first_packet); // сначала устанавливаем первый пакет
          ac.send(0); // отправляем без повтора

          ac.setRaw(default_state); // устанавливаем дефалтовый режим кондиционера

          ac.setTemp(set_temperature);
          // Получаем режим работы
          std::string mode = id(ha_climate_mode).state;
          if (mode == "cool") {
            ac.setMode(kTcl112AcCool);
          } else if (mode == "heat") {
            ac.setMode(kTcl112AcHeat);
          } else if (mode == "fan_only") {
            ac.setMode(kTcl112AcFan);
          } else if (mode == "dry") {
            ac.setMode(kTcl112AcDry);
          }
          // Устанавливаем скорость вентилятора
          std::string fan_mode = id(ha_climate_fan_mode).state;
          if (fan_mode == "low") {
            ac.setFan(kTcl112AcFanLow);
          } else if (fan_mode == "medium") {
            ac.setFan(kTcl112AcFanMed);
          } else if (fan_mode == "high") {
            ac.setFan(kTcl112AcFanHigh);
          } else if (fan_mode == "auto") {
            ac.setFan(kTcl112AcFanAuto);
          }
          ac.setHealth(uv_sterilization);  // Устанавливаем значение Health
          ac.setLight(display);  // Устанавливаем значение включения экрана
          // ради этих двух строчек все и затевалось
          ac.setIFeel(true); // для передачи температы с внешнего датчика нужна нажатая кнопка IFEEL
          ac.setSensorTemp(static_cast<uint8_t>(external_temperature)); // температура с внешнего датчика, целое
          ac.send(0); // отправляем без повтора

Проверить работу IR светодиода можно через камеру телефона, при отправке сигнала, он будет еле светиться фиолетовым.

После заливки прошивки выше, получаем возможность управлять кондиционером отправляя его текущий статус плюс данные с внешнего температурного сенсора.

Автоматизация

Осталось написать автоматизацию для HomeAssistant, которая, в зависимости от времени суток, с заданным интервалом отправляла бы, температуру с внешнего датчика кондиционеру - днем среднюю температуру в гостиной, ночью среднюю температуру в спальне.

Автоматизация отправки температуры с разных датчиков
alias: Обновляем датчик температуры кондиционера
description: ""
trigger:
  - platform: time_pattern
    minutes: "/3"
condition:
  - condition: device
    device_id: b6c541864ffbdbbc25da3b9dfd309b0e
    domain: climate
    entity_id: 6634d8677c24063798122ef1142ef016
    type: is_hvac_mode
    hvac_mode: cool
action:
  - if:
      - condition: time
        after: "00:00:00"
        before: "06:00:00"
    then:
      - action: select.select_option
        metadata: {}
        data:
          option: Средняя температура в спальне
        target:
          entity_id: select.test
    else:
      - action: select.select_option
        metadata: {}
        data:
          option: Средняя температура в гостиной
        target:
          entity_id: select.test
  - action: button.press
    metadata: {}
    data: {}
    target:
      entity_id: button.test_send_tcl_ac_cool_command
mode: single

Спрятал в коробочку с домашней "погодной" станцией

Я добавил IR трансмиттер в свою домашнюю станцию по определению качества воздуха.

Мира!

Tags:
Hubs:
Total votes 8: ↑8 and ↓0+12
Comments32

Articles