Files
esphome_aux_ac_component/components/aux_ac/aux_ac.h
2022-05-26 23:17:40 +03:00

3194 lines
211 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Custom ESPHome component for AUX-based air conditioners
// Need some soldering skills
// Source code and detailed instructions are available on github: https://github.com/GrKoR/esphome_aux_ac_component
/// немного переработанная версия старого компонента
#pragma once
#include <Arduino.h>
#include "esphome.h"
#include <stdarg.h>
#include "esphome/core/component.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/helpers.h"
#if defined(ESP32)
#include "esphome/core/preferences.h"
#else
#warning "Saving presets does not work with ESP8266"
#endif
//#define HOLMS 9 // раскоментируй ключ для вывода лога под Эксель, значение ключа - размер пакетов которые будут видны
namespace esphome {
namespace aux_ac {
using climate::ClimatePreset;
using climate::ClimateTraits;
using climate::ClimateMode;
using climate::ClimateSwingMode;
using climate::ClimateFanMode;
class Constants {
public:
static const std::string AC_FIRMWARE_VERSION;
static const char *const TAG;
static const std::string MUTE;
static const std::string TURBO;
static const std::string CLEAN;
static const std::string HEALTH;
static const std::string ANTIFUNGUS;
/// минимальная и максимальная температура в градусах Цельсия, ограничения самого кондиционера
static const float AC_MIN_TEMPERATURE;
static const float AC_MAX_TEMPERATURE;
/// шаг изменения целевой температуры, градусы Цельсия
static const float AC_TEMPERATURE_STEP;
// периодичность опроса кондиционера на предмет изменения состояния
// изменение параметров с пульта не сообщается в UART, поэтому надо запрашивать состояние, чтобы быть в курсе
// значение в миллисекундах
static const uint32_t AC_STATES_REQUEST_INTERVAL;
};
const std::string Constants::AC_FIRMWARE_VERSION = "0.2.4";
const char *const Constants::TAG = "AirCon";
const std::string Constants::MUTE = "mute";
const std::string Constants::TURBO = "turbo";
const std::string Constants::CLEAN = "clean";
const std::string Constants::HEALTH = "health";
const std::string Constants::ANTIFUNGUS = "antifungus";
const float Constants::AC_MIN_TEMPERATURE = 16.0;
const float Constants::AC_MAX_TEMPERATURE = 32.0;
const float Constants::AC_TEMPERATURE_STEP = 0.5;
const uint32_t Constants::AC_STATES_REQUEST_INTERVAL = 7000;
class AirCon;
// состояния конечного автомата компонента
enum acsm_state : uint8_t {
ACSM_IDLE = 0, // ничего не делаем, ждем, на что бы среагировать
ACSM_RECEIVING_PACKET, // находимся в процессе получения пакета, никакие отправки в этом состоянии невозможны
ACSM_PARSING_PACKET, // разбираем полученный пакет
ACSM_SENDING_PACKET, // отправляем пакет сплиту
};
/**
* Кондиционер отправляет пакеты следующей структуры:
* HEADER: 8 bytes
* BODY: 0..24 bytes
* CRC: 2 bytes
* Весь пакет максимум 34 байта
* По крайней мере все встреченные мной пакеты имели такой размер и структуру.
**/
#define AC_HEADER_SIZE 8
#define AC_MAX_BODY_SIZE 24
#define AC_BUFFER_SIZE 35
/**
* таймаут загрузки пакета
*
* через такое количиство миллисекунд конечный автомат перейдет из состояния ACSM_RECEIVING_PACKET в ACSM_IDLE, если пакет не будет загружен
* расчетное время передачи 1 бита при скорости 4800 примерно 0,208 миллисекунд;
* 1 байт передается 11 битами (1 стартовый, 8 бит данных, 1 бит четности и 1 стоповый бит) или 2,30 мс.
* максимальный размер пакета AC_BUFFER_SIZE = 34 байта => 78,2 мсек. Плюс накладные расходы.
* Скорее всего на получение пакета должно хватать 100 мсек.
*
* По факту проверка показала:
* - если отрабатывать по 1 символу из UART на один вызов loop, то на 10 байт пинг-пакета требуется 166 мсек.
* То есть примерно по 16,6 мсек на байт. Примем 17 мсек.
* Значит на максимальный пакет потребуется 17*34 = 578 мсек. Примем 600 мсек.
* - если отрабатывать пакет целиком или хотя бы имеющимися в буфере UART кусками, то на 10 байт пинг-пакета требуется 27 мсек.
* То есть примерно по 2,7 мсек. на байт. Что близко к расчетным значениям. Примем 3 мсек.
* Значит на максимальный пакет потребуется 3*34 = 102 мсек. Примем 150 мсек.
* Опыт показал, что 150 мсек вполне хватает на большие пакеты
**/
#define AC_PACKET_TIMEOUT 150 // 150 мсек - отработка буфера UART за раз, 600 мсек - отработка буфера UART по 1 байту за вызов loop
// типы пакетов
#define AC_PTYPE_PING 0x01 // ping-пакет, рассылается кондиционером каждые 3 сек.; модуль на него отвечает
#define AC_PTYPE_CMD 0x06 // команда сплиту; модуль отправляет такие команды, когда что-то хочет от сплита
#define AC_PTYPE_INFO 0x07 // информационный пакет; бывает 3 видов; один из них рассылается кондиционером самостоятельно раз в 10 мин. и все 3 могут быть ответом на запросы модуля
#define AC_PTYPE_INIT 0x09 // инициирующий пакет; присылается сплитом, если кнопка HEALTH на пульте нажимается 8 раз; как там и что работает - не разбирался.
#define AC_PTYPE_UNKN 0x0b // какой-то странный пакет, отправляемый пультом при инициации и иногда при включении питания... как работает и зачем нужен - не разбирался, сплит на него вроде бы не реагирует
// типы команд
#define AC_CMD_STATUS_BIG 0x21 // большой пакет статуса кондиционера
#define AC_CMD_STATUS_SMALL 0x11 // маленький пакет статуса кондиционера
#define AC_CMD_STATUS_PERIODIC 0x2C // иногда встречается, сплит её рассылает по своему разумению; (вроде бы может быть и другой код! надо больше данных)
#define AC_CMD_SET_PARAMS 0x01 // команда установки параметров кондиционера
// значения байтов в пакетах
#define AC_PACKET_START_BYTE 0xBB // Стартовый байт любого пакета 0xBB, других не встречал
#define AC_PACKET_ANSWER 0x80 // признак ответа wifi-модуля
// заголовок пакета
struct packet_header_t {
uint8_t start_byte; // стартовый бит пакета, всегда 0xBB
uint8_t _unknown1; // не расшифрован
uint8_t packet_type; // тип пакета:
// 0x01 - пинг
// 0x06 - команда кондиционеру
// 0x07 - информационный пакет со статусом кондиционера
// 0x09 - (не разбирался) инициирование коннекта wifi-модуля с приложением на телефоне, с ESP работает и без этого
// 0x0b - (не разбирался) wifi-модуль так сигналит, когда не получает пинги от кондиционера и в каких-то еще случаях
uint8_t wifi; // признак пакета от wifi-модуля
// 0x80 - для всех сообщений, посылаемых модулем
// 0x00 - для всех сообщений, посылаемых кондиционером
uint8_t ping_answer_01; // не расшифрован, почти всегда 0x00, только в ответе на ping этот байт равен 0x01
uint8_t _unknown2; // не расшифрован
uint8_t body_length; // длина тела пакета в байтах
uint8_t _unknown3; // не расшифрован
};
// CRC пакета
union packet_crc_t {
uint16_t crc16;
uint8_t crc[2];
};
struct packet_t {
uint32_t msec; // значение millis в момент определения корректности пакета
packet_header_t * header;
packet_crc_t * crc;
uint8_t * body; // указатель на первый байт тела; можно приведением типов указателей обращаться к отдельным битам как к полям соответсвующей структуры
uint8_t bytesLoaded; //количество загруженных в пакет байт, включая CRC
uint8_t data[AC_BUFFER_SIZE];
};
// тело ответа на пинг
struct packet_ping_answer_body_t {
uint8_t byte_1C = 0x1C; // первый байт всегда 0x1C
uint8_t byte_27 = 0x27; // второй байт тела пинг-ответа всегда 0x27
uint8_t zero1 = 0; // всегда 0x00
uint8_t zero2 = 0; // всегда 0x00
uint8_t zero3 = 0; // всегда 0x00
uint8_t zero4 = 0; // всегда 0x00
uint8_t zero5 = 0; // всегда 0x00
uint8_t zero6 = 0; // всегда 0x00
};
// тело большого информационного пакета
struct packet_big_info_body_t {
uint8_t byte_01; // всегда 0x01
uint8_t cmd_answer; // код команды, ответом на которую пришел данный пакет (0x21);
// пакет может рассылаться и в дежурном режиме (без запроса со стороны wifi-модуля)
// в этом случае тут могут быть значения, отличные от 0x21
// БАЙТ2
uint8_t reserv20 :5; // не расшифрован, всегда 0xC0 11000000
bool is_invertor :1; // флаг инвертора
uint8_t reserv21 :2; // для RoyalClima18HNI: всегда 0xE0 11100000
// Brokly: для Energolux Bern: 0xE0; иногда, с равными промежутками во времени проскакивает )xE4
// предполагаю, что это байт конфига кондиционера (5 бит - инвертер), (2 бит - периодический мпульсный сигнал,период пимерно 500 сек)
// БАЙТ 3
bool power:1;
bool sleep:1;
bool louver_V:1;
uint8_t louver_H:2; // у шторок лево-право, почему то два бита
uint8_t mode:3; // enum { AC_BIG_MODE_AUTO = 0,
// AC_BIG_MODE_COOL = 1,
// AC_BIG_MODE_DRY = 2,
// AC_BIG_MODE_HEAT = 4,
// AC_BIG_MODE_FAN = 6}
//
//
// Встречались такие значения:
// 0x04 100 - сплит выключен, до этого работал (статус держится 1 час после выкл.)
// 0x05 101 - режим AUTO
// 0x24 100100 - режим OFF
// 0x25 100101 - режим COOL
// 0x39 111001 - ??
// 0x45 1000101 - режим DRY
// 0x85 10000101 - режим HEAT
// 0xC4 11000100 - режим OFF, выключен давно, зима
// 0xC5 11000101 - режим FAN
// Brokly:
// Встречались такие значения :
// 0x00 00000000 - OFF
// 0x01 00000001 - AUTO // режим авто, нет отдельного бита
// 0x41 1000001 - DRY
// 0x21 100001 - COOL
// 0x81 10000001 - HEAT
// 0x85 10000101 - HEAT+шторки верх-низ
// 0x99 10011001 - HEAT+шторки влево вправо
// 0xC1 11000001 - FAN // 7 и 6 бит связаны
// 0x80 10000000 - продувка после переключения из HEAT в OFF
// 0xC5 11000101 - FAN+шторки верх-низ
// 0xDD 11011101 - FAN+шторки лево-право/верх-низ
// 0xD9 11011001 - FAN+шторки лево-право
// 0xD8 11011000 - из FAN+шторки лево-право в OFF
// 0x39 111001 - COOL+шторки лево-право
// Очевидно битовые, но связные, поля, предположительные зависимости
// ВНИМАНИЕ : режимы номинальны, например в режиме АВТО нагрев или охлаждение не отображаются
// 7+6+5 4+3 2 1 0
// MODE Louv_L Louv_H SLEEP ON/OFF
//
// ФУНКЦМЯ CLEEN, HEALTH, ANTIFUNGUS на данный байт не влияют
//
// #define AC_BIG_MASK_MODE b11100000
// enum { AC_BIG_MODE_DRY = 0x40,
// AC_BIG_MODE_COOL = 0x20,
// AC_BIG_MODE_HEAT = 0x80,
// AC_BIG_MODE_FAN = 0xC0}
// #define AC_BIG_MASK_MODE b00011100
// enum { AC_BIG_LOUVERS_H = 0x04,
// AC_BIG_LOUVERS_L = 0x18,
// AC_BIG_LOUVERS_BOTH = 0x1C}
// #define AC_BIG_MASK_POWER b00000001
// #define AC_BIG_MASK_SLEEP b00000010
// #define AC_BIG_MASK_COOL b00100000
//
// БАЙТ 4
uint8_t reserv40:4; // 8
bool needDefrost:1; // 5 бит начало разморозки(накопление тепла)
bool defrostMode:1; // 6 бит режим разморозки внешнего блока (прогрев испарителя)
bool reserv41:1; // для RoyalClima18HNI: режим разморозки внешнего блока - 0x20, в других случаях 0x00
bool cleen:1; // 8 бит CLEAN
// Для кондея старт-стоп
// x xx
// C5 11000101
// C4 11000100
// 85 10000101
// 84 10000100
// 3D 00111101
// 3C 00111100
// 25 00100101
// 24 00100100
// 5 00000101
// 4 00000100
// БАЙТ5
uint8_t realFanSpeed:3; // Brokly: Energolux Bern - подтверждаю, ВАЖНО !!!! Это реальная скорость фена
uint8_t reserv30:5; // та которая в данный момент. Например может быть установлен нагрев со скоростью
// вентилятора HI, но кондей еще не произвел достаточно тепла, и крутит на LOW,
// Тут будет отображаться LOW
// fanSpeed: OFF=0x00, MUTE=0x01, LOW=0x02, MID=0x04, HIGH=0x06, TURBO=0x07
// в дежурных пакетах тут похоже что-то другое
// БАЙТ6
bool reserv60:1;
uint8_t fanPWM:7; // скорость шима вентилятора
// 126...128 - turbo
// 100...113 - hi
// 84...85 - mid
// 59...62 - low
// 0 - off
//
// БАЙТ7
uint8_t ambient_temperature_int; // Brokly: ПОДТВЕРЖДАЮ это точно показания датчика под крышкой внутреннего блока,
// физически доступен для пользователя
// целая часть комнатной температуры воздуха с датчика на внутреннем блоке сплит-системы
// перевод по формуле T = Тin - 0x20 + Tid/10
// где
// Tin - целая часть температуры
// Tid - десятичная часть температуры
// В ВЫКЛЮЧЕНОМ СОСТОНИИ занчение 7 8 9 10 равны !!!!!!
// А значит это термодатчики внутри внутреннего блока !!!!!!
// БАЙТ8
uint8_t zero3; // Brokly: полностью повторяет значение 9 байта
// БАЙТ9
uint8_t in_temperature_int; // Brokly: скорее всего это действительно какая то температура или дельта температур
// СКОРЕЕ ВСЕГО ЭТО ТЕМПЕРАТУРА ПОДАЧИ !!!!!! При охлаждении - холодная, при нагреве теплая
// холоднее или горячее температуры в команте. В выключеном состоянии стремится к комнатной темп.
// у меня на трех инверторных кондиционерах Energolux серии Bern
// в выключеном состоянии значение этого байта находится на уровне 57-68 (мощность 0%)
// зависит от мощности работы компрессора (измерения при 12гр на улице)
// в режиме охлаждения уменьшается и при мощности 47% = 40 / 73% = 38
// в режиме нагрева увеличивается и при мощности 47% = 70 / 73% = 75 / 84% = 84
// изменение этого значения более вялое, с западыванием относительно изменения мощности
// видимо является реакцией (следствием работы) на изменение мощности инвертора
// учитывая стиль записи температур имеет смысл рассматривать это значение как увеличенное на 0x20
// этот байт как-то связан с температурой во внешнем блоке. Требуются дополнительные исследования.
// При выключенном сплите характер изменения значения примерно соответствует изменению температуры на улице.
// При включенном сплите значение может очень сильно скакать.
// По схеме wiring diagram сплит-системы, во внешнем блоке есть термодатчик, отслеживающий температуру испарителя.
// Возможно, этот байт как раз и отражает изменение температуры на испарителе.
// Но я не смог разобраться, как именно перевести эти значения в градусы.
// Кроме того, зимой даже в минусовую температуру этот байт не уходит ниже 0x33 по крайней мере
// для температур в диапазоне -5..-10 градусов Цельсия.
// БАЙТ10
uint8_t zero4; // Brokly: полностью повторяет значение 9 байта
// БАЙТ11
uint8_t zero5; // всегда = 100 (0x64)
// БАЙТ12
uint8_t outdoor_temperature; // Brokly Energolux Bern: Внешняя температура формула T=БАЙТ12 - 0x20
// Датчик на радиаторе внешнего блока, доступен для пользователя, без разборки блока
// температура внешнего теплообменника влияет на это значение (при работе на обогрев - понижает, при охлаждении или при разморозке - повышает)
// для RoyalClima18HNI: похоже на какую-то температуру, точно неизвестно
// БАЙТ13
uint8_t out_temperature_int; // всегда 0x00
// для RoyalClima18HNI: 0x20
// Brokly Energolux Bern: похоже не какой то Термодатчик T=БАЙТ13 - 0x20
// При охлаждении растет, при нагреве падает, можно делать вывод о режиме COOL или HEAT
// ПОХОЖЕ НА ТЕМПЕРАТУРУ ОБРАТКИ !!!
// БАЙТ14
uint8_t strange_temperature_int;// всегда 0x00
// для RoyalClima18HNI: 0x20
// Brokly Energolux Bern: похоже не какой то Термодатчик T=БАЙТ14 - 0x20
// показания РАСТЕТ ПРИ ВКЛЮЧЕНИИ ИНВЕРТОРА, при выключении падают до комнатной
// от режима охлаждения или нагрева не зависит !!!
// БАЙТ15
uint8_t zero9; // всегда 0x00, Brokly: Energolux Bern, всегда 0x39 111001
// БАЙТ16
uint8_t invertor_power; // МОщность инвертера (Brokly: подтверждаю)
// для RoyalClima18HNI: мощность инвертора (от 0 до 100) в %
// например, разморозка внешнего блока происходит при 80%
// БАЙТ17
uint8_t zero11; // всегда 0x00
// Brokly: Energolux Bern : полное наложение на показания инвертора (от 0 до 22, когда инвертор отключен и тут 0
// при включении инвертора плавно растет, при выключении резко падает в 0, форма графика достаточно плавна
// БАЙТ18
uint8_t zero12; //
// Brokly: Energolux Bern : наложение на показания инвертора (от 144 до 174, когда инвертор отключен
// показания немного скачут в районе 149...154, при включении инвертора быстро растет, при выключении
// моментально падает до 149...154, бывают опускания ниже этих значений до 144, чаще в момент первоначального
// включения инвертора, а потом вверх, не всегда. При включении уходит в 0 на одну посылку
// БАЙТ19
uint8_t zero13; // Brokly: Energolux Bern : включение 144 -> 124 -> 110 далее все время держим 110
// БАЙТ20
uint8_t zero14; //
// Brokly: Energolux Bern : полное наложение на показания инвертора (от 0 до 45, когда инвертор отключен и тут 0
// при включении инвертора плавно растет, при выключении резко падает в 0, форма графика дрожащая нестабильная
// колебания в районе +-2...4 единицы
// БАЙТ21
uint8_t zero15; // всегда 0x00 Brokly: подтверждаю
// БАЙТ22
uint8_t zero16; // всегда 0x00 Brokly: подтверждаю
// БАЙТ23
uint8_t ambient_temperature_frac:4; // младшие 4 бита - дробная часть комнатной температуры воздуха с датчика на внутреннем блоке сплит-системы
// подробнее смотреть ambient_temperature_int
// для RoyalClima18HNI: старшие 4 бита - 0x2
uint8_t reserv023:4;
};
// тело малого информационного пакета
struct packet_small_info_body_t {
uint8_t byte_01; // не расшифрован, всегда 0x01
uint8_t cmd_answer; // код команды, ответом на которую пришел данный пакет (0x11);
// в пакетах сплита другие варианты не встречаются
// в отправляемых wifi-модулем пакетах тут может быть 0x01, если требуется установить режим работы
uint8_t target_temp_int_and_v_louver; // целая часть целевой температуры и положение вертикальных жалюзи
// три младших бита - положение вертикальных жалюзи
// если они все = 0, то вертикальный SWING включен
// если они все = 1, то выключен вертикальный SWING
// протокол универсильный, другие комбинации битов могут задавать какие-то положения
// вертикальных жалюзи, но у меня на пульте таких возможностей нет, надо экспериментировать.
// пять старших бит - целая часть целевой температуры
// температура определяется по формуле:
// 8 + (target_temp_int_and_v_louver >> 3) + (0.5 * (target_temp_frac >> 7))
uint8_t h_louver; // старшие 3 бита - положение горизонтальных жалюзи, остальное не изучено и всегда было 0
// если все 3 бита = 0, то горизонтальный SWING включен
// если все 3 бита = 1, то горизонтальный SWING отключен
// надо изучить другие комбинации
uint8_t target_temp_frac; // старший бит - дробная часть целевой температуры
// остальные биты до конца не изучены:
// бит 6 был всегда 0
// биты 0..5 растут на 1 каждую минуту, возможно внутренний таймер для включения/выключения по времени
uint8_t fan_speed; // три старших бита - скорость вентилятора, остальные биты не известны
// AUTO = 0xA0, LOW = 0x60, MEDIUM = 0x40, HIGH = 0x20
uint8_t fan_turbo_and_mute; // бит 7 = режим MUTE, бит 6 - режим TURBO; остальные не известны
// БФЙТ 7
uint8_t mode; // режим работы сплита:
// AUTO : bits[7, 6, 5] = [0, 0, 0]
// COOL : bits[7, 6, 5] = [0, 0, 1]
// DRY : bits[7, 6, 5] = [0, 1, 0]
// HEAT : bits[7, 6, 5] = [1, 0, 1]
// FAN : bits[7, 6, 5] = [1, 1, 1]
// Sleep function : bit 2 = 1
// iFeel function : bit 3 = 1
uint8_t zero1; // всегда 0x00
uint8_t zero2; // всегда 0x00
uint8_t status; // бит 5 = 1: включен, обычный режим работы (когда можно включить нагрев, охлаждение и т.п.)
// бит 2 = 1: режим самоочистки, должен запускаться только при бит 5 = 0
// бит 0 и бит 1: активация режима ионизатора воздуха (не проверен, у меня его нет)
uint8_t zero3; // всегда 0x00
uint8_t display_and_mildew; // бит4 = 1, чтобы погасить дисплей на внутреннем блоке сплита
// бит3 = 1, чтобы включить функцию "антиплесень" (после отключения как-то прогревает или просушивает теплообменник, чтобы на нем не росла плесень)
uint8_t zero4; // всегда 0x00
uint8_t target_temp_frac2; // дробная часть целевой температуры, может быть только 0x00 и 0x05
// при установке температуры тут 0x00, а заданная температура передается в target_temp_int_and_v_louver и target_temp_frac
// после установки сплит в информационных пакетах тут начинает показывать дробную часть
// не очень понятно, зачем так сделано
};
//****************************************************************************************************************************************************
//*************************************************** ПАРАМЕТРЫ РАБОТЫ КОНДИЦИОНЕРА ******************************************************************
//****************************************************************************************************************************************************
// для показаний о реальной скорости фена из большого пакета
enum ac_realFan : uint8_t { AC_REAL_FAN_OFF = 0x00, AC_REAL_FAN_MUTE = 0x01, AC_REAL_FAN_LOW = 0x02, AC_REAL_FAN_MID = 0x04,
AC_REAL_FAN_HIGH = 0x06, AC_REAL_FAN_TURBO = 0x07, AC_REAL_FAN_UNTOUCHED = 0xFF };
// для всех параметров ниже вариант X_UNTOUCHED = 0xFF означает, что этот параметр команды должен остаться тот, который уже установлен
// питание кондиционера
#define AC_POWER_MASK 0b00100000
enum ac_power : uint8_t { AC_POWER_OFF = 0x00, AC_POWER_ON = 0x20, AC_POWER_UNTOUCHED = 0xFF };
// режим очистки кондиционера, включается (или должен включаться) при AC_POWER_OFF
#define AC_CLEAN_MASK 0b00000100
enum ac_clean : uint8_t { AC_CLEAN_OFF = 0x00, AC_CLEAN_ON = 0x04, AC_CLEAN_UNTOUCHED = 0xFF };
// для включения ионизатора нужно установить второй бит в байте
// по результату этот бит останется установленным, но кондиционер еще и установит первый бит
#define AC_HEALTH_MASK 0b00000010
enum ac_health : uint8_t { AC_HEALTH_OFF = 0x00, AC_HEALTH_ON = 0x02, AC_HEALTH_UNTOUCHED = 0xFF };
// Статус ионизатора. Если бит поднят, то обнаружена ошибка ключения ионизатора
#define AC_HEALTH_STATUS_MASK 0b00000001
enum ac_health_status : uint8_t { AC_HEALTH_STATUS_OFF = 0x00, AC_HEALTH_STATUS_ON = 0x01, AC_HEALTH_STATUS_UNTOUCHED = 0xFF };
// целевая температура
#define AC_TEMP_TARGET_INT_PART_MASK 0b11111000
#define AC_TEMP_TARGET_FRAC_PART_MASK 0b10000000
// задержка отключения кондиционера
#define AC_TIMER_MINUTES_MASK 0b00111111
#define AC_TIMER_HOURS_MASK 0b00011111
// включение таймера сна
#define AC_TIMER_MASK 0b01000000
enum ac_timer : uint8_t {AC_TIMER_OFF = 0x00, AC_TIMER_ON = 0x40, AC_TIMER_UNTOUCHED = 0xFF};
// основные режимы работы кондиционера
#define AC_MODE_MASK 0b11100000
enum ac_mode : uint8_t { AC_MODE_AUTO = 0x00, AC_MODE_COOL = 0x20, AC_MODE_DRY = 0x40, AC_MODE_HEAT = 0x80, AC_MODE_FAN = 0xC0, AC_MODE_UNTOUCHED = 0xFF };
// Ночной режим (SLEEP). Комбинируется только с режимами COOL и HEAT. Автоматически выключается через 7 часов.
// COOL: температура +1 градус через час, еще через час дополнительные +1 градус, дальше не меняется.
// HEAT: температура -2 градуса через час, еще через час дополнительные -2 градуса, дальше не меняется.
// Восстанавливается ли температура через 7 часов при отключении режима - не понятно.
#define AC_SLEEP_MASK 0b00000100
enum ac_sleep : uint8_t { AC_SLEEP_OFF = 0x00, AC_SLEEP_ON = 0x04, AC_SLEEP_UNTOUCHED = 0xFF };
// функция iFeel - поддерживате температуру по датчику в пульте ДУ, а не во внутреннем блоке кондиционера
#define AC_IFEEL_MASK 0b00001000
enum ac_ifeel : uint8_t { AC_IFEEL_OFF = 0x00, AC_IFEEL_ON = 0x08, AC_IFEEL_UNTOUCHED = 0xFF };
// Вертикальные жалюзи. В протоколе зашита возможность двигать ими по всякому, но додлжна быть такая возможность на уровне железа.
// ToDo: надо протестировать значения 0x01, 0x02, 0x03, 0x04, 0x05, 0x06 для ac_louver_V
#define AC_LOUVERV_MASK 0b00000111
enum ac_louver_V : uint8_t { AC_LOUVERV_SWING_UPDOWN = 0x00, AC_LOUVERV_OFF = 0x07, AC_LOUVERV_UNTOUCHED = 0xFF };
// Горизонтальные жалюзи. В протоколе зашита возможность двигать ими по всякому, но додлжна быть такая возможность на уровне железа.
// ToDo: надо протестировать значения 0x20, 0x40, 0x60, 0x80, 0xA0, 0xC0 для ac_louver_H
#define AC_LOUVERH_MASK 0b11100000
enum ac_louver_H : uint8_t { AC_LOUVERH_SWING_LEFTRIGHT = 0x00, AC_LOUVERH_OFF = 0xE0, AC_LOUVERH_UNTOUCHED = 0xFF };
struct ac_louver {
ac_louver_H louver_h;
ac_louver_V louver_v;
};
// скорость вентилятора
#define AC_FANSPEED_MASK 0b11100000
enum ac_fanspeed : uint8_t { AC_FANSPEED_HIGH = 0x20, AC_FANSPEED_MEDIUM = 0x40, AC_FANSPEED_LOW = 0x60, AC_FANSPEED_AUTO = 0xA0, AC_FANSPEED_UNTOUCHED = 0xFF };
// TURBO работает только в режимах COOL и HEAT
#define AC_FANTURBO_MASK 0b01000000
enum ac_fanturbo : uint8_t { AC_FANTURBO_OFF = 0x00, AC_FANTURBO_ON = 0x40, AC_FANTURBO_UNTOUCHED = 0xFF };
// MUTE работает только в режиме FAN. В режиме COOL кондей команду принимает, но MUTE не устанавливается
#define AC_FANMUTE_MASK 0b10000000
enum ac_fanmute : uint8_t { AC_FANMUTE_OFF = 0x00, AC_FANMUTE_ON = 0x80, AC_FANMUTE_UNTOUCHED = 0xFF };
// включение-выключение дисплея на корпусе внутреннего блока
#define AC_DISPLAY_MASK 0b00010000
enum ac_display : uint8_t { AC_DISPLAY_OFF = 0x00, AC_DISPLAY_ON = 0x10, AC_DISPLAY_UNTOUCHED = 0xFF };
// включение-выключение функции "Антиплесень".
// По факту: после выключения сплита он оставляет минут на 5 открытые жалюзи и глушит вентилятор. Уличный блок при этом гудит и тарахтит.
// Возможно, прогревается теплообменник для высыхания. Через некоторое время внешний блок замолкает и сплит закрывает жалюзи.
#define AC_MILDEW_MASK 0b00001000
enum ac_mildew : uint8_t { AC_MILDEW_OFF = 0x00, AC_MILDEW_ON = 0x08, AC_MILDEW_UNTOUCHED = 0xFF };
// маска счетчика минут прошедших с последней команды
#define AC_MIN_COUTER 0b00111111 //
// настройка усреднения фильтра температуры. Это значение - взнос нового измерения
// в усредненные показания в процентах
#define OUTDOOR_FILTER_PESCENT 0.2
/** команда для кондиционера
*
* ВАЖНО! В коде используется копирование команд простым присваиванием.
* Если в структуру будут введены указатели, то копирование надо будет изменить!
*/
// данные структур содержат настройку, специально вынес в макрос
#define AC_COMMAND_BASE float temp_target;\
ac_power power;\
ac_clean clean;\
ac_health health;\
ac_mode mode;\
ac_sleep sleep;\
ac_louver louver;\
ac_fanspeed fanSpeed;\
ac_fanturbo fanTurbo;\
ac_fanmute fanMute;\
ac_display display;\
ac_mildew mildew;\
ac_timer timer;\
uint8_t timer_hours;\
uint8_t timer_minutes;\
bool temp_target_matter
// чистый размер этой структуры 20 байт, скорее всего из-за выравнивания, она будет больше
// из-за такого приема нужно контролировать размер копируемых данных руками
#define AC_COMMAND_BASE_SIZE 20
struct ac_command_t {
/*
ac_power power;
ac_clean clean;
ac_health health; // включение ионизатора
ac_mode mode;
ac_sleep sleep;
ac_louver louver;
ac_fanspeed fanSpeed;
ac_fanturbo fanTurbo;
ac_fanmute fanMute;
ac_display display;
ac_mildew mildew;
ac_timer timer;
uint8_t timer_hours;
uint8_t timer_minutes;
float temp_target;
bool temp_target_matter; // показывает, задана ли температура. Если false, то оставляем уже установленную
*/
AC_COMMAND_BASE;
ac_health_status health_status;
float temp_ambient; // внутренняя температура
int8_t temp_outdoor; // внешняя температура
int8_t temp_inbound; // температура входящая
int8_t temp_outbound; // температура исходящая
int8_t temp_strange; // непонятная температура, понаблюдаем
ac_realFan realFanSpeed; // текущая скорость вентилятора
uint8_t invertor_power; // мощность инвертора
uint8_t pressure; // предположительно давление
bool defrost; // режим разморозки внешнего блока (накопление тепла + прогрев испарителя)
ac_ifeel iFeel;
};
// структура для сохранения данных
struct ac_save_command_t {
AC_COMMAND_BASE;
};
// номера сохранений пресетов
enum store_pos : uint8_t {
POS_MODE_AUTO = 0,
POS_MODE_COOL,
POS_MODE_DRY,
POS_MODE_HEAT,
POS_MODE_FAN,
POS_MODE_OFF};
typedef ac_command_t ac_state_t; // текущее состояние параметров кондея можно хранить в таком же формате, как и комманды
//****************************************************************************************************************************************************
//************************************************ КОНЕЦ ПАРАМЕТРОВ РАБОТЫ КОНДИЦИОНЕРА **************************************************************
//****************************************************************************************************************************************************
/*****************************************************************************************************************************************************
* структуры и типы для последовательности команд
*****************************************************************************************************************************************************
*
* Последовательность команд позволяет выполнить несколько последовательных команд с контролем получаемых в ответ пакетов.
* Если требуется, в получаемых в ответ пакетах можно контролировать значение любых байт.
* Для входящего пакета байт, значение которого не проверяется, должен быть установлен в AC_SEQUENCE_ANY_BYTE.
* Контроль возможен только для входящих пакетов, исходящие отправляются "как есть".
*
* Для исходящих пакетов значения CRC могут не рассчитываться, контрольная сумма будет рассчитана автоматически.
* Для входящих пакетов значение CRC также можно не рассчитывать, установив байты CRC в AC_SEQUENCE_ANY_BYTE,
* так как контроль CRC для получаемых пакетов выполняется автоматически при получении.
*
* Для входящих пакетов в последовательности можно указать таймаут. Если таймаут равен 0, то используется значение AC_SEQUENCE_DEFAULT_TIMEOUT.
* Если в течение указанного времени подходящий пакет не будет получен, то последовательность прерывается с ошибкой.
* Пинг-пакеты в последовательности игнорируются.
*
* Пауза в последовательности задается значением timeout элемента AC_DELAY. Никакие другие параметры такого элемента можно не заполнять.
*
**/
// максимальная длина последовательности; больше вроде бы не требовалось
#define AC_SEQUENCE_MAX_LEN 0x0F
// в пакетах никогда не встречалось значение 0xFF (только в CRC), поэтому решено его использовать как признак не важного значение байта
//#define AC_SEQUENCE_ANY_BYTE 0xFF
// дефолтный таймаут входящего пакета в миллисекундах
// если для входящего пакета в последовательности указан таймаут 0, то используется значение по-умолчанию
// если нужный пакет не поступил в течение указанного времени, то последовательность прерывается с ошибкой
// Brokly: пришлось увеличить
#define AC_SEQUENCE_DEFAULT_TIMEOUT 580
enum sequence_item_type_t : uint8_t {
AC_SIT_NONE = 0x00, // пустой элемент последовательности
AC_SIT_DELAY = 0x01, // пауза в последовательности на нужное количество миллисекунд
AC_SIT_FUNC = 0x02 // рабочий элемент последовательности
};
// тип пакета в массиве последовательности
// информирует о том, что за пакет лежит в поле packet элемента последовательности
enum sequence_packet_type_t : uint8_t {
AC_SPT_CLEAR = 0x00, // пустой пакет
AC_SPT_RECEIVED_PACKET = 0x01, // полученный пакет
AC_SPT_SENT_PACKET = 0x02 // отправленный пакет
};
/** элемент последовательности
* Поля item_type, func, timeout и cmd устанавливаются ручками и задают параметры выполнения шага последовательности.
* Поля msec, packet_type и packet заполняются движком при обработке последовательности.
**/
struct sequence_item_t {
sequence_item_type_t item_type; // тип элемента последовательности
bool (AirCon::*func)(); // указатель на функцию, отрабатывающую шаг последовательности
uint16_t timeout; // допустимый таймаут в ожидании пакета (применим только для входящих пакетов)
ac_command_t cmd; // новое состояние сплита, нужно для передачи кондиционеру команд
//******* поля ниже заполняются функциями обработки последовательности ***********
uint32_t msec; // время старта текущего шага последовательности (для входящего пакета и паузы)
sequence_packet_type_t packet_type; // тип пакета (входящий, исходящий или вовсе не пакет)
packet_t packet; // данные пакета
};
/*****************************************************************************************************************************************************/
class AirCon : public esphome::Component, public esphome::climate::Climate {
private:
// массив для сохранения данных глобальных персетов
ac_save_command_t global_presets[POS_MODE_OFF+1];
#if defined(ESP32)
// тут будем хранить данные глобальных пресетов во флеше
// ВНИМАНИЕ на данный момент 22.05.22 ESPHOME 20022.5.0 имеет ошибку
// траблтикет: https://github.com/esphome/issues/issues/3298
// из-за этого сохранение в энергонезависимую память не работает !!!
ESPPreferenceObject storage = global_preferences->make_preference<ac_save_command_t[POS_MODE_OFF+1]>(this->get_object_id_hash(), true);
#endif
// настройка-ключ, для включения сохранения - восстановления настроек каждого
// режима работы в отдельности, то есть каждый режим работы имеет свои настройки
// температуры, шторок, скорости вентилятора, пресетов
bool _store_settings = false;
// флаги для сохранения пресетов
bool _new_command_set = false; // флаг отправки новой команды, необходимо сохранить данные пресета, если разрешено
// время последнего запроса статуса у кондея
uint32_t _dataMillis;
// периодичность обновления статуса кондея, по дефолту AC_STATES_REQUEST_INTERVAL
uint32_t _update_period = Constants::AC_STATES_REQUEST_INTERVAL;
// надо ли отображать текущий режим работы внешнего блока
// в режиме нагрева, например, кондиционер может как греть воздух, так и работать в режиме вентилятора, если целевая темпреатура достигнута
// по дефолту показываем
bool _show_action = true;
// как отрабатывается включание-выключение дисплея.
// если тут false, то 1 в соответствующем бите включает дисплей, а 0 выключает.
// если тут true, то 1 потушит дисплей, а 0 включит.
bool _display_inverted = false;
// флаг типа кондиционера инвертор - true, ON/OFF - false, начальная установка false
// в таком режиме точность и скорость определения реального состояния системы для инвертора,
// будет работать, но будет ниже, переменная устанавливается при первом получении большого пакета;
// если эта переменная установлена, то режим работы не инверторного кондиционера будет распознаваться
// как "в простое" (IDLE)
bool _is_invertor = false;
// поддерживаемые кондиционером опции
std::set<ClimateMode> _supported_modes{};
std::set<ClimateSwingMode> _supported_swing_modes{};
std::set<ClimatePreset> _supported_presets{};
std::set<std::string> _supported_custom_presets{};
std::set<std::string> _supported_custom_fan_modes{};
// состояние конечного автомата
acsm_state _ac_state = ACSM_IDLE;
// текущее состояние задаваемых пользователем параметров системы
ac_state_t _current_ac_state;
// флаг подключения к UART
bool _hw_initialized = false;
// указатель на UART, по которому общаемся с кондиционером
esphome::uart::UARTComponent *_ac_serial;
// UART wrappers: peek
int peek() {
uint8_t data;
if (!_ac_serial->peek_byte(&data)) return -1;
return data;
}
// UART wrappers: read
int read() {
uint8_t data;
if (!_ac_serial->read_byte(&data)) return -1;
return data;
}
// флаг обмена пакетами с кондиционером (если проходят пинги, значит есть коннект)
bool _has_connection = false;
// входящий и исходящий пакеты
packet_t _inPacket;
packet_t _outPacket;
// пакет для тестирования всякой фигни
packet_t _outTestPacket;
// последовательность пакетов текущий шаг в последовательности
sequence_item_t _sequence[AC_SEQUENCE_MAX_LEN];
uint8_t _sequence_current_step;
// флаг успешного выполнения стартовой последовательности команд
bool _startupSequenceComlete = false;
// очистка последовательности команд
void _clearSequence(){
for (uint8_t i = 0; i < AC_SEQUENCE_MAX_LEN; i++) {
_sequence[i].item_type = AC_SIT_NONE;
_sequence[i].func = nullptr;
_sequence[i].timeout = 0;
_sequence[i].msec = 0;
_sequence[i].packet_type = AC_SPT_CLEAR;
_clearPacket(&_sequence[i].packet);
_clearCommand(&_sequence[i].cmd);
}
_sequence_current_step = 0;
}
// проверяет, есть ли свободные шаги в последовательности команд
bool _hasFreeSequenceStep(){
return (_getNextFreeSequenceStep() < AC_SEQUENCE_MAX_LEN);
}
// возвращает индекс первого пустого шага последовательности команд
uint8_t _getNextFreeSequenceStep(){
for (size_t i = 0; i < AC_SEQUENCE_MAX_LEN; i++) {
if (_sequence[i].item_type == AC_SIT_NONE){
return i;
}
}
// если свободных слотов нет, то возвращаем значение за пределом диапазона
return AC_SEQUENCE_MAX_LEN;
}
// возвращает количество свободных шагов в последовательности
uint8_t _getFreeSequenceSpace() {
return (AC_SEQUENCE_MAX_LEN - _getNextFreeSequenceStep());
}
// добавляет шаг в последовательность команд
// возвращает false, если не нашлось места для шага
bool _addSequenceStep(const sequence_item_type_t item_type, bool (AirCon::*func)() = nullptr, ac_command_t *cmd = nullptr, uint16_t timeout = AC_SEQUENCE_DEFAULT_TIMEOUT){
if (!_hasFreeSequenceStep()) return false; // если места нет, то уходим
if (item_type == AC_SIT_NONE) return false; // глупость какая-то, уходим
if ((item_type == AC_SIT_FUNC) && (func == nullptr)) return false; // должна быть передана функция для такого типа шага
if ((item_type != AC_SIT_DELAY) && (item_type != AC_SIT_FUNC)){
// какой-то неизвестный тип
_debugMsg(F("_addSequenceStep: unknown sequence item type = %u"), ESPHOME_LOG_LEVEL_DEBUG, __LINE__, item_type);
return false;
}
uint8_t step = _getNextFreeSequenceStep();
_sequence[step].item_type = item_type;
// если задержка нулевая, то присваиваем дефолтную задержку
if (timeout == 0) timeout = AC_SEQUENCE_DEFAULT_TIMEOUT;
_sequence[step].timeout = timeout;
_sequence[step].func = func;
if (cmd != nullptr) _sequence[step].cmd = *cmd; // так как в структуре команды только простые типы, то можно вот так присваивать
return true;
}
// добавляет в последовательность шаг с задержкой
bool _addSequenceDelayStep(uint16_t timeout){
return this->_addSequenceStep(AC_SIT_DELAY, nullptr, nullptr, timeout);
}
// добавляет в последовательность функциональный шаг
bool _addSequenceFuncStep(bool (AirCon::*func)(), ac_command_t *cmd = nullptr, uint16_t timeout = AC_SEQUENCE_DEFAULT_TIMEOUT){
return this->_addSequenceStep(AC_SIT_FUNC, func, cmd, timeout);
}
// выполняет всю логику очередного шага последовательности команд
void _doSequence(){
if (!hasSequence()) return;
// если шаг уже максимальный из возможных
if (_sequence_current_step >= AC_SEQUENCE_MAX_LEN) {
// значит последовательность закончилась, надо её очистить
// при очистке последовательности будет и _sequence_current_step обнулён
_debugMsg(F("Sequence [step %u]: maximum step reached"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step);
_clearSequence();
return;
}
// смотрим тип текущего элемента в последовательности
switch (_sequence[_sequence_current_step].item_type) {
case AC_SIT_FUNC: {
// если указатель на функцию пустой, то прерываем последовательность
if (_sequence[_sequence_current_step].func == nullptr) {
_debugMsg(F("Sequence [step %u]: function pointer is NULL, sequence broken"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step);
_clearSequence();
return;
}
// сохраняем время начала паузы
if (_sequence[_sequence_current_step].msec == 0) {
_sequence[_sequence_current_step].msec = millis();
_debugMsg(F("Sequence [step %u]: step started"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step);
}
// если таймаут не указан, берем значение по-умолчанию
if (_sequence[_sequence_current_step].timeout == 0 ) _sequence[_sequence_current_step].timeout = AC_SEQUENCE_DEFAULT_TIMEOUT;
// если время вышло, то отчитываемся в лог и очищаем последовательность
if (millis() - _sequence[_sequence_current_step].msec >= _sequence[_sequence_current_step].timeout) {
_debugMsg(F("Sequence [step %u]: step timed out (it took %u ms instead of %u ms)"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step, millis() - _sequence[_sequence_current_step].msec, _sequence[_sequence_current_step].timeout);
_clearSequence();
return;
}
// можно вызывать функцию
// она самомтоятельно загружает отправляемые/полученные пакеты в packet последовательности
// а также самостоятельно увеличивает счетчик шагов последовательности _sequence_current_step
// единственное исключение - таймауты
if (!(this->*_sequence[_sequence_current_step].func)()) {
_debugMsg(F("Sequence [step %u]: error was occur in step function"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step, millis() - _sequence[_sequence_current_step].msec);
_clearSequence();
return;
}
break;
}
case AC_SIT_DELAY: { // это пауза в последовательности
// пауза задается параметром timeout элемента последовательности
// начало паузы сохраняется в параметре msec
// сохраняем время начала паузы
if (_sequence[_sequence_current_step].msec == 0) {
_sequence[_sequence_current_step].msec = millis();
_debugMsg(F("Sequence [step %u]: begin delay (%u ms)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step, _sequence[_sequence_current_step].timeout);
}
// если время вышло, то переходим на следующий шаг
if (millis() - _sequence[_sequence_current_step].msec >= _sequence[_sequence_current_step].timeout) {
_debugMsg(F("Sequence [step %u]: delay culminated (plan = %u ms, fact = %u ms)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step, _sequence[_sequence_current_step].timeout, millis() - _sequence[_sequence_current_step].msec);
_sequence_current_step++;
}
break;
}
case AC_SIT_NONE: // шаги закончились
default: // или какой-то мусор в последовательности
// надо очистить последовательность и уходить
_debugMsg(F("Sequence [step %u]: sequence complete"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step);
_clearSequence();
break;
}
}
// заполняет структуру команды нейтральными значениями
void _clearCommand(ac_command_t * cmd){
cmd->clean = AC_CLEAN_UNTOUCHED;
cmd->display = AC_DISPLAY_UNTOUCHED;
cmd->fanMute = AC_FANMUTE_UNTOUCHED;
cmd->fanSpeed = AC_FANSPEED_UNTOUCHED;
cmd->fanTurbo = AC_FANTURBO_UNTOUCHED;
cmd->health = AC_HEALTH_UNTOUCHED;
cmd->health_status = AC_HEALTH_STATUS_UNTOUCHED;
cmd->iFeel = AC_IFEEL_UNTOUCHED;
cmd->louver.louver_h = AC_LOUVERH_UNTOUCHED;
cmd->louver.louver_v = AC_LOUVERV_UNTOUCHED;
cmd->mildew = AC_MILDEW_UNTOUCHED;
cmd->mode = AC_MODE_UNTOUCHED;
cmd->power = AC_POWER_UNTOUCHED;
cmd->sleep = AC_SLEEP_UNTOUCHED;
cmd->timer = AC_TIMER_UNTOUCHED;
cmd->timer_hours = 0;
cmd->timer_minutes = 0;
cmd->temp_target = 0;
cmd->temp_target_matter = false;
cmd->temp_ambient = 0;
cmd->temp_outdoor = 0;
cmd->temp_inbound = 0;
cmd->temp_outbound = 0;
cmd->temp_strange = 0;
cmd->realFanSpeed = AC_REAL_FAN_UNTOUCHED;
};
// очистка буфера размером AC_BUFFER_SIZE
void _clearBuffer(uint8_t * buf){
memset(buf, 0, AC_BUFFER_SIZE);
}
// очистка структуры пакета по указателю
void _clearPacket(packet_t * pckt){
if (pckt == nullptr) {
_debugMsg(F("Clear packet error: pointer is NULL!"), ESPHOME_LOG_LEVEL_ERROR, __LINE__);
return;
}
pckt->crc = nullptr;
pckt->header = (packet_header_t *)(pckt->data); // заголовок же всегда стартует с начала пакета
pckt->msec = 0;
pckt->bytesLoaded = 0;
pckt->body = nullptr;
_clearBuffer(pckt->data);
}
// очистка входящего пакета
void _clearInPacket(){
_clearPacket(&_inPacket);
}
// очистка исходящего пакета
void _clearOutPacket(){
_clearPacket(&_outPacket);
_outPacket.header->start_byte = AC_PACKET_START_BYTE; // для исходящего сразу ставим стартовый байт
_outPacket.header->wifi = AC_PACKET_ANSWER; // для исходящего пакета сразу ставим признак ответа
}
// копирует пакет из одной структуры в другую с корректным переносом указателей на заголовки и т.п.
bool _copyPacket(packet_t *dest, packet_t *src){
if (dest == nullptr) return false;
if (src == nullptr) return false;
dest->msec = src->msec;
dest->bytesLoaded = src->bytesLoaded;
memcpy(dest->data, src->data, AC_BUFFER_SIZE);
dest->header = (packet_header_t *)&dest->data;
if (dest->header->body_length > 0) dest->body = &dest->data[AC_HEADER_SIZE];
dest->crc = (packet_crc_t *)&dest->data[AC_HEADER_SIZE + dest->header->body_length];
return true;
}
// устанавливает состояние конечного автомата
// можно и напрямую устанавливать переменную, но для целей отладки лучше так
void _setStateMachineState(acsm_state state = ACSM_IDLE){
if (_ac_state == state) return; // состояние не меняется
_ac_state = state;
switch (state) {
case ACSM_IDLE:
_debugMsg(F("State changed to ACSM_IDLE."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
break;
case ACSM_RECEIVING_PACKET:
_debugMsg(F("State changed to ACSM_RECEIVING_PACKET."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
break;
case ACSM_PARSING_PACKET:
_debugMsg(F("State changed to ACSM_PARSING_PACKET."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
break;
case ACSM_SENDING_PACKET:
_debugMsg(F("State changed to ACSM_SENDING_PACKET."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
break;
default:
_debugMsg(F("State changed to ACSM_IDLE by default. Given state is %02X."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, state);
_ac_state = ACSM_IDLE;
break;
}
}
// состояние конечного автомата: IDLE
void _doIdleState(){
// вначале нужно выполнить очередной шаг последовательности команд
_doSequence();
// Если нет входящих данных, значит можно отправить исходящий пакет, если он есть
if (_ac_serial->available() == 0) {
// если есть пакет на отправку, то надо отправлять
// вначале думал, что сейчас отправка пакетов тут не нужна, т.к. состояние ACSM_SENDING_PACKET устанавливается сразу в парсере пакетов
// но потом понял, что у нас пакеты уходят не только когда надо отвечать, но и мы можем быть инициаторами
// поэтому вызов отправки тут пригодится
if (_outPacket.msec > 0) _setStateMachineState(ACSM_SENDING_PACKET);
// иначе просто выходим
return;
};
if (this->peek() == AC_PACKET_START_BYTE) {
// если во входящий пакет что-то уже загружено, значит это какие-то ошибочные данные или неизвестные пакеты
// надо эту инфу вывалить в лог
if (_inPacket.bytesLoaded > 0){
_debugMsg(F("Start byte received but there are some unparsed bytes in the buffer:"), ESPHOME_LOG_LEVEL_DEBUG, __LINE__);
_debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_DEBUG, __LINE__);
}
_clearInPacket();
_inPacket.msec = millis();
_setStateMachineState(ACSM_RECEIVING_PACKET);
//******************************************** экспериментальная секция *************************************************************
// пробуем сократить время ответа с помощью прямых вызовов обработчиков, а не через состояние IDLE
//_doReceivingPacketState();
// получилось всё те же 123 мсек. Только изредка падает до 109 мсек. Странно.
// логический анализатор показал примерно то же время от начала запроса до окончания ответа.
// запрос имеет длительность 18 мсек (лог.анализатор говорит 22,5 мсек).
// ответ имеет длительность 41 мсек по лог.анализатору.
// длительность паузы между запросом и ответом порядка 60 мсек.
// Скорее всего за один вызов _doReceivingPacketState не удается загрузить весь пакет (на момент вызова не все байы поступили в буфер UART)
// и поэтому программа отдает управление ESPHome для выполнения своих задач
// Стоит ли переделать код наоборот для непрерывного выполнения всё время, пока ожидается посылка - не знаю. Может быть такой риалтайм и не нужен.
//***********************************************************************************************************************************
} else {
while (_ac_serial->available() > 0)
{
// если наткнулись на старт пакета, то выходим из while
// если какие-то данные были загружены в буфер, то они будут выгружены в лог при загрузке нового пакета
if (this->peek() == AC_PACKET_START_BYTE) break;
// читаем байт в буфер входящего пакета
_inPacket.data[_inPacket.bytesLoaded] = this->read();
_inPacket.bytesLoaded++;
// если буфер уже полон, надо его вывалить в лог и очистить
if (_inPacket.bytesLoaded >= AC_BUFFER_SIZE){
_debugMsg(F("Some unparsed data on the bus:"), ESPHOME_LOG_LEVEL_DEBUG, __LINE__);
_debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_DEBUG, __LINE__);
_clearInPacket();
}
}
}
};
// состояние конечного автомата: ACSM_RECEIVING_PACKET
void _doReceivingPacketState(){
while (_ac_serial-> available() > 0) {
// если в буфере пакета данных уже под завязку, то надо сообщить о проблеме и выйти
if (_inPacket.bytesLoaded >= AC_BUFFER_SIZE) {
_debugMsg(F("Receiver: packet buffer overflow!"), ESPHOME_LOG_LEVEL_WARN, __LINE__);
_debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__);
_clearInPacket();
_setStateMachineState(ACSM_IDLE);
return;
}
_inPacket.data[_inPacket.bytesLoaded] = this->read();
_inPacket.bytesLoaded++;
// данных достаточно для заголовка
if (_inPacket.bytesLoaded == AC_HEADER_SIZE) {
// указатель заголовка установлен еще при обнулении пакета, его можно не трогать
//_inPacket.header = (packet_header_t *)(_inPacket.data);
// уже знаем размер пакета и можем установить указатели на тело пакета и CRC
_inPacket.crc = (packet_crc_t *)&(_inPacket.data[AC_HEADER_SIZE + _inPacket.header->body_length]);
if (_inPacket.header->body_length > 0) _inPacket.body = &(_inPacket.data[AC_HEADER_SIZE]);
_debugMsg(F("Header loaded: timestamp = %010u, start byte = %02X, packet type = %02X, body size = %02X"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _inPacket.msec, _inPacket.header->start_byte, _inPacket.header->packet_type, _inPacket.header->body_length);
}
// если все байты пакета загружены, надо его распарсить
// максимальный по размеру пакет будет упираться в размер буфера. если такой пакет здесь не уйдет на парсинг,
// то на следующей итерации будет ошибка о переполнении буфера, которая в начале цикла while
if (_inPacket.bytesLoaded == AC_HEADER_SIZE + _inPacket.header->body_length + 2) {
_debugMsg(F("Packet loaded: timestamp = %010u, start byte = %02X, packet type = %02X, body size = %02X, crc = [%02X, %02X]."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _inPacket.msec, _inPacket.header->start_byte, _inPacket.header->packet_type, _inPacket.header->body_length, _inPacket.crc->crc[0], _inPacket.crc->crc[1]);
_debugMsg(F("Loaded %02u bytes for a %u ms."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _inPacket.bytesLoaded, (millis() - _inPacket.msec));
_debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
_setStateMachineState(ACSM_PARSING_PACKET);
return;
}
}
// если пакет не загружен, а время вышло, то надо вернуться в IDLE
if (millis() - _inPacket.msec >= AC_PACKET_TIMEOUT) {
_debugMsg(F("Receiver: packet timed out!"), ESPHOME_LOG_LEVEL_WARN, __LINE__);
_debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__);
_clearInPacket();
_setStateMachineState(ACSM_IDLE);
return;
}
};
// состояние конечного автомата: ACSM_PARSING_PACKET
void _doParsingPacket(){
if (!_checkCRC(&_inPacket)) {
_debugMsg(F("Parser: packet CRC fail!"), ESPHOME_LOG_LEVEL_ERROR, __LINE__);
_debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_ERROR, __LINE__);
_clearInPacket();
_setStateMachineState(ACSM_IDLE);
return;
}
bool stateChangedFlag = false; // флаг, показывающий, изменилось ли состояние кондиционера
uint8_t stateByte = 0; // переменная для временного сохранения текущих параметров сплита для проверки их изменения
float stateFloat = 0.0; // переменная для временного сохранения текущих параметров сплита для проверки их изменения
// вначале выводим полученный пакет в лог, чтобы он шел до информации об ответах и т.п.
_debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_DEBUG, __LINE__);
// разбираем тип пакета
switch (_inPacket.header->packet_type) {
case AC_PTYPE_PING: { // ping-пакет, рассылается кондиционером каждые 3 сек.; модуль на него отвечает
if (_inPacket.header->body_length != 0 ) { // у входящего ping-пакета тело должно отсутствовать
// если тело есть, то жалуемся в лог
_debugMsg(F("Parser: ping packet should not to have body. Received one has body length %02X."), ESPHOME_LOG_LEVEL_WARN, __LINE__, _inPacket.header->body_length);
// очищаем пакет
_clearInPacket();
_setStateMachineState(ACSM_IDLE);
break;
}
_debugMsg(F("Parser: ping packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
// поднимаем флаг, что есть коннект с кондиционером
_has_connection = true;
// надо отправлять ответ на пинг
_clearOutPacket();
_outPacket.msec = millis();
_outPacket.header->packet_type = AC_PTYPE_PING;
_outPacket.header->ping_answer_01 = 0x01; // только в ответе на пинг этот байт равен 0x01; что означает не ясно
_outPacket.header->body_length = 8; // в ответе на пинг у нас тело 8 байт
_outPacket.body = &(_outPacket.data[AC_HEADER_SIZE]);
// заполняем тело пакета
packet_ping_answer_body_t * ping_body;
ping_body = (packet_ping_answer_body_t *) (_outPacket.body);
ping_body->byte_1C = 0x1C;
ping_body->byte_27 = 0x27;
// расчет контрольной суммы и прописывание её в пакет
_outPacket.crc = (packet_crc_t *)&(_outPacket.data[AC_HEADER_SIZE + _outPacket.header->body_length]);
_setCRC16(&_outPacket);
_outPacket.bytesLoaded = AC_HEADER_SIZE + _outPacket.header->body_length + 2;
_debugMsg(F("Parser: generated ping answer. Waiting for sending."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
// до отправки пинг-ответа проверяем, не выполнялась ли стартовая последовательность команд
// по задумке она выполняется после подключения к кондиционеру после ответа на первый пинг
// нужна для максимально быстрого определния текущих параметров кондиционера
if (!_startupSequenceComlete){
_startupSequenceComlete = startupSequence();
}
// изначально предполагал, что передачу пакета на отправку выполнит обработчик IDLE, но показалось, что слишком долго
// логика отправки через IDLE в том, что получение запросов может быть важнее отправки ответов и IDLE позволяет реализовать такой приоритет
// но потом решил всё же напрямую отправлять в отправку
// в этом случае пинг-ответ заканчивает отправку спустя 144 мсек после стартового байта пинг-запроса
//_setStateMachineState(ACSM_IDLE);
_setStateMachineState(ACSM_SENDING_PACKET);
// решил провести эксперимент
// получилось от начала запроса до отправки ответа порядка 165 мсек., если отправка идет не сразу, а через состояние IDLE
// Если сразу отсюда отправляться в обработчик отправки, то время сокращается до 131 мсек. Основные потери идут до входа в парсер пакетов
break;
}
case AC_PTYPE_CMD: { // команда сплиту; модуль отправляет такие команды, когда что-то хочет от сплита
// сплит такие команды отправлять не должен, поэтому жалуемся в лог
_debugMsg(F("Parser: packet type=0x06 received from HVAC. This isn't expected."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
// очищаем пакет
_clearInPacket();
_setStateMachineState(ACSM_IDLE);
break;
}
case AC_PTYPE_INFO: { // информационный пакет; бывает 3 видов; один из них рассылается кондиционером самостоятельно раз в 10 мин. и все 3 могут быть ответом на запросы модуля
// смотрим тип поступившего пакета по второму байту тела
_debugMsg(F("Parser: status packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
switch (_inPacket.body[1]) {
case AC_CMD_STATUS_SMALL: { // маленький пакет статуса кондиционера
_debugMsg(F("Parser: status packet type = small"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
stateChangedFlag = false;
// будем обращаться к телу пакета через указатель на структуру
packet_small_info_body_t * small_info_body;
small_info_body = (packet_small_info_body_t *) (_inPacket.body);
// в малом пакете передается большое количество установленных пользователем параметров работы
stateFloat = 8 + (small_info_body->target_temp_int_and_v_louver >> 3) + 0.5*(float)(small_info_body->target_temp_frac >> 7);
stateChangedFlag = stateChangedFlag || (_current_ac_state.temp_target != stateFloat);
_current_ac_state.temp_target = stateFloat;
_current_ac_state.temp_target_matter = true;
stateByte = small_info_body->target_temp_int_and_v_louver & AC_LOUVERV_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.louver.louver_v != (ac_louver_V)stateByte);
_current_ac_state.louver.louver_v = (ac_louver_V)stateByte;
stateByte = small_info_body->h_louver & AC_LOUVERH_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.louver.louver_h != (ac_louver_H)stateByte);
_current_ac_state.louver.louver_h = (ac_louver_H)stateByte;
stateByte = small_info_body->fan_speed & AC_FANSPEED_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.fanSpeed != (ac_fanspeed)stateByte);
_current_ac_state.fanSpeed = (ac_fanspeed)stateByte;
stateByte = small_info_body->fan_turbo_and_mute & AC_FANTURBO_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.fanTurbo != (ac_fanturbo)stateByte);
_current_ac_state.fanTurbo = (ac_fanturbo)stateByte;
stateByte = small_info_body->fan_turbo_and_mute & AC_FANMUTE_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.fanMute != (ac_fanmute)stateByte);
_current_ac_state.fanMute = (ac_fanmute)stateByte;
stateByte = small_info_body->mode & AC_MODE_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.mode != (ac_mode)stateByte);
_current_ac_state.mode = (ac_mode)stateByte;
stateByte = small_info_body->mode & AC_SLEEP_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.sleep != (ac_sleep)stateByte);
_current_ac_state.sleep = (ac_sleep)stateByte;
stateByte = small_info_body->mode & AC_IFEEL_MASK;
_current_ac_state.iFeel = (ac_ifeel)stateByte;
stateByte = small_info_body->status & AC_POWER_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.power != (ac_power)stateByte);
_current_ac_state.power = (ac_power)stateByte;
stateByte = small_info_body->status & AC_HEALTH_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.health != (ac_health)stateByte);
_current_ac_state.health = (ac_health)stateByte;
stateByte = small_info_body->status & AC_HEALTH_STATUS_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.health_status != (ac_health_status)stateByte);
_current_ac_state.health_status = (ac_health_status)stateByte;
stateByte = small_info_body->status & AC_CLEAN_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.clean != (ac_clean)stateByte);
_current_ac_state.clean = (ac_clean)stateByte;
stateByte = small_info_body->display_and_mildew & AC_DISPLAY_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.display != (ac_display)stateByte);
_current_ac_state.display = (ac_display)stateByte;
stateByte = small_info_body->display_and_mildew & AC_MILDEW_MASK;
stateChangedFlag = stateChangedFlag || (_current_ac_state.mildew != (ac_mildew)stateByte);
_current_ac_state.mildew = (ac_mildew)stateByte;
// уведомляем об изменении статуса сплита
if (stateChangedFlag) stateChanged();
break;
}
case AC_CMD_STATUS_BIG: // большой пакет статуса кондиционера
case AC_CMD_STATUS_PERIODIC: { // раз в 10 минут рассылается сплитом, структура аналогична большому пакету статуса
// TODO: вроде как AC_CMD_STATUS_PERIODIC могут быть и с другими кодами; пока что другие будут игнорироваться; если это будет критично, надо будет поправить
_debugMsg(F("Parser: status packet type = big or periodic"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
stateChangedFlag = false;
// будем обращаться к телу пакета через указатель на структуру
packet_big_info_body_t * big_info_body;
big_info_body = (packet_big_info_body_t *) (_inPacket.body);
// тип кондея (инвертор или старт стоп)
_is_invertor = big_info_body->is_invertor;
// температура воздуха в помещении по версии сплит-системы
stateFloat = big_info_body->ambient_temperature_int - 0x20 + (float)(big_info_body->ambient_temperature_frac & 0x0f) / 10.0;
stateChangedFlag = stateChangedFlag || (_current_ac_state.temp_ambient != stateFloat);
_current_ac_state.temp_ambient = stateFloat;
// некая температура из наружного блока, скорее всего температура испарителя
// temp = big_info_body->outdoor_temperature - 0x20;
// фильтруем простейшим фильтром OUTDOOR_FILTER_PESCENT - взнос одного измерения в процентах
{
const float koef = ((float)OUTDOOR_FILTER_PESCENT)/100;
const float antkoef = 1.0 - koef;
static float temp = _current_ac_state.temp_outdoor;
temp = temp * antkoef + koef * (big_info_body->outdoor_temperature - 0x20);
stateChangedFlag = stateChangedFlag || (_current_ac_state.temp_outdoor != temp);
_current_ac_state.temp_outdoor = temp;
}
// температура входящей магистрали
stateFloat = big_info_body->in_temperature_int - 0x20;
stateChangedFlag = stateChangedFlag || (_current_ac_state.temp_inbound != stateFloat);
_current_ac_state.temp_inbound = stateFloat;
// температура исходящей магистрали
stateFloat = big_info_body->out_temperature_int - 0x20;
stateChangedFlag = stateChangedFlag || (_current_ac_state.temp_outbound != stateFloat);
_current_ac_state.temp_outbound = stateFloat;
// температура непонятная температура
stateFloat = big_info_body->strange_temperature_int - 0x20;
stateChangedFlag = stateChangedFlag || (_current_ac_state.temp_strange != stateFloat);
_current_ac_state.temp_strange = stateFloat;
// реальная скорость проперлера
stateFloat = big_info_body->realFanSpeed;
stateChangedFlag = stateChangedFlag || (_current_ac_state.realFanSpeed != (ac_realFan)stateFloat);
_current_ac_state.realFanSpeed = (ac_realFan)stateFloat;
// мощность инвертора
stateFloat = big_info_body->invertor_power;
stateChangedFlag = stateChangedFlag || (_current_ac_state.invertor_power != stateFloat);
_current_ac_state.invertor_power = stateFloat;
// режим разморозки
bool temp = (big_info_body->needDefrost && big_info_body->defrostMode);
stateChangedFlag = stateChangedFlag || (_current_ac_state.defrost != temp);
_current_ac_state.defrost = temp;
// уведомляем об изменении статуса сплита
if (stateChangedFlag) {
stateChanged();
}
break;
}
case AC_CMD_SET_PARAMS: { // такой статусный пакет присылается кондиционером в ответ на команду установки параметров
// в теле пакета нет ничего примечательного
// в байтах 2 и 3 тела похоже передается CRC пакета поступившей команды, на которую сплит отвечает
// но я решил этот момент тут не проверять и не контролировать.
// корректную установку параметров можно определить, запросив статус кондиционера сразу после получения этой команды кондея
// в настоящий момент проверка сделана в механизме sequences
// TODO: если доводить до идеала, то проверку байтов 2 и 3 можно сделать и тут
break;
}
default:
_debugMsg(F("Parser: status packet type = unknown (%02X)"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _inPacket.body[1]);
break;
}
_setStateMachineState(ACSM_IDLE);
break;
}
case AC_PTYPE_INIT: // инициирующий пакет; присылается сплитом, если кнопка HEALTH на пульте нажимается 8 раз; как там и что работает - не разбирался.
case AC_PTYPE_UNKN: // какой-то странный пакет, отправляемый пультом при инициации и иногда при включении питания... как работает и зачем нужен - не разбирался, сплит на него вроде бы не реагирует
default:
// игнорируем. Для нашего случая эти пакеты не важны
_setStateMachineState(ACSM_IDLE);
break;
}
// если есть последовательность команд, то надо отработать проверку последовательности
if (hasSequence()) _doSequence();
// после разбора входящего пакета его надо очистить
_clearInPacket();
}
// состояние конечного автомата: ACSM_SENDING_PACKET
void _doSendingPacketState(){
// если нет исходящего пакета, то выходим
if ((_outPacket.msec == 0) || (_outPacket.crc == nullptr) || (_outPacket.bytesLoaded == 0)) {
_debugMsg(F("Sender: no packet to send."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
_setStateMachineState(ACSM_IDLE);
return;
}
_debugMsg(F("Sender: sending packet."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
_ac_serial->write_array(_outPacket.data, _outPacket.bytesLoaded);
_ac_serial->flush();
_debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_DEBUG, __LINE__);
_debugMsg(F("Sender: %u bytes sent (%u ms)."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _outPacket.bytesLoaded, millis()-_outPacket.msec);
_clearOutPacket();
_setStateMachineState(ACSM_IDLE);
};
/** вывод отладочной информации в лог
*
* dbgLevel - уровень сообщения, определен в ESPHome. За счет его использования можно из ESPHome управлять полнотой сведений в логе.
* msg - сообщение, выводимое в лог
* line - строка, на которой произошел вызов (удобно при отладке)
*/
void _debugMsg(const String &msg, uint8_t dbgLevel = ESPHOME_LOG_LEVEL_DEBUG, unsigned int line = 0, ... ){
if (dbgLevel < ESPHOME_LOG_LEVEL_NONE) dbgLevel = ESPHOME_LOG_LEVEL_NONE;
if (dbgLevel > ESPHOME_LOG_LEVEL_VERY_VERBOSE) dbgLevel = ESPHOME_LOG_LEVEL_VERY_VERBOSE;
if (line == 0) line = __LINE__; // если строка не передана, берем текущую строку
va_list vl;
va_start(vl, line);
esp_log_vprintf_(dbgLevel, Constants::TAG, line, msg.c_str(), vl); // так тоже вроде через Ж*, только ее не видно :)
va_end(vl);
}
/** выводим данные пакета в лог для отладки
*
* dbgLevel - уровень сообщения, определен в ESPHome. За счет его использования можно из ESPHome управлять полнотой сведений в логе.
* packet - указатель на пакет для вывода;
* если указатель на crc равен nullptr или первый байт в буфере не AC_PACKET_START_BYTE, то считаем, что передан битый пакет
* или не пакет вовсе. Для такого выводим только массив байт.
* Для нормального пакета данные выводятся с форматированием.
* line - строка, на которой произошел вызов (удобно при отладке)
**/
void _debugPrintPacket(packet_t * packet, uint8_t dbgLevel = ESPHOME_LOG_LEVEL_DEBUG, unsigned int line = 0){
// определяем, полноценный ли пакет нам передан
bool notAPacket = false;
// указатель заголовка всегда установден на начало буфера
notAPacket = notAPacket || (packet->crc == nullptr);
notAPacket = notAPacket || (packet->data[0] != AC_PACKET_START_BYTE);
String st = "";
char textBuf[11];
// заполняем время получения пакета
memset(textBuf, 0, 11);
sprintf(textBuf, "%010u", packet->msec);
st = st + textBuf + ": ";
// формируем преамбулы
if (packet == &_inPacket) {
st += "[<=] "; // преамбула входящего пакета
} else if (packet == &_outPacket) {
st += "[=>] "; // преамбула исходящего пакета
} else {
st += "[--] "; // преамбула для "непакета"
}
// формируем данные
#ifdef HOLMS
dbgLevel = ESPHOME_LOG_LEVEL_ERROR;
if(packet->header->body_length > HOLMS){
for (int i=0; i<packet->bytesLoaded; i++){
sprintf(textBuf, "%03d;", packet->data[i]);
st += textBuf;
}
if (line == 0) line = __LINE__;
_debugMsg(st, dbgLevel, line);
}
#else
for (int i=0; i<packet->bytesLoaded; i++){
// для нормальных пакетов надо заключить заголовок в []
if ((!notAPacket) && (i == 0)) st += "[";
// для нормальных пакетов надо заключить CRC в []
if ((!notAPacket) && (i == packet->header->body_length+AC_HEADER_SIZE)) st += "[";
//memset(textBuf, 0, 11);
sprintf(textBuf, "%02X", packet->data[i]);
st += textBuf;
// для нормальных пакетов надо заключить заголовок в []
if ((!notAPacket) && (i == AC_HEADER_SIZE-1)) st += "]";
// для нормальных пакетов надо заключить CRC в []
if ((!notAPacket) && (i == packet->header->body_length+AC_HEADER_SIZE+2-1)) st += "]";
st += " ";
}
if (line == 0) line = __LINE__;
_debugMsg(st, dbgLevel, line);
#endif
}
/** расчет CRC16 для блока данных data длиной len
* data - данные для расчета CRC16, указатель на массив байт
* len - длина блока данных для расчета, в байтах
*
* возвращаем uint16_t CRC16
**/
uint16_t _CRC16(uint8_t *data, uint8_t len){
uint32_t crc = 0;
// выделяем буфер для расчета CRC и копируем в него переданные данные
// это нужно для того, чтобы в случае нечетной длины данных можно было дополнить тело пакета
// одним нулевым байтом и не попортить загруженный пакет (ведь в загруженном сразу за телом идёт CRC)
uint8_t _crcBuffer[AC_BUFFER_SIZE];
memset(_crcBuffer, 0, AC_BUFFER_SIZE);
memcpy(_crcBuffer, data, len);
// если длина данных нечетная, то надо сделать четной, дополнив данные в конце нулевым байтом
// но так как выше буфер заполняли нулями, то отдельно тут присваивать 0x00 нет смысла
if ((len%2) == 1) len++;
// рассчитываем CRC16
uint32_t word = 0;
for (uint8_t i=0; i < len; i+=2){
word = (_crcBuffer[i] << 8) + _crcBuffer[i+1];
crc += word;
}
crc = (crc >> 16) + (crc & 0xFFFF);
crc = ~ crc;
return crc & 0xFFFF;
}
// расчитываем CRC16 и заполняем эти данные в структуре пакета
void _setCRC16(packet_t* pack = nullptr){
// если пакет не указан, то устанавливаем CRC для исходящего пакета
if (pack == nullptr) pack = &_outPacket;
packet_crc_t crc;
crc.crc16 = _CRC16(pack->data, AC_HEADER_SIZE + pack->header->body_length);
// если забыли указатель на crc установить, то устанавливаем
if (pack->crc == nullptr) pack->crc = (packet_crc_t *) &(pack->data[AC_HEADER_SIZE + pack->header->body_length]);
pack->crc->crc[0] = crc.crc[1];
pack->crc->crc[1] = crc.crc[0];
return;
}
// проверяет CRC пакета по указателю
bool _checkCRC(packet_t* pack = nullptr){
// если пакет не указан, то проверяем входящий
if (pack == nullptr) pack = &_inPacket;
if (pack->bytesLoaded < AC_HEADER_SIZE) {
_debugMsg(F("CRC check: incoming packet size error."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
// если забыли указатель на crc установить, то устанавливаем
if (pack->crc == nullptr) pack->crc = (packet_crc_t *) &(pack->data[AC_HEADER_SIZE + pack->header->body_length]);
packet_crc_t crc;
crc.crc16 = _CRC16(pack->data, AC_HEADER_SIZE + pack->header->body_length);
return ((pack->crc->crc[0] == crc.crc[1]) && (pack->crc->crc[1] == crc.crc[0]));
}
// заполняет пакет по ссылке командой запроса маленького пакета статуса
void _fillStatusSmall(packet_t * pack = nullptr){
// по умолчанию заполняем исходящий пакет
if (pack == nullptr) pack = &_outPacket;
// присваиваем параметры пакета
pack->msec = millis();
pack->header->start_byte = AC_PACKET_START_BYTE;
pack->header->wifi = AC_PACKET_ANSWER; // для исходящего пакета ставим признак ответа
pack->header->packet_type = AC_PTYPE_CMD;
pack->header->body_length = 2; // тело команды 2 байта
pack->body = &(pack->data[AC_HEADER_SIZE]);
pack->body[0] = AC_CMD_STATUS_SMALL;
pack->body[1] = 0x01; // он всегда 0x01
pack->bytesLoaded = AC_HEADER_SIZE + pack->header->body_length + 2;
// рассчитываем и записываем в пакет CRC
pack->crc = (packet_crc_t *) &(pack->data[AC_HEADER_SIZE + pack->header->body_length]);
_setCRC16(pack);
}
// заполняет пакет по ссылке командой запроса большого пакета статуса
void _fillStatusBig(packet_t * pack = nullptr){
// по умолчанию заполняем исходящий пакет
if (pack == nullptr) pack = &_outPacket;
// присваиваем параметры пакета
pack->msec = millis();
pack->header->start_byte = AC_PACKET_START_BYTE;
pack->header->wifi = AC_PACKET_ANSWER; // для исходящего пакета ставим признак ответа
pack->header->packet_type = AC_PTYPE_CMD;
pack->header->body_length = 2; // тело команды 2 байта
pack->body = &(pack->data[AC_HEADER_SIZE]);
pack->body[0] = AC_CMD_STATUS_BIG;
pack->body[1] = 0x01; // он всегда 0x01
pack->bytesLoaded = AC_HEADER_SIZE + pack->header->body_length + 2;
// рассчитываем и записываем в пакет CRC
pack->crc = (packet_crc_t *) &(pack->data[AC_HEADER_SIZE + pack->header->body_length]);
_setCRC16(pack);
}
/** заполняет пакет по ссылке командой установки параметров
*
* указатель на пакет может отсутствовать, тогда заполняется _outPacket
* указатель на команду также может отсутствовать, тогда используется текущее состояние из _current_ac_state
* все *__UNTOUCHED параметры заполняются из _current_ac_state
**/
void _fillSetCommand(bool clrPacket = false, packet_t * pack = nullptr, ac_state_t *cmd = nullptr){
// по умолчанию заполняем исходящий пакет
if (pack == nullptr) pack = &_outPacket;
// очищаем пакет, если это указано
if (clrPacket) _clearPacket(pack);
// заполняем его параметрами из _current_ac_state
if (cmd != &_current_ac_state) {
_fillSetCommand(false, pack, &_current_ac_state);
}
// если команда не указана, значит выходим
if (cmd == nullptr) return;
// команда указана, дополнительно внесем в пакет те параметры, которые установлены в команде
// присваиваем параметры пакета
pack->msec = millis();
pack->header->start_byte = AC_PACKET_START_BYTE;
pack->header->wifi = AC_PACKET_ANSWER; // для исходящего пакета ставим признак ответа
pack->header->packet_type = AC_PTYPE_CMD;
pack->header->body_length = 15; // тело команды 15 байт, как у Small status
pack->body = &(pack->data[AC_HEADER_SIZE]);
pack->body[0] = AC_CMD_SET_PARAMS; // устанавливаем параметры
pack->body[1] = 0x01; // он всегда 0x01
pack->bytesLoaded = AC_HEADER_SIZE + pack->header->body_length + 2;
// целевая температура кондиционера
if (cmd->temp_target_matter){
// устраняем выход за границы диапазона (это ограничение самого кондиционера)
if (cmd->temp_target < Constants::AC_MIN_TEMPERATURE) cmd->temp_target = Constants::AC_MIN_TEMPERATURE;
if (cmd->temp_target > Constants::AC_MAX_TEMPERATURE) cmd->temp_target = Constants::AC_MAX_TEMPERATURE;
// целая часть температуры
pack->body[2] = (pack->body[2] & ~AC_TEMP_TARGET_INT_PART_MASK) | (((uint8_t)(cmd->temp_target) - 8) << 3);
// дробная часть температуры
if (cmd->temp_target - (uint8_t)(cmd->temp_target) > 0) {
pack->body[4] = (pack->body[4] & ~AC_TEMP_TARGET_FRAC_PART_MASK) | 1;
} else {
pack->body[4] = (pack->body[4] & ~AC_TEMP_TARGET_FRAC_PART_MASK) | 0;
}
}
// обнулить счетчик минут с последней команды
pack->body[4] &= ~ AC_MIN_COUTER ;
// вертикальные жалюзи
if (cmd->louver.louver_v != AC_LOUVERV_UNTOUCHED){
pack->body[2] = (pack->body[2] & ~AC_LOUVERV_MASK) | cmd->louver.louver_v;
}
// горизонтальные жалюзи
if (cmd->louver.louver_h != AC_LOUVERH_UNTOUCHED){
pack->body[3] = (pack->body[3] & ~AC_LOUVERH_MASK) | cmd->louver.louver_h;
}
// скорость вентилятора
if (cmd->fanSpeed != AC_FANSPEED_UNTOUCHED){
pack->body[5] = (pack->body[5] & ~AC_FANSPEED_MASK) | cmd->fanSpeed;
}
// спец.режимы вентилятора: TURBO
if (cmd->fanTurbo != AC_FANTURBO_UNTOUCHED){
pack->body[6] = (pack->body[6] & ~AC_FANTURBO_MASK) | cmd->fanTurbo;
}
// спец.режимы вентилятора: MUTE
if (cmd->fanMute != AC_FANMUTE_UNTOUCHED){
pack->body[6] = (pack->body[6] & ~AC_FANMUTE_MASK) | cmd->fanMute;
}
// режим кондея
if (cmd->mode != AC_MODE_UNTOUCHED){
pack->body[7] = (pack->body[7] & ~AC_MODE_MASK) | cmd->mode;
}
if (cmd->sleep != AC_SLEEP_UNTOUCHED){
pack->body[7] = (pack->body[7] & ~AC_SLEEP_MASK) | cmd->sleep;
}
if (cmd->iFeel != AC_IFEEL_UNTOUCHED){
pack->body[7] = (pack->body[7] & ~AC_IFEEL_MASK) | cmd->iFeel;
}
// питание вкл/выкл
if (cmd->power != AC_POWER_UNTOUCHED){
pack->body[10] = (pack->body[10] & ~AC_POWER_MASK) | cmd->power;
}
if (cmd->clean != AC_CLEAN_UNTOUCHED){
pack->body[10] = (pack->body[10] & ~AC_CLEAN_MASK) | cmd->clean;
}
// ионизатор
if (cmd->health != AC_HEALTH_UNTOUCHED){
pack->body[10] = (pack->body[10] & ~AC_HEALTH_MASK) | cmd->health;
}
// какой то флаг ионизатора
if (cmd->health_status != AC_HEALTH_STATUS_UNTOUCHED){
pack->body[10] = (pack->body[10] & ~AC_HEALTH_STATUS_MASK) | cmd->health_status;
}
// дисплей
if (cmd->display != AC_DISPLAY_UNTOUCHED){
pack->body[12] = (pack->body[12] & ~AC_DISPLAY_MASK) | cmd->display;
}
// антиплесень
if (cmd->mildew != AC_MILDEW_UNTOUCHED){
pack->body[12] = (pack->body[12] & ~AC_MILDEW_MASK) | cmd->mildew;
}
// рассчитываем и записываем в пакет CRC
pack->crc = (packet_crc_t *) &(pack->data[AC_HEADER_SIZE + pack->header->body_length]);
_setCRC16(pack);
}
// отправка запроса на маленький статусный пакет
bool sq_requestSmallStatus(){
// если исходящий пакет не пуст, то выходим и ждем освобождения
if (_outPacket.bytesLoaded > 0) return true;
_fillStatusSmall(&_outPacket);
_fillStatusSmall(&_sequence[_sequence_current_step].packet);
_sequence[_sequence_current_step].packet_type = AC_SPT_SENT_PACKET;
// Отчитываемся в лог
_debugMsg(F("Sequence [step %u]: small status request generated:"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step);
_debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
// увеличиваем текущий шаг
_sequence_current_step++;
return true;
}
// проверка ответа на запрос маленького статусного пакета
bool sq_controlSmallStatus(){
// если по каким-то причинам нет входящего пакета, значит проверять нам нечего - просто выходим
if (_inPacket.bytesLoaded == 0) return true;
// Пинги игнорируем
if (_inPacket.header->packet_type == AC_PTYPE_PING) return true;
// сохраняем полученный пакет в последовательность, чтобы на возможных следующих шагах с ним можно было работать
_copyPacket(&_sequence[_sequence_current_step].packet, &_inPacket);
_sequence[_sequence_current_step].packet_type = AC_SPT_RECEIVED_PACKET;
// проверяем ответ
bool relevant = true;
relevant = (relevant && (_inPacket.header->packet_type == AC_PTYPE_INFO));
relevant = (relevant && (_inPacket.header->body_length == 0x0F));
relevant = (relevant && (_inPacket.body[0] == 0x01));
relevant = (relevant && (_inPacket.body[1] == AC_CMD_STATUS_SMALL));
// если пакет подходит, значит можно переходить к следующему шагу
if (relevant) {
_debugMsg(F("Sequence [step %u]: correct small status packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step);
_sequence_current_step++;
} else {
// если пакет не подходящий, то отчитываемся в лог...
_debugMsg(F("Sequence [step %u]: irrelevant incoming packet"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step);
_debugMsg(F("Incoming packet:"), ESPHOME_LOG_LEVEL_WARN, __LINE__);
_debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__);
_debugMsg(F("Sequence packet needed: PACKET_TYPE = %02X, CMD = %02X"), ESPHOME_LOG_LEVEL_WARN, __LINE__, AC_PTYPE_INFO, AC_CMD_STATUS_SMALL);
// ...и прерываем последовательность, так как вернем false
}
return relevant;
}
// отправка запроса на большой статусный пакет
bool sq_requestBigStatus(){
// если исходящий пакет не пуст, то выходим и ждем освобождения
if (_outPacket.bytesLoaded > 0) return true;
_fillStatusBig(&_outPacket);
_fillStatusBig(&_sequence[_sequence_current_step].packet);
_sequence[_sequence_current_step].packet_type = AC_SPT_SENT_PACKET;
// Отчитываемся в лог
_debugMsg(F("Sequence [step %u]: big status request generated:"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step);
_debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
// увеличиваем текущий шаг
_sequence_current_step++;
return true;
}
// проверка ответа на запрос большого статусного пакета
bool sq_controlBigStatus(){
// если по каким-то причинам нет входящего пакета, значит проверять нам нечего - просто выходим
if (_inPacket.bytesLoaded == 0) return true;
// Пинги игнорируем
if (_inPacket.header->packet_type == AC_PTYPE_PING) return true;
// сохраняем полученный пакет в последовательность, чтобы на возможных следующих шагах с ним можно было работать
_copyPacket(&_sequence[_sequence_current_step].packet, &_inPacket);
_sequence[_sequence_current_step].packet_type = AC_SPT_RECEIVED_PACKET;
// проверяем ответ
bool relevant = true;
relevant = (relevant && (_inPacket.header->packet_type == AC_PTYPE_INFO));
relevant = (relevant && (_inPacket.header->body_length == 0x18 || _inPacket.header->body_length == 0x19)); // канальник Royal Clima отвечает пакетом длиной 0x19
relevant = (relevant && (_inPacket.body[0] == 0x01));
relevant = (relevant && (_inPacket.body[1] == AC_CMD_STATUS_BIG));
// если пакет подходит, значит можно переходить к следующему шагу
if (relevant) {
_debugMsg(F("Sequence [step %u]: correct big status packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step);
_sequence_current_step++;
} else {
// если пакет не подходящий, то отчитываемся в лог...
_debugMsg(F("Sequence [step %u]: irrelevant incoming packet"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step);
_debugMsg(F("Incoming packet:"), ESPHOME_LOG_LEVEL_WARN, __LINE__);
_debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__);
_debugMsg(F("Sequence packet needed: PACKET_TYPE = %02X, CMD = %02X"), ESPHOME_LOG_LEVEL_WARN, __LINE__, AC_PTYPE_INFO, AC_CMD_STATUS_BIG);
// ...и прерываем последовательность
}
return relevant;
}
// отправка запроса на выполнение команды
bool sq_requestDoCommand(){
// если исходящий пакет не пуст, то выходим и ждем освобождения
if (_outPacket.bytesLoaded > 0) return true;
_fillSetCommand(true, &_outPacket, &_sequence[_sequence_current_step].cmd);
_fillSetCommand(true, &_sequence[_sequence_current_step].packet, &_sequence[_sequence_current_step].cmd);
_sequence[_sequence_current_step].packet_type = AC_SPT_SENT_PACKET;
// Отчитываемся в лог
_debugMsg(F("Sequence [step %u]: doCommand request generated:"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step);
_debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
// увеличиваем текущий шаг
_sequence_current_step++;
return true;
}
// проверка ответа на выполнение команды
bool sq_controlDoCommand(){
// если по каким-то причинам нет входящего пакета, значит проверять нам нечего - просто выходим
if (_inPacket.bytesLoaded == 0) return true;
// Пинги игнорируем
if (_inPacket.header->packet_type == AC_PTYPE_PING) return true;
// сохраняем полученный пакет в последовательность, чтобы на возможных следующих шагах с ним можно было работать
_copyPacket(&_sequence[_sequence_current_step].packet, &_inPacket);
_sequence[_sequence_current_step].packet_type = AC_SPT_RECEIVED_PACKET;
// проверяем ответ
bool relevant = true;
relevant = (relevant && (_inPacket.header->packet_type == AC_PTYPE_INFO));
relevant = (relevant && (_inPacket.header->body_length == 0x04));
relevant = (relevant && (_inPacket.body[0] == 0x01));
relevant = (relevant && (_inPacket.body[1] == AC_CMD_SET_PARAMS));
// байты 2 и 3 обычно равны CRC отправленного пакета с командой
relevant = (relevant && (_inPacket.body[2] == _sequence[_sequence_current_step-1].packet.crc->crc[0]));
relevant = (relevant && (_inPacket.body[3] == _sequence[_sequence_current_step-1].packet.crc->crc[1]));
// если пакет подходит, значит можно переходить к следующему шагу
if (relevant) {
_debugMsg(F("Sequence [step %u]: correct doCommand packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step);
_sequence_current_step++;
} else {
// если пакет не подходящий, то отчитываемся в лог...
_debugMsg(F("Sequence [step %u]: irrelevant incoming packet"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step);
_debugMsg(F("Incoming packet:"), ESPHOME_LOG_LEVEL_WARN, __LINE__);
_debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__);
_debugMsg(F("Sequence packet needed: PACKET_TYPE = %02X, CMD = %02X"), ESPHOME_LOG_LEVEL_WARN, __LINE__, AC_PTYPE_INFO, AC_CMD_STATUS_BIG);
// ...и прерываем последовательность
}
return relevant;
}
// отправка запроса с тестовым пакетом
bool sq_requestTestPacket(){
// если исходящий пакет не пуст, то выходим и ждем освобождения
if (_outPacket.bytesLoaded > 0) return true;
_copyPacket(&_outPacket, &_outTestPacket);
_copyPacket(&_sequence[_sequence_current_step].packet, &_outTestPacket);
_sequence[_sequence_current_step].packet_type = AC_SPT_SENT_PACKET;
// Отчитываемся в лог
_debugMsg(F("Sequence [step %u]: Test Packet request generated:"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step);
_debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
// увеличиваем текущий шаг
_sequence_current_step++;
return true;
}
// сенсоры, отображающие параметры сплита
esphome::sensor::Sensor *sensor_indoor_temperature_ = nullptr;
esphome::sensor::Sensor *sensor_outdoor_temperature_ = nullptr;
esphome::sensor::Sensor *sensor_inbound_temperature_ =nullptr;
esphome::sensor::Sensor *sensor_outbound_temperature_ =nullptr;
esphome::sensor::Sensor *sensor_strange_temperature_ =nullptr;
// текущая мощность компрессора
esphome::sensor::Sensor *sensor_invertor_power_ = nullptr;
// бинарный сенсор, отображающий состояние дисплея
esphome::binary_sensor::BinarySensor *sensor_display_ = nullptr;
// бинарный сенсор состония разморозки
esphome::binary_sensor::BinarySensor *sensor_defrost_ = nullptr;
// загружает на выполнение последовательность команд на включение/выключение табло с температурой
bool _displaySequence(ac_display dsp = AC_DISPLAY_ON){
// нет смысла в последовательности, если нет коннекта с кондиционером
if (!get_has_connection()) {
_debugMsg(F("displaySequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__);
return false;
}
if (dsp == AC_DISPLAY_UNTOUCHED) return false; // выходим, чтобы не тратить время
// формируем команду
ac_command_t cmd;
_clearCommand(&cmd); // не забываем очищать, а то будет мусор
cmd.display = dsp;
// добавляем команду в последовательность
if (!commandSequence(&cmd)) return false;
_debugMsg(F("displaySequence: loaded (display = %02X)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, dsp);
return true;
}
// номер глобального пресета от режима работы
uint8_t get_num_preset(ac_command_t* cmd){
if(cmd->power == AC_POWER_OFF){
return POS_MODE_OFF;
} else if(cmd->mode == AC_MODE_AUTO){
return POS_MODE_AUTO;
} else if(cmd->mode == AC_MODE_COOL){
return POS_MODE_COOL;
} else if(cmd->mode == AC_MODE_DRY){
return POS_MODE_DRY;
} else if(cmd->mode == AC_MODE_FAN){
return POS_MODE_FAN;
} else if(cmd->mode == AC_MODE_HEAT){
return POS_MODE_HEAT;
}
cmd->power = AC_POWER_OFF;
return POS_MODE_OFF;
}
// восстановление данных из пресета
void load_preset(ac_command_t* cmd, uint8_t num_preset){
if(num_preset < sizeof(global_presets)/sizeof(global_presets[0])){ // проверка выхода за пределы массива
if(cmd->power == global_presets[num_preset].power && cmd->mode == global_presets[num_preset].mode){ //контроль инициализации
memcpy(cmd,&(global_presets[num_preset]), AC_COMMAND_BASE_SIZE); // просто копируем из массива
_debugMsg(F("Preset %02d read from RAM massive."), ESPHOME_LOG_LEVEL_WARN, __LINE__, num_preset);
} else {
_debugMsg(F("Preset %02d not initialized, use current settings."), ESPHOME_LOG_LEVEL_WARN, __LINE__, num_preset);
}
}
}
// запись данных в массив персетов
void save_preset(ac_command_t* cmd){
uint8_t num_preset = get_num_preset(cmd);
if(memcmp(cmd,&(global_presets[num_preset]), AC_COMMAND_BASE_SIZE) != 0){ // содержимое пресетов разное
memcpy(&(global_presets[num_preset]), cmd, AC_COMMAND_BASE_SIZE); // копируем пресет в массив
#if defined(ESP32)
_debugMsg(F("Save preset %02d to NVRAM."), ESPHOME_LOG_LEVEL_WARN, __LINE__, num_preset);
if(storage.save(global_presets)){
if(!global_preferences->sync()) // сохраняем все пресеты
_debugMsg(F("Sync NVRAM error ! (load result: %02d)"), ESPHOME_LOG_LEVEL_ERROR, __LINE__, load_presets_result);
} else {
_debugMsg(F("Save presets to flash ERROR ! (load result: %02d)"), ESPHOME_LOG_LEVEL_ERROR, __LINE__, load_presets_result);
}
#endif
} else {
_debugMsg(F("Preset %02d has not been changed, Saving canceled."), ESPHOME_LOG_LEVEL_WARN, __LINE__, num_preset);
}
}
public:
// инициализация объекта
void initAC(esphome::uart::UARTComponent *parent = nullptr){
_dataMillis = millis();
_clearInPacket();
_clearOutPacket();
_clearPacket(&_outTestPacket);
_outTestPacket.header->start_byte = AC_PACKET_START_BYTE;
_outTestPacket.header->wifi = AC_PACKET_ANSWER;
_setStateMachineState(ACSM_IDLE);
_ac_serial = parent;
_hw_initialized = (_ac_serial != nullptr);
_has_connection = false;
// заполняем структуру состояния начальными значениями
_clearCommand((ac_command_t *)&_current_ac_state);
// очищаем последовательность пакетов
_clearSequence();
// выполнена ли уже стартовая последовательность команд (сбор информации о статусе кондея)
_startupSequenceComlete = false;
};
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
void set_indoor_temperature_sensor(sensor::Sensor *temperature_sensor) { sensor_indoor_temperature_ = temperature_sensor; }
void set_outdoor_temperature_sensor(sensor::Sensor *temperature_sensor) { sensor_outdoor_temperature_ = temperature_sensor; }
void set_inbound_temperature_sensor(sensor::Sensor *temperature_sensor) { sensor_inbound_temperature_ = temperature_sensor; }
void set_outbound_temperature_sensor(sensor::Sensor *temperature_sensor) { sensor_outbound_temperature_ = temperature_sensor; }
void set_strange_temperature_sensor(sensor::Sensor *temperature_sensor) { sensor_strange_temperature_ = temperature_sensor; }
void set_defrost_state(binary_sensor::BinarySensor *defrost_state) { sensor_defrost_ = defrost_state; }
void set_display_sensor(binary_sensor::BinarySensor *display_sensor) { sensor_display_ = display_sensor; }
void set_invertor_power_sensor(sensor::Sensor *invertor_power_sensor) { sensor_invertor_power_ = invertor_power_sensor; }
bool get_hw_initialized(){ return _hw_initialized; };
bool get_has_connection(){ return _has_connection; };
// возвращает, есть ли елементы в последовательности команд
bool hasSequence(){
return (_sequence[0].item_type != AC_SIT_NONE);
}
// вызывается для обновления отображения состояния кондиционера, ДЛЯ ПУБЛИКАЦИИ
void stateChanged(){
_debugMsg(F("State changed, let's publish it."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
if(_is_invertor){ // анализ режима для инвертора, точнее потому что использует показания мощности инвертора
static uint32_t timerInv = 0;
if(_current_ac_state.invertor_power == 0){ // инвертор выключен
timerInv = millis();
if(_current_ac_state.realFanSpeed == AC_REAL_FAN_OFF &&
_current_ac_state.power == AC_POWER_OFF ){ // внутренний кулер остановлен, кондей выключен
this->action = climate::CLIMATE_ACTION_OFF; // значит кондей не работает
} else {
int16_t delta_temp=_current_ac_state.temp_ambient - _current_ac_state.temp_inbound;
if (delta_temp > 0 && delta_temp < 2 &&
(_current_ac_state.realFanSpeed == AC_REAL_FAN_OFF ||
_current_ac_state.realFanSpeed == AC_REAL_FAN_MUTE ||
_current_ac_state.realFanSpeed == AC_REAL_FAN_MUTE )){
this->action = climate::CLIMATE_ACTION_DRYING; // ОСУШЕНИЕ
} else if (_current_ac_state.realFanSpeed == AC_REAL_FAN_MUTE ||
_current_ac_state.realFanSpeed == AC_REAL_FAN_OFF ){ // кулер чуть вертится
this->action = climate::CLIMATE_ACTION_IDLE; // кондей в простое
} else {
this->action = climate::CLIMATE_ACTION_FAN; // другие режимы - вентиляция
}
}
} else if(millis()-timerInv > 2000){ // инвертор включен, но нужно дождаться реакции на его включение
if(_current_ac_state.realFanSpeed == AC_REAL_FAN_OFF ||
_current_ac_state.realFanSpeed == AC_REAL_FAN_MUTE ){ //медленное вращение
if(_current_ac_state.temp_ambient - _current_ac_state.temp_inbound > 0){ //холодный радиатор
this->action = climate::CLIMATE_ACTION_DRYING; // ОСУШЕНИЕ
} else { // теплый радиатор, видимо переходный режим
this->action = climate::CLIMATE_ACTION_IDLE;
}
} else {
int16_t delta_temp=_current_ac_state.temp_ambient - _current_ac_state.temp_inbound;
if(delta_temp < -2){ // входящая температура выше комнатной, быстрый фен - ОБОГРЕВ
this->action = climate::CLIMATE_ACTION_HEATING;
} else if(delta_temp > 2){ // ниже, быстрый фен - ОХЛАЖДЕНИЕ
this->action = climate::CLIMATE_ACTION_COOLING;
} else { // просто вентиляция
this->action = climate::CLIMATE_ACTION_IDLE;
}
}
} else {
if(_current_ac_state.realFanSpeed == AC_REAL_FAN_OFF ||
_current_ac_state.realFanSpeed == AC_REAL_FAN_MUTE){
this->action = climate::CLIMATE_ACTION_IDLE;
} else {
this->action = climate::CLIMATE_ACTION_FAN; // другие режимы - вентиляция
}
}
} else {
if(_current_ac_state.realFanSpeed == AC_REAL_FAN_OFF &&
_current_ac_state.power == AC_POWER_OFF){
this->action = climate::CLIMATE_ACTION_OFF; // значит кондей не работает
} else {
int16_t delta_temp=_current_ac_state.temp_ambient - _current_ac_state.temp_inbound; // разность температуры между комнатной и входящей
if (delta_temp > 0 && delta_temp < 2 &&
(_current_ac_state.realFanSpeed == AC_REAL_FAN_OFF ||
_current_ac_state.realFanSpeed == AC_REAL_FAN_MUTE ||
_current_ac_state.realFanSpeed == AC_REAL_FAN_MUTE )){
this->action = climate::CLIMATE_ACTION_DRYING; // ОСУШЕНИЕ
} else if(_current_ac_state.realFanSpeed != AC_REAL_FAN_OFF &&
_current_ac_state.realFanSpeed != AC_REAL_FAN_MUTE){
if(delta_temp > 2){
this->action = climate::CLIMATE_ACTION_COOLING;
} else if(delta_temp < -2){
this->action = climate::CLIMATE_ACTION_HEATING;
} else {
this->action = climate::CLIMATE_ACTION_FAN; // другие режимы - вентиляция
}
} else {
this->action = climate::CLIMATE_ACTION_IDLE;
}
}
}
_debugMsg(F("Action mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->action);
/*************************** POWER & MODE ***************************/
if (_current_ac_state.power == AC_POWER_ON){
switch (_current_ac_state.mode) {
case AC_MODE_AUTO:
this->mode = climate::CLIMATE_MODE_HEAT_COOL; // по факту режим, названный в AUX как AUTO, является режимом HEAT_COOL
break;
case AC_MODE_COOL:
this->mode = climate::CLIMATE_MODE_COOL;
break;
case AC_MODE_DRY:
this->mode = climate::CLIMATE_MODE_DRY;
break;
case AC_MODE_HEAT:
this->mode = climate::CLIMATE_MODE_HEAT;
break;
case AC_MODE_FAN:
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
default:
_debugMsg(F("Warning: unknown air conditioner mode."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
break;
}
} else {
this->mode = climate::CLIMATE_MODE_OFF;
}
_debugMsg(F("Climate mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->mode);
/*************************** FAN SPEED ***************************/
if(_current_ac_state.power == AC_POWER_ON){
this->fan_mode = climate::CLIMATE_FAN_OFF;
switch (_current_ac_state.fanSpeed) {
case AC_FANSPEED_HIGH:
this->fan_mode = climate::CLIMATE_FAN_HIGH;
this->custom_fan_mode = (std::string)"";
break;
case AC_FANSPEED_MEDIUM:
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
this->custom_fan_mode = (std::string)"";
break;
case AC_FANSPEED_LOW:
this->fan_mode = climate::CLIMATE_FAN_LOW;
break;
case AC_FANSPEED_AUTO:
this->fan_mode = climate::CLIMATE_FAN_AUTO;
break;
default:
break;
}
/*************************** TURBO FAN MODE ***************************/
switch (_current_ac_state.fanTurbo) {
case AC_FANTURBO_ON:
this->custom_fan_mode = Constants::TURBO;
break;
case AC_FANTURBO_OFF:
default:
if (this->custom_fan_mode == Constants::TURBO) this->custom_fan_mode = (std::string)"";
break;
}
/*************************** MUTE FAN MODE ***************************/
switch (_current_ac_state.fanMute) {
case AC_FANMUTE_ON:
this->custom_fan_mode = Constants::MUTE;
break;
case AC_FANMUTE_OFF:
default:
if (this->custom_fan_mode == Constants::MUTE) this->custom_fan_mode = (std::string)"";
break;
}
} else { // при выключеном питании публикуем фальшивый статус
//this->fan_mode = climate::CLIMATE_FAN_LOW ;
this->custom_fan_mode = Constants::MUTE;
}
_debugMsg(F("Climate fan mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->fan_mode);
_debugMsg(F("Climate fan TURBO mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.fanTurbo);
_debugMsg(F("Climate fan MUTE mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.fanMute);
//======================== ОТОБРАЖЕНИЕ ПРЕСЕТОВ ================================
/*************************** SLEEP PRESET ***************************/
// Комбинируется только с режимами COOL и HEAT. Автоматически выключается через 7 часов.
// COOL: температура +1 градус через час, еще через час дополнительные +1 градус, дальше не меняется.
// HEAT: температура -2 градуса через час, еще через час дополнительные -2 градуса, дальше не меняется.
// Восстанавливается ли температура через 7 часов при отключении режима - не понятно.
if(_current_ac_state.sleep == AC_SLEEP_ON &&
_current_ac_state.power == AC_POWER_ON ) {
this->preset = climate::CLIMATE_PRESET_SLEEP;
} else if (this->preset == climate::CLIMATE_PRESET_SLEEP) {
this->preset = climate::CLIMATE_PRESET_NONE;
}
_debugMsg(F("Climate preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->preset);
/*************************** HEALTH CUSTOM PRESET ***************************/
// режим работы ионизатора
if(_current_ac_state.health == AC_HEALTH_ON &&
_current_ac_state.power == AC_POWER_ON ) {
this->custom_preset = Constants::HEALTH;
} else if (this->custom_preset == Constants::HEALTH) {
this->custom_preset = (std::string)"";
}
_debugMsg(F("Climate HEALTH preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.health);
/*************************** CLEAN CUSTOM PRESET ***************************/
// режим очистки кондиционера, включается (или должен включаться) при AC_POWER_OFF
if(_current_ac_state.clean == AC_CLEAN_ON &&
_current_ac_state.power == AC_POWER_OFF ) {
this->custom_preset = Constants::CLEAN;
} else if (this->custom_preset == Constants::CLEAN) {
this->custom_preset = (std::string)"";
}
_debugMsg(F("Climate CLEAN preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.clean);
/*************************** ANTIFUNGUS CUSTOM PRESET ***************************/
// пресет просушки кондиционера после выключения
// По факту: после выключения сплита он оставляет минут на 5 открытые жалюзи и глушит вентилятор.
// Уличный блок при этом гудит и тарахтит. Возможно, прогревается теплообменник для высыхания.
// Через некоторое время внешний блок замолкает и сплит закрывает жалюзи.
//
// Brokly:
// У меня есть на этот режим, конедй реагирует только в выключеном состоянии. Причем пульт шлет
// 5 посылок и при включении и при выключении. Но каких то видимых отличий в работе или в сценарии
// при выключении кондея, я не наблюдаю. На пульте горит пиктограмма этого режима, но просушки
// я не вижу. После выключения , с активированым режимом Anti-FUNGUS, кондей сразу закрывает хлебало
// и затыкается.
if(_current_ac_state.mildew == AC_MILDEW_ON &&
_current_ac_state.power == AC_POWER_OFF ) {
this->custom_preset = Constants::ANTIFUNGUS;
} else if (this->custom_preset == Constants::ANTIFUNGUS) {
this->custom_preset = (std::string)"";
}
_debugMsg(F("Climate ANTIFUNGUS preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.mildew);
/*************************** LOUVERs ***************************/
if( _current_ac_state.power == AC_POWER_OFF){ //ЕСЛИ КОНДЕЙ ВЫКЛЮЧЕН
this->swing_mode = climate::CLIMATE_SWING_OFF;
} else if (_current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN &&
_current_ac_state.louver.louver_h == AC_LOUVERH_SWING_LEFTRIGHT){
this->swing_mode = climate::CLIMATE_SWING_BOTH;
} else if (_current_ac_state.louver.louver_h == AC_LOUVERH_SWING_LEFTRIGHT){
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
} else if (_current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN){
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
} else {
this->swing_mode = climate::CLIMATE_SWING_OFF;
}
_debugMsg(F("Climate swing mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->swing_mode);
/*************************** TEMPERATURE ***************************/
if(_current_ac_state.mode == AC_MODE_FAN || _current_ac_state.power == AC_POWER_OFF){
this->target_temperature = _current_ac_state.temp_ambient;
} else if (_current_ac_state.mode == AC_MODE_AUTO ){
this->target_temperature = 25;
} else {
this->target_temperature = _current_ac_state.temp_target;
}
_debugMsg(F("Target temperature: %f"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->target_temperature);
this->current_temperature = _current_ac_state.temp_ambient;
_debugMsg(F("Room temperature: %f"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->current_temperature);
/*********************************************************************/
/*************************** PUBLISH STATE ***************************/
/*********************************************************************/
this->publish_state();
// температура в комнате
if (sensor_indoor_temperature_ != nullptr)
sensor_indoor_temperature_->publish_state(_current_ac_state.temp_ambient);
// температура уличного блока
if (sensor_outdoor_temperature_ != nullptr)
sensor_outdoor_temperature_->publish_state(_current_ac_state.temp_outdoor);
// температура подводящей магистрали
if (sensor_inbound_temperature_ != nullptr)
sensor_inbound_temperature_->publish_state(_current_ac_state.temp_inbound);
// температура отводящей магистрали
if (sensor_outbound_temperature_ != nullptr)
sensor_outbound_temperature_->publish_state(_current_ac_state.temp_outbound);
// температура странного датчика
if (sensor_strange_temperature_ != nullptr)
sensor_strange_temperature_->publish_state(_current_ac_state.temp_strange);
// мощность инвертора
if (sensor_invertor_power_ != nullptr)
sensor_invertor_power_->publish_state(_current_ac_state.invertor_power);
// флаг режима разморозки
if (sensor_defrost_ != nullptr)
sensor_defrost_->publish_state(_current_ac_state.defrost);
// состояние дисплея
if (sensor_display_ != nullptr) {
switch (_current_ac_state.display) {
case AC_DISPLAY_ON:
if (this->get_display_inverted()) {
sensor_display_->publish_state(false);
} else {
sensor_display_->publish_state(true);
}
break;
case AC_DISPLAY_OFF:
if (this->get_display_inverted()) {
sensor_display_->publish_state(true);
} else {
sensor_display_->publish_state(false);
}
break;
default:
// могут быть и другие состояния, поэтому так
break;
}
}
}
// вывод в дебаг текущей конфигурации компонента
void dump_config() {
ESP_LOGCONFIG(Constants::TAG, "AUX HVAC:");
ESP_LOGCONFIG(Constants::TAG, " [x] Firmware version: %s", Constants::AC_FIRMWARE_VERSION.c_str());
ESP_LOGCONFIG(Constants::TAG, " [x] Period: %dms", this->get_period());
ESP_LOGCONFIG(Constants::TAG, " [x] Show action: %s", TRUEFALSE(this->get_show_action()));
ESP_LOGCONFIG(Constants::TAG, " [x] Display inverted: %s", TRUEFALSE(this->get_display_inverted()));
ESP_LOGCONFIG(Constants::TAG, " [x] Save settings %s", TRUEFALSE(this->get_store_settings()));
ESP_LOGCONFIG(Constants::TAG, " [?] Is invertor %s", millis() > _update_period + 1000 ? YESNO(_is_invertor): "pending...");
if ((this->sensor_indoor_temperature_) != nullptr) {
ESP_LOGCONFIG(Constants::TAG, "%s%s '%s'", " ", LOG_STR_LITERAL("Indoor Temperature"), (this->sensor_indoor_temperature_)->get_name().c_str());
if (!(this->sensor_indoor_temperature_)->get_device_class().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Device Class: '%s'", " ", (this->sensor_indoor_temperature_)->get_device_class().c_str());
}
ESP_LOGCONFIG(Constants::TAG, "%s State Class: '%s'", " ", state_class_to_string((this->sensor_indoor_temperature_)->get_state_class()).c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Unit of Measurement: '%s'", " ", (this->sensor_indoor_temperature_)->get_unit_of_measurement().c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Accuracy Decimals: %d", " ", (this->sensor_indoor_temperature_)->get_accuracy_decimals());
if (!(this->sensor_indoor_temperature_)->get_icon().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Icon: '%s'", " ", (this->sensor_indoor_temperature_)->get_icon().c_str());
}
if (!(this->sensor_indoor_temperature_)->unique_id().empty()) {
ESP_LOGV(Constants::TAG, "%s Unique ID: '%s'", " ", (this->sensor_indoor_temperature_)->unique_id().c_str());
}
if ((this->sensor_indoor_temperature_)->get_force_update()) {
ESP_LOGV(Constants::TAG, "%s Force Update: YES", " ");
}
}
if ((this->sensor_outdoor_temperature_) != nullptr) {
ESP_LOGCONFIG(Constants::TAG, "%s%s '%s'", " ", LOG_STR_LITERAL("Outdoor Temperature"), (this->sensor_outdoor_temperature_)->get_name().c_str());
if (!(this->sensor_outdoor_temperature_)->get_device_class().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Device Class: '%s'", " ", (this->sensor_outdoor_temperature_)->get_device_class().c_str());
}
ESP_LOGCONFIG(Constants::TAG, "%s State Class: '%s'", " ", state_class_to_string((this->sensor_outdoor_temperature_)->get_state_class()).c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Unit of Measurement: '%s'", " ", (this->sensor_outdoor_temperature_)->get_unit_of_measurement().c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Accuracy Decimals: %d", " ", (this->sensor_outdoor_temperature_)->get_accuracy_decimals());
if (!(this->sensor_outdoor_temperature_)->get_icon().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Icon: '%s'", " ", (this->sensor_outdoor_temperature_)->get_icon().c_str());
}
if (!(this->sensor_outdoor_temperature_)->unique_id().empty()) {
ESP_LOGV(Constants::TAG, "%s Unique ID: '%s'", " ", (this->sensor_outdoor_temperature_)->unique_id().c_str());
}
if ((this->sensor_outdoor_temperature_)->get_force_update()) {
ESP_LOGV(Constants::TAG, "%s Force Update: YES", " ");
}
}
if ((this->sensor_inbound_temperature_) != nullptr) {
ESP_LOGCONFIG(Constants::TAG, "%s%s '%s'", " ", LOG_STR_LITERAL("Inbound Temperature"), (this->sensor_inbound_temperature_)->get_name().c_str());
if (!(this->sensor_inbound_temperature_)->get_device_class().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Device Class: '%s'", " ", (this->sensor_inbound_temperature_)->get_device_class().c_str());
}
ESP_LOGCONFIG(Constants::TAG, "%s State Class: '%s'", " ", state_class_to_string((this->sensor_inbound_temperature_)->get_state_class()).c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Unit of Measurement: '%s'", " ", (this->sensor_inbound_temperature_)->get_unit_of_measurement().c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Accuracy Decimals: %d", " ", (this->sensor_inbound_temperature_)->get_accuracy_decimals());
if (!(this->sensor_inbound_temperature_)->get_icon().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Icon: '%s'", " ", (this->sensor_inbound_temperature_)->get_icon().c_str());
}
if (!(this->sensor_inbound_temperature_)->unique_id().empty()) {
ESP_LOGV(Constants::TAG, "%s Unique ID: '%s'", " ", (this->sensor_inbound_temperature_)->unique_id().c_str());
}
if ((this->sensor_inbound_temperature_)->get_force_update()) {
ESP_LOGV(Constants::TAG, "%s Force Update: YES", " ");
}
}
if ((this->sensor_outbound_temperature_) != nullptr) {
ESP_LOGCONFIG(Constants::TAG, "%s%s '%s'", " ", LOG_STR_LITERAL("Outbound Temperature"), (this->sensor_outbound_temperature_)->get_name().c_str());
if (!(this->sensor_outbound_temperature_)->get_device_class().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Device Class: '%s'", " ", (this->sensor_outbound_temperature_)->get_device_class().c_str());
}
ESP_LOGCONFIG(Constants::TAG, "%s State Class: '%s'", " ", state_class_to_string((this->sensor_outbound_temperature_)->get_state_class()).c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Unit of Measurement: '%s'", " ", (this->sensor_outbound_temperature_)->get_unit_of_measurement().c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Accuracy Decimals: %d", " ", (this->sensor_outbound_temperature_)->get_accuracy_decimals());
if (!(this->sensor_outbound_temperature_)->get_icon().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Icon: '%s'", " ", (this->sensor_outbound_temperature_)->get_icon().c_str());
}
if (!(this->sensor_outbound_temperature_)->unique_id().empty()) {
ESP_LOGV(Constants::TAG, "%s Unique ID: '%s'", " ", (this->sensor_outbound_temperature_)->unique_id().c_str());
}
if ((this->sensor_outbound_temperature_)->get_force_update()) {
ESP_LOGV(Constants::TAG, "%s Force Update: YES", " ");
}
}
if ((this->sensor_strange_temperature_) != nullptr) {
ESP_LOGCONFIG(Constants::TAG, "%s%s '%s'", " ", LOG_STR_LITERAL("Strange Temperature"), (this->sensor_strange_temperature_)->get_name().c_str());
if (!(this->sensor_strange_temperature_)->get_device_class().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Device Class: '%s'", " ", (this->sensor_strange_temperature_)->get_device_class().c_str());
}
ESP_LOGCONFIG(Constants::TAG, "%s State Class: '%s'", " ", state_class_to_string((this->sensor_strange_temperature_)->get_state_class()).c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Unit of Measurement: '%s'", " ", (this->sensor_strange_temperature_)->get_unit_of_measurement().c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Accuracy Decimals: %d", " ", (this->sensor_strange_temperature_)->get_accuracy_decimals());
if (!(this->sensor_strange_temperature_)->get_icon().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Icon: '%s'", " ", (this->sensor_strange_temperature_)->get_icon().c_str());
}
if (!(this->sensor_strange_temperature_)->unique_id().empty()) {
ESP_LOGV(Constants::TAG, "%s Unique ID: '%s'", " ", (this->sensor_strange_temperature_)->unique_id().c_str());
}
if ((this->sensor_strange_temperature_)->get_force_update()) {
ESP_LOGV(Constants::TAG, "%s Force Update: YES", " ");
}
}
if ((this->sensor_invertor_power_) != nullptr) {
ESP_LOGCONFIG(Constants::TAG, "%s%s '%s'", " ", LOG_STR_LITERAL("Inverter Power"), (this->sensor_invertor_power_)->get_name().c_str());
if (!(this->sensor_invertor_power_)->get_device_class().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Device Class: '%s'", " ", (this->sensor_invertor_power_)->get_device_class().c_str());
}
ESP_LOGCONFIG(Constants::TAG, "%s State Class: '%s'", " ", state_class_to_string((this->sensor_invertor_power_)->get_state_class()).c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Unit of Measurement: '%s'", " ", (this->sensor_invertor_power_)->get_unit_of_measurement().c_str());
ESP_LOGCONFIG(Constants::TAG, "%s Accuracy Decimals: %d", " ", (this->sensor_invertor_power_)->get_accuracy_decimals());
if (!(this->sensor_invertor_power_)->get_icon().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Icon: '%s'", " ", (this->sensor_invertor_power_)->get_icon().c_str());
}
if (!(this->sensor_invertor_power_)->unique_id().empty()) {
ESP_LOGV(Constants::TAG, "%s Unique ID: '%s'", " ", (this->sensor_invertor_power_)->unique_id().c_str());
}
if ((this->sensor_invertor_power_)->get_force_update()) {
ESP_LOGV(Constants::TAG, "%s Force Update: YES", " ");
}
}
if ((this->sensor_defrost_) != nullptr) {
ESP_LOGCONFIG(Constants::TAG, "%s%s '%s'", " ", LOG_STR_LITERAL("Defrost status"), (this->sensor_defrost_)->get_name().c_str());
if (!(this->sensor_defrost_)->get_device_class().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Device Class: '%s'", " ", (this->sensor_defrost_)->get_device_class().c_str());
}
if (!(this->sensor_defrost_)->get_icon().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Icon: '%s'", " ", (this->sensor_defrost_)->get_icon().c_str());
}
if (!(this->sensor_defrost_)->get_object_id().empty()) {
ESP_LOGV(Constants::TAG, "%s Object ID: '%s'", " ", (this->sensor_defrost_)->get_object_id().c_str());
}
}
if ((this->sensor_display_) != nullptr) {
ESP_LOGCONFIG(Constants::TAG, "%s%s '%s'", " ", LOG_STR_LITERAL("Display"), (this->sensor_display_)->get_name().c_str());
if (!(this->sensor_display_)->get_device_class().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Device Class: '%s'", " ", (this->sensor_display_)->get_device_class().c_str());
}
if (!(this->sensor_display_)->get_icon().empty()) {
ESP_LOGCONFIG(Constants::TAG, "%s Icon: '%s'", " ", (this->sensor_display_)->get_icon().c_str());
}
if (!(this->sensor_display_)->get_object_id().empty()) {
ESP_LOGV(Constants::TAG, "%s Object ID: '%s'", " ", (this->sensor_display_)->get_object_id().c_str());
}
}
this->dump_traits_(Constants::TAG);
}
// вызывается пользователем из интерфейса ESPHome или Home Assistant
void control(const esphome::climate::ClimateCall &call) override {
bool hasCommand = false;
ac_command_t cmd;
_clearCommand(&cmd); // не забываем очищать, а то будет мусор
// User requested mode change
if (call.get_mode().has_value()) {
ClimateMode mode = *call.get_mode();
// Send mode to hardware
switch (mode) {
case climate::CLIMATE_MODE_OFF:
hasCommand = true;
cmd.power = AC_POWER_OFF;
load_preset(&cmd, POS_MODE_OFF);
cmd.temp_target = _current_ac_state.temp_ambient; // просто от нехрен делать
this->mode = mode;
break;
case climate::CLIMATE_MODE_COOL:
hasCommand = true;
cmd.power = AC_POWER_ON;
cmd.mode = AC_MODE_COOL;
load_preset(&cmd, POS_MODE_COOL);
this->mode = mode;
break;
case climate::CLIMATE_MODE_HEAT:
hasCommand = true;
cmd.power = AC_POWER_ON;
cmd.mode = AC_MODE_HEAT;
load_preset(&cmd, POS_MODE_HEAT);
this->mode = mode;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
hasCommand = true;
cmd.power = AC_POWER_ON;
cmd.mode = AC_MODE_AUTO;
load_preset(&cmd, POS_MODE_AUTO);
cmd.temp_target = 25; // зависимость от режима HEAT_COOL
cmd.temp_target_matter = true;
cmd.fanTurbo = AC_FANTURBO_OFF; // зависимость от режима HEAT_COOL
this->mode = mode;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
hasCommand = true;
cmd.power = AC_POWER_ON;
cmd.mode = AC_MODE_FAN;
load_preset(&cmd, POS_MODE_FAN);
cmd.temp_target = _current_ac_state.temp_ambient; // зависимость от режима FAN
cmd.temp_target_matter = true;
cmd.fanTurbo = AC_FANTURBO_OFF; // зависимость от режима FAN
cmd.sleep = AC_SLEEP_OFF;
if(cmd.fanSpeed == AC_FANSPEED_AUTO || _current_ac_state.fanSpeed == AC_FANSPEED_AUTO){
cmd.fanSpeed = AC_FANSPEED_LOW; // зависимость от режима FAN
}
this->mode = mode;
break;
case climate::CLIMATE_MODE_DRY:
hasCommand = true;
cmd.power = AC_POWER_ON;
cmd.mode = AC_MODE_DRY;
load_preset(&cmd, POS_MODE_DRY);
cmd.fanTurbo = AC_FANTURBO_OFF; // зависимость от режима DRY
cmd.sleep = AC_SLEEP_OFF; // зависимость от режима DRY
this->mode = mode;
break;
case climate::CLIMATE_MODE_AUTO: // этот режим в будущем можно будет использовать для автоматического пресета (ПИД-регулятора, например)
default:
break;
}
}
// User requested fan_mode change
if(_current_ac_state.power == AC_POWER_ON || cmd.power == AC_POWER_ON) { // пресеты для включенного кондея
// User requested swing_mode change
if (call.get_swing_mode().has_value()) {
ClimateSwingMode swingmode = *call.get_swing_mode();
// Send fan mode to hardware
switch (swingmode) {
// The protocol allows other combinations for SWING.
// For example "turn the louvers to the desired position or "spread to the sides" / "concentrate in the center".
// But the ROVEX IR-remote does not provide this features. Therefore this features haven't been tested.
// May be suitable for other models of AUX-based ACs.
case climate::CLIMATE_SWING_OFF:
cmd.louver.louver_h = AC_LOUVERH_OFF;
cmd.louver.louver_v = AC_LOUVERV_OFF;
hasCommand = true;
this->swing_mode = swingmode;
break;
case climate::CLIMATE_SWING_BOTH:
cmd.louver.louver_h = AC_LOUVERH_SWING_LEFTRIGHT;
cmd.louver.louver_v = AC_LOUVERV_SWING_UPDOWN;
hasCommand = true;
this->swing_mode = swingmode;
break;
case climate::CLIMATE_SWING_VERTICAL:
cmd.louver.louver_h = AC_LOUVERH_OFF;
cmd.louver.louver_v = AC_LOUVERV_SWING_UPDOWN;
hasCommand = true;
this->swing_mode = swingmode;
break;
case climate::CLIMATE_SWING_HORIZONTAL:
cmd.louver.louver_h = AC_LOUVERH_SWING_LEFTRIGHT;
cmd.louver.louver_v = AC_LOUVERV_OFF;
hasCommand = true;
this->swing_mode = swingmode;
break;
}
}
if (call.get_target_temperature().has_value()) {
if((cmd.mode !=AC_MODE_UNTOUCHED && cmd.mode == AC_MODE_FAN) ||
(cmd.mode ==AC_MODE_UNTOUCHED && _current_ac_state.mode == AC_MODE_FAN)){ // блокировка ввода температуры в режиме FAN
this->target_temperature = _current_ac_state.temp_ambient;
this->publish_state();
} else if((cmd.mode !=AC_MODE_UNTOUCHED && cmd.mode == AC_MODE_AUTO) ||
(cmd.mode ==AC_MODE_UNTOUCHED && _current_ac_state.mode == AC_MODE_AUTO)){ // блокировка ввода температуры в режиме АВТО
this->target_temperature = 25;
this->publish_state();
} else {
// User requested target temperature change
float temp = *call.get_target_temperature();
// Send target temp to climate
if (temp > Constants::AC_MAX_TEMPERATURE) temp = Constants::AC_MAX_TEMPERATURE;
if (temp < Constants::AC_MIN_TEMPERATURE) temp = Constants::AC_MIN_TEMPERATURE;
if(cmd.temp_target != temp){
cmd.temp_target = temp;
hasCommand = true;
cmd.temp_target_matter = true;
}
}
}
if (call.get_fan_mode().has_value()) {
ClimateFanMode fanmode = *call.get_fan_mode();
// Send fan mode to hardware
switch (fanmode) {
case climate::CLIMATE_FAN_AUTO:
if(cmd.mode != AC_MODE_FAN){ // при вентиляции нет такого режима
hasCommand = true;
cmd.fanSpeed = AC_FANSPEED_AUTO;
// changing fan speed cancels fan TURBO and MUTE modes for ROVEX air conditioners
cmd.fanTurbo = AC_FANTURBO_OFF;
cmd.fanMute = AC_FANMUTE_OFF;
this->fan_mode = fanmode;
}
break;
case climate::CLIMATE_FAN_LOW:
hasCommand = true;
cmd.fanSpeed = AC_FANSPEED_LOW;
// changing fan speed cancels fan TURBO and MUTE modes for ROVEX air conditioners
cmd.fanTurbo = AC_FANTURBO_OFF;
cmd.fanMute = AC_FANMUTE_OFF;
this->fan_mode = fanmode;
break;
case climate::CLIMATE_FAN_MEDIUM:
hasCommand = true;
cmd.fanSpeed = AC_FANSPEED_MEDIUM;
// changing fan speed cancels fan TURBO and MUTE modes for ROVEX air conditioners
cmd.fanTurbo = AC_FANTURBO_OFF;
cmd.fanMute = AC_FANMUTE_OFF;
this->fan_mode = fanmode;
break;
case climate::CLIMATE_FAN_HIGH:
hasCommand = true;
cmd.fanSpeed = AC_FANSPEED_HIGH;
// changing fan speed cancels fan TURBO and MUTE modes for ROVEX air conditioners
cmd.fanTurbo = AC_FANTURBO_OFF;
cmd.fanMute = AC_FANMUTE_OFF;
this->fan_mode = fanmode;
break;
case climate::CLIMATE_FAN_ON:
case climate::CLIMATE_FAN_OFF:
case climate::CLIMATE_FAN_MIDDLE:
case climate::CLIMATE_FAN_FOCUS:
case climate::CLIMATE_FAN_DIFFUSE:
default:
break;
}
} else if (call.get_custom_fan_mode().has_value()) {
std::string customfanmode = *call.get_custom_fan_mode();
// Send fan mode to hardware
if (customfanmode == Constants::TURBO) {
// TURBO fan mode is suitable in COOL and HEAT modes for Rovex air conditioners.
// Other modes don't accept TURBO fan mode.
// May be other AUX-based air conditioners do the same.
if ( cmd.mode == AC_MODE_COOL || cmd.mode == AC_MODE_HEAT ||
_current_ac_state.mode == AC_MODE_COOL || _current_ac_state.mode == AC_MODE_HEAT) {
hasCommand = true;
cmd.fanTurbo = AC_FANTURBO_ON;
cmd.fanMute = AC_FANMUTE_OFF; // зависимость от fanturbo
this->custom_fan_mode = customfanmode;
} else {
_debugMsg(F("TURBO fan mode is suitable in COOL and HEAT modes only."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
}
} else if (customfanmode == Constants::MUTE) {
// MUTE fan mode is suitable in FAN mode only for Rovex air conditioner.
// In COOL mode AC receives command without any changes.
// May be other AUX-based air conditioners do the same.
//if ( cmd.mode == AC_MODE_FAN ||
// _current_ac_state.mode == AC_MODE_FAN) {
hasCommand = true;
cmd.fanMute = AC_FANMUTE_ON;
cmd.fanTurbo = AC_FANTURBO_OFF; // зависимость от fanmute
this->custom_fan_mode = customfanmode;
//} else {
// _debugMsg(F("MUTE fan mode is suitable in FAN mode only."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
//}
}
}
// Пользователь выбрал пресет
if (call.get_preset().has_value()) {
ClimatePreset preset = *call.get_preset();
switch (preset) {
case climate::CLIMATE_PRESET_SLEEP:
// Ночной режим (SLEEP). Комбинируется только с режимами COOL, HEAT AUTO и DRY. Автоматически выключается через 7 часов.
// COOL: температура +1 градус через час, еще через час дополнительные +1 градус, дальше не меняется.
// HEAT: температура -2 градуса через час, еще через час дополнительные -2 градуса, дальше не меняется.
// Восстанавливается ли температура через 7 часов при отключении режима - не понятно.
if ( cmd.mode == AC_MODE_COOL || cmd.mode == AC_MODE_HEAT || cmd.mode == AC_MODE_DRY || cmd.mode == AC_MODE_AUTO ||
_current_ac_state.mode == AC_MODE_COOL || _current_ac_state.mode == AC_MODE_HEAT ||
_current_ac_state.mode == AC_MODE_DRY || _current_ac_state.mode == AC_MODE_AUTO){
hasCommand = true;
cmd.sleep = AC_SLEEP_ON;
cmd.health = AC_HEALTH_OFF; // для логики пресетов
cmd.health_status = AC_HEALTH_STATUS_OFF;
this->preset = preset;
} else {
_debugMsg(F("SLEEP preset is suitable in COOL,HEAT and AUTO modes only."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
}
break;
case climate::CLIMATE_PRESET_NONE:
// выбран пустой пресет, сбрасываем все настройки
hasCommand = true;
cmd.health = AC_HEALTH_OFF; // для логики пресетов
cmd.health_status = AC_HEALTH_STATUS_OFF;
cmd.sleep = AC_SLEEP_OFF; // для логики пресетов
this->preset = preset;
_debugMsg(F("Clear all power ON presets"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
break;
default:
// никакие другие встроенные пресеты не поддерживаются
break;
}
} else if (call.get_custom_preset().has_value()) {
std::string custom_preset = *call.get_custom_preset();
if (custom_preset == Constants::HEALTH) {
hasCommand = true;
cmd.health = AC_HEALTH_ON;
cmd.health_status = AC_HEALTH_STATUS_ON;
cmd.fanTurbo = AC_FANTURBO_OFF; // зависимость от health
cmd.fanMute = AC_FANMUTE_OFF; // зависимость от health
cmd.sleep = AC_SLEEP_OFF; // для логики пресетов
if(cmd.mode == AC_MODE_COOL || cmd.mode == AC_MODE_HEAT || cmd.mode == AC_MODE_AUTO ||
_current_ac_state.mode == AC_MODE_COOL || _current_ac_state.mode == AC_MODE_HEAT ||
_current_ac_state.mode == AC_MODE_AUTO ){
cmd.fanSpeed = AC_FANSPEED_AUTO; // зависимость от health
} else if(cmd.mode == AC_MODE_FAN || _current_ac_state.mode == AC_MODE_FAN){
cmd.fanSpeed = AC_FANSPEED_MEDIUM; // зависимость от health
}
this->custom_preset = custom_preset;
} else if (custom_preset == Constants::CLEAN) {
_debugMsg(F("CLEAN work only in POWER ON mode."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
} else if (custom_preset == Constants::ANTIFUNGUS) {
_debugMsg(F("Anti-FUNGUS works only in POWER ON mode."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
}
}
} else if(_current_ac_state.power == AC_POWER_OFF || cmd.power == AC_POWER_OFF){ // функции при выключеном питании
if (call.get_target_temperature().has_value()) { // блокировка изменения температуры в выключеном состоянии
this->target_temperature = _current_ac_state.temp_ambient;
this->publish_state();
}
if (call.get_preset().has_value()) { // пользователь выбрал пустой пресет
ClimatePreset preset = *call.get_preset();
switch (preset) {
case climate::CLIMATE_PRESET_SLEEP:
_debugMsg(F("SLEEP preset is suitable in POWER ON mode only."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
break;
case climate::CLIMATE_PRESET_NONE:
// выбран пустой пресет, сбрасываем все настройки
hasCommand = true;
cmd.clean = AC_CLEAN_OFF; // для логики пресетов
cmd.mildew = AC_MILDEW_OFF; // для логики пресетов
this->preset = preset;
_debugMsg(F("Clear all 'Power OFF' presets"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
break;
default:
// никакие другие встроенные пресеты не поддерживаются
break;
}
} else {
std::string custom_preset = *call.get_custom_preset();
if (call.get_custom_preset().has_value()) {
if (custom_preset == Constants::CLEAN) {
// режим очистки кондиционера при AC_POWER_OFF
hasCommand = true;
cmd.clean = AC_CLEAN_ON;
cmd.mildew = AC_MILDEW_OFF; // для логики пресетов
this->custom_preset = custom_preset;
} else if (custom_preset == Constants::ANTIFUNGUS) {
// Brokly:
// включение-выключение функции "Антиплесень".
// у меня пульт отправляет 5 посылок и на включение и на выключение, но реагирует на эту кнопку
// только в режиме POWER_OFF
// По факту: после выключения сплита он оставляет минут на 5 открытые жалюзи и глушит вентилятор.
// Уличный блок при этом гудит и тарахтит. Возможно, прогревается теплообменник для высыхания.
// Через некоторое время внешний блок замолкает и сплит закрывает жалюзи.
cmd.mildew = AC_MILDEW_ON;
cmd.clean = AC_CLEAN_OFF; // для логики пресетов
hasCommand = true;
this->custom_preset = custom_preset;
} else if (custom_preset == Constants::HEALTH) {
_debugMsg(F("HEALTH is not supported in POWER OFF mode."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
}
}
}
}
if (hasCommand) {
commandSequence(&cmd);
this->publish_state(); // Publish updated state
_new_command_set = _store_settings; // флаг отправки новой команды, для процедуры сохранения пресетов, если есть настройка
}
}
esphome::climate::ClimateTraits traits() override {
// The capabilities of the climate device
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
traits.set_supports_two_point_target_temperature(false); // if the climate device's target temperature should be split in target_temperature_low and target_temperature_high instead of just the single target_temperature
// tells the frontend what range of temperatures the climate device should display (gauge min/max values)
traits.set_visual_min_temperature(Constants::AC_MIN_TEMPERATURE);
traits.set_visual_max_temperature(Constants::AC_MAX_TEMPERATURE);
// the step with which to increase/decrease target temperature. This also affects with how many decimal places the temperature is shown.
traits.set_visual_temperature_step(Constants::AC_TEMPERATURE_STEP);
traits.set_supported_modes(this->_supported_modes);
traits.set_supported_swing_modes(this->_supported_swing_modes);
traits.set_supported_presets(this->_supported_presets);
traits.set_supported_custom_presets(this->_supported_custom_presets);
traits.set_supported_custom_fan_modes(this->_supported_custom_fan_modes);
/* + MINIMAL SET */
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_OFF);
traits.add_supported_mode(ClimateMode::CLIMATE_MODE_FAN_ONLY);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_MEDIUM);
traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_HIGH);
traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_OFF);
//traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_VERTICAL);
//traits.add_supported_swing_mode(ClimateSwingMode::CLIMATE_SWING_BOTH);
traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_NONE);
//traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_SLEEP);
/* *************** TODO: надо сделать информирование о текущем режиме, сплит поддерживает ***************
* смотри climate::ClimateAction
*/
// if the climate device supports reporting the active current action of the device with the action property.
traits.set_supports_action(this->_show_action);
return traits;
}
// запрос маленького пакета статуса кондиционера
bool getStatusSmall(){
// нет смысла в последовательности, если нет коннекта с кондиционером
if (!get_has_connection()) {
_debugMsg(F("getStatusSmall: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__);
return false;
}
// есть ли место на запрос в последовательности команд?
if (_getFreeSequenceSpace() < 2) {
_debugMsg(F("getStatusSmall: not enough space in command sequence. Sequence steps doesn't loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/*************************************** getSmallInfo request ***********************************************/
if (!_addSequenceFuncStep(&AirCon::sq_requestSmallStatus)) {
_debugMsg(F("getStatusSmall: getSmallInfo request sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/*************************************** getSmallInfo control ***********************************************/
if (!_addSequenceFuncStep(&AirCon::sq_controlSmallStatus)) {
_debugMsg(F("getStatusSmall: getSmallInfo control sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/**************************************************************************************/
_debugMsg(F("getStatusSmall: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
return true;
}
// запрос большого пакета статуса кондиционера
bool getStatusBig(){
// нет смысла в последовательности, если нет коннекта с кондиционером
if (!get_has_connection()) {
_debugMsg(F("getStatusBig: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__);
return false;
}
// есть ли место на запрос в последовательности команд?
if (_getFreeSequenceSpace() < 2) {
_debugMsg(F("getStatusBig: not enough space in command sequence. Sequence steps doesn't loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/*************************************** getBigInfo request ***********************************************/
if (!_addSequenceFuncStep(&AirCon::sq_requestBigStatus)) {
_debugMsg(F("getStatusBig: getBigInfo request sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/*************************************** getBigInfo control ***********************************************/
if (!_addSequenceFuncStep(&AirCon::sq_controlBigStatus)) {
_debugMsg(F("getStatusBig: getBigInfo control sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/**************************************************************************************/
_debugMsg(F("getStatusBig: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
return true;
}
// запрос большого и малого пакетов статуса последовательно
bool getStatusBigAndSmall(){
// нет смысла в последовательности, если нет коннекта с кондиционером
if (!get_has_connection()) {
_debugMsg(F("getStatusBigAndSmall: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__);
return false;
}
if (!getStatusSmall()) {
_debugMsg(F("getStatusBigAndSmall: error with small status sequence."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
if (!getStatusBig()) {
_debugMsg(F("getStatusBigAndSmall: error with big status sequence."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
_debugMsg(F("getStatusBigAndSmall: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
return true;
}
/** стартовая последовательность пакетов
*
* нужна, чтобы не ждать долго обновления статуса кондиционера
* запускаем сразу, как только удалось подключиться к кондиционеру и прошел первый пинг-пакет
* возвращаемое значение будет присвоено флагу выполнения последовательности
* то есть при возврате false последовательность считается не запущенной и будет вызоваться до тех пор, пока не вернет true
**/
bool startupSequence(){
// нет смысла в последовательности, если нет коннекта с кондиционером
if (!get_has_connection()) {
_debugMsg(F("startupSequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__);
return false;
}
// по сути на старте надо получить от кондиционера два статуса
if (!getStatusBigAndSmall()){
_debugMsg(F("startupSequence: error with big&small status sequence."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
};
_debugMsg(F("startupSequence: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
return true;
}
/** загружает на выполнение команду
*
* стандартная последовательность - это запрос маленького статусного пакета, выполнение команды и повторный запрос
* такого же статуса для проверки, что всё включилось, ну и для обновления интерфейсов всяких связанных компонентов
**/
bool commandSequence(ac_command_t * cmd){
// нет смысла в последовательности, если нет коннекта с кондиционером
if (!get_has_connection()) {
_debugMsg(F("commandSequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__);
return false;
}
// добавление начального запроса маленького статусного пакета в последовательность команд
if (!getStatusSmall()) {
_debugMsg(F("commandSequence: error with first small status sequence."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
// есть ли место на запрос в последовательности команд?
if (_getFreeSequenceSpace() < 2) {
_debugMsg(F("commandSequence: not enough space in command sequence. Sequence steps doesn't loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/*************************************** set params request ***********************************************/
if (!_addSequenceFuncStep(&AirCon::sq_requestDoCommand, cmd)) {
_debugMsg(F("commandSequence: getBigInfo request sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/*************************************** set params control ***********************************************/
if (!_addSequenceFuncStep(&AirCon::sq_controlDoCommand)) {
_debugMsg(F("commandSequence: getBigInfo control sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/**************************************************************************************/
// добавление финального запроса маленького статусного пакета в последовательность команд
if (!getStatusSmall()) {
_debugMsg(F("commandSequence: error with last small status sequence."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
_debugMsg(F("commandSequence: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
return true;
}
// загружает на выполнение последовательность команд на включение/выключение
bool powerSequence(ac_power pwr = AC_POWER_ON){
// нет смысла в последовательности, если нет коннекта с кондиционером
if (!get_has_connection()) {
_debugMsg(F("powerSequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__);
return false;
}
if (pwr == AC_POWER_UNTOUCHED) return false; // выходим, чтобы не тратить время
// формируем команду
ac_command_t cmd;
_clearCommand(&cmd); // не забываем очищать, а то будет мусор
cmd.power = pwr;
// добавляем команду в последовательность
if (!commandSequence(&cmd)) return false;
_debugMsg(F("powerSequence: loaded (power = %02X)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, pwr);
return true;
}
// выключает экран
bool displayOffSequence(){
ac_display dsp = AC_DISPLAY_OFF;
if (this->get_display_inverted()) dsp = AC_DISPLAY_ON;
return _displaySequence(dsp);
}
// включает экран
bool displayOnSequence(){
ac_display dsp = AC_DISPLAY_ON;
if (this->get_display_inverted()) dsp = AC_DISPLAY_OFF;
return _displaySequence(dsp);
}
// отправляет сплиту заданный набор байт
// Перед отправкой проверяет пакет на корректность структуры. CRC16 рассчитывает самостоятельно и перезаписывает.
bool sendTestPacket(const std::vector<uint8_t> &data){
//bool sendTestPacket(uint8_t *data = nullptr, uitn8_t data_length = 0){
//if (data == nullptr) return false;
//if (data_length == 0) return false;
if (data.size() == 0) return false;
//if (data_length > AC_BUFFER_SIZE) return false;
if (data.size() > AC_BUFFER_SIZE) return false;
// нет смысла в отправке, если нет коннекта с кондиционером
if (!get_has_connection()) {
_debugMsg(F("sendTestPacket: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__);
return false;
}
// очищаем пакет
_clearPacket(&_outTestPacket);
// копируем данные в пакет
//memcpy(_outTestPacket.data, data, data_length);
uint8_t i = 0;
for (uint8_t n : data) {
_outTestPacket.data[i] = n;
i++;
}
// на всякий случай указываем правильные некоторые байты
_outTestPacket.header->start_byte = AC_PACKET_START_BYTE;
//_outTestPacket.header->wifi = AC_PACKET_ANSWER;
_outTestPacket.msec = millis();
_outTestPacket.body = &(_outTestPacket.data[AC_HEADER_SIZE]);
_outTestPacket.bytesLoaded = AC_HEADER_SIZE + _outTestPacket.header->body_length + 2;
// рассчитываем и записываем в пакет CRC
_outTestPacket.crc = (packet_crc_t *) &(_outTestPacket.data[AC_HEADER_SIZE + _outTestPacket.header->body_length]);
_setCRC16(&_outTestPacket);
_debugMsg(F("sendTestPacket: test packet loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
_debugPrintPacket(&_outTestPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__);
// ниже блок добавления отправки пакета в последовательность команд
//*****************************************************************
// есть ли место на запрос в последовательности команд?
if (_getFreeSequenceSpace() < 1) {
_debugMsg(F("sendTestPacket: not enough space in command sequence. Sequence steps doesn't loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/*************************************** sendTestPacket request ***********************************************/
if (!_addSequenceFuncStep(&AirCon::sq_requestTestPacket)) {
_debugMsg(F("sendTestPacket: sendTestPacket request sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__);
return false;
}
/**************************************************************************************/
_debugMsg(F("sendTestPacket: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__);
return true;
}
void set_period(uint32_t ms) { this->_update_period = ms; }
uint32_t get_period() { return this->_update_period; }
void set_show_action(bool show_action) { this->_show_action = show_action; }
bool get_show_action() { return this->_show_action; }
void set_display_inverted(bool display_inverted) { this->_display_inverted = display_inverted; }
bool get_display_inverted() { return this->_display_inverted; }
void set_store_settings(bool store_settings) { this->_store_settings = store_settings; }
bool get_store_settings() { return this->_store_settings; }
void set_supported_modes(const std::set<ClimateMode> &modes) { this->_supported_modes = modes; }
void set_supported_swing_modes(const std::set<ClimateSwingMode> &modes) { this->_supported_swing_modes = modes; }
void set_supported_presets(const std::set<ClimatePreset> &presets) { this->_supported_presets = presets; }
void set_custom_presets(const std::set<std::string> &presets) { this->_supported_custom_presets = presets; }
void set_custom_fan_modes(const std::set<std::string> &modes) { this->_supported_custom_fan_modes = modes; }
uint8_t load_presets_result = 0xFF;
void setup() override {
#if defined(ESP32)
load_presets_result = storage.load(global_presets); // читаем все пресеты из флеша
_debugMsg(F("Preset base read from NVRAM, result %02d."), ESPHOME_LOG_LEVEL_WARN, __LINE__, load_presets_result);
#endif
};
void loop() override {
if (!get_hw_initialized()) return;
// контролируем сохранение пресета
if(_new_command_set){ //нужно сохранить пресет
_new_command_set = false;
save_preset((ac_command_t *)&_current_ac_state); // переносим текущие данные в массив пресетов
}
// отрабатываем состояния конечного автомата
switch (_ac_state) {
case ACSM_RECEIVING_PACKET:
// находимся в процессе получения пакета, никакие отправки в этом состоянии невозможны
_doReceivingPacketState();
break;
case ACSM_PARSING_PACKET:
// разбираем полученный пакет
_doParsingPacket();
break;
case ACSM_SENDING_PACKET:
// отправляем пакет сплиту
_doSendingPacketState();
break;
case ACSM_IDLE: // ничего не делаем, ждем, на что бы среагировать
default: // если состояние какое-то посторонее, то считаем, что IDLE
_doIdleState();
break;
}
// раз в заданное количество миллисекунд запрашиваем обновление статуса кондиционера
if ((millis()-_dataMillis) > _update_period){
_dataMillis = millis();
// обычный wifi-модуль запрашивает маленький пакет статуса
// но нам никто не мешает запрашивать и большой и маленький, чтобы чаще обновлять комнатную температуру
// делаем этот запросо только в случае, если есть коннект с кондиционером
if (get_has_connection()) getStatusBigAndSmall();
}
};
};
} // namespace aux_ac
} // namespace esphome