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

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

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

После года владения кондиционером 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 трансмиттер в свою домашнюю станцию по определению качества воздуха.

Мира!

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


  1. helloworld0
    02.09.2024 09:36

    Для этого кондиционера возможно реализовать управление через аналог брендового Wi-Fi модуля.
    Родной модуль работает только с китайскими облаками. Альтернативной прошивкой можно добиться управления через HA/MQTT.


    1. foxyrus Автор
      02.09.2024 09:36

      Я ж многократно написал в статье, мой кондиционер отлично управлялся(ется) через встроенный в него модуль wifi и интеграцию Tuya Local (без китайских облаков), обертку для него я тоже сам написал. Но API не предполагает отправку данных с внешнего сенсора температуры, только по IR можно.

      Скрытый текст
       в статье
      в статье


      1. helloworld0
        02.09.2024 09:36

        Почему бы в таком случае не управлять кондиционером по API на основе какого-либо внешнего датчика температуры?

        Есть ли преимущество у использования iFeel по сравнению с внешней автоматизацией?


        1. foxyrus Автор
          02.09.2024 09:36

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


  1. StarkIII
    02.09.2024 09:36

    У меня точно такой же кондиционер, и мне кажется что или он чересчур умный, или я глупый. Он у меня каждую ночь самопроизвольно включает функцию GENTLE WIND (мягкий обдув), при этом внутренние шторки поворачиваются на 90гр. и практически полностью перекрывают поток воздуха (в шторках есть отверстия, через них воздух все таки немного (мягко) просачивается). В таком режиме комната очень плохо охлаждается. И я не могу никак это поведение изменить. На пульте эта функция естественно выключена, к тому же если она и включена, то она режим включается моментально. Кондиционер подключен к интернету, пробовал и в смартфоне всё включать и выключать... Ничего не помогает. Может у кого есть какие идеи? Может его можно как-то сбросить к заводским настройкам?


    1. foxyrus Автор
      02.09.2024 09:36

      Да это умный режим, после того как в комнате темнеет (и вечернее время), включается этот режим. Нужно писать разработчикам.


      1. StarkIII
        02.09.2024 09:36

        И как вы с этим живете? Свет не выключаете? Или через HA можно этим управлять?


        1. foxyrus Автор
          02.09.2024 09:36

          дергаю повторно установку температуры и "повторно" включаю включенный кондиционер

          Скрытый текст
          alias: Включение кондиционера ночью
          description: ""
          trigger:
            - type: temperature
              platform: device
              device_id: b718c0f6b262ea8f33c5a4bf8d01b89c
              entity_id: 752121ecc078653fd46f100ba736c743
              domain: sensor
              above: 24
          condition:
            - condition: and
              conditions:
                - condition: time
                  after: "00:00:00"
                  before: "05:55:00"
                - condition: state
                  entity_id: binary_sensor.presence_sensor
                  state: "on"
          action:
            - action: light.turn_off
              metadata: {}
              data: {}
              target:
                entity_id: light.air_conditioner_display
            - device_id: b6c541864ffbdbbc25da3b9dfd309b0e
              domain: climate
              entity_id: 6634d8677c24063798122ef1142ef016
              type: set_hvac_mode
              hvac_mode: cool
            - type: turn_off
              device_id: b6c541864ffbdbbc25da3b9dfd309b0e
              entity_id: f58fb4bcb4a0b21fbdb8786c9037b1f2
              domain: switch
            - device_id: b6c541864ffbdbbc25da3b9dfd309b0e
              domain: select
              entity_id: 3642e78e16504801bc167f2ced6a902d
              type: select_option
              option: Leftmost
            - metadata: {}
              data:
                temperature: 25
                hvac_mode: cool
              target:
                device_id: b6c541864ffbdbbc25da3b9dfd309b0e
              action: climate.set_temperature
          mode: single


          1. StarkIII
            02.09.2024 09:36

            Похоже, что тоже придется НА настраивать. А то вручную приходится всю ночь дергать (

            И разработчикам надо написать!


            1. foxyrus Автор
              02.09.2024 09:36

              короче, убирает gentle wind выключение Adaptive display - китайцы к этой функции привязали спящий режим для кондиционера.


              1. StarkIII
                02.09.2024 09:36

                А где эта функция, я не могу её найти?


                1. foxyrus Автор
                  02.09.2024 09:36

                  в приложении tuya есть


                  1. StarkIII
                    02.09.2024 09:36

                    А что это за приложение? Есть Smart Life и SmartLife-SmartHome...


                    1. foxyrus Автор
                      02.09.2024 09:36

                      Всегда ею пользовался https://play.google.com/store/apps/details?id=com.tuya.smart&hl=ru

                      Это основное приложение умных устройств на китайской платформе Tuya

                      Скрытый текст


                      1. StarkIII
                        02.09.2024 09:36

                        Спасибо. Это приложение не доступно для моего устройства, но такую же кнопку я нашел в своём приложении, и даже вчера выключил функцию, но так крепко спал, что не смог проверить, сработало или нет)


    1. Sergey_12345
      02.09.2024 09:36
      +1

      1. Подсветить датчик освещенности

      2. Вынуть одну шторку.

        Ну а как иначе с умными девайсами?


      1. foxyrus Автор
        02.09.2024 09:36

        писать разработчикам


        1. StarkIII
          02.09.2024 09:36

          Я написал в поддержку, через форму на сайте, пока тихо.


    1. foxyrus Автор
      02.09.2024 09:36

      А через HA вы через что управляете кондиционером этим?


      1. StarkIII
        02.09.2024 09:36

        Через пуль ДУ и через SmartLife-SmartHome на телефоне.


        1. foxyrus Автор
          02.09.2024 09:36

          Вот думаю, сделать эксперимент: отвязать от приложения, сбросить, чтобы доступа к сети не было, не будет установлено время (внутренние часы), не сможет ночью включать этот режим.


          1. StarkIII
            02.09.2024 09:36

            Я его отлучал от сети, но часы не сбрасывал (они сбросятся если его обесточить?), не помогло.


            1. foxyrus Автор
              02.09.2024 09:36

              Тут наверно нужно делать полный сброс. чтоб как с завода и не подключать к приложению.

              Если просто отключить от wifi, внутренние часы скорее всего продолжат идти.