diff --git a/README-EN.md b/README-EN.md index 3ec6ba5..53434a5 100644 --- a/README-EN.md +++ b/README-EN.md @@ -89,15 +89,44 @@ climate: indoor_temperature: name: AC Indoor Temperature id: ac_indoor_temp - internal: true + accuracy_decimals: 1 + internal: false + outdoor_temperature: + name: AC Outdoor Temperature + id: ac_outdoor_temp + internal: false + outbound_temperature: + name: AC Colant Outbound Temperature + id: ac_outbound_temp + internal: false + inbound_temperature: + name: AC Colant Inbound Temperature + id: ac_inbound_temp + internal: false + compressor_temperature: + name: AC Compressor Temperature + id: ac_strange_temp + internal: false display_state: - name: AC Display - id: ac_display + name: AC Display State + id: ac_display_state + internal: false + defrost_state: + name: AC Defrost State + id: ac_defrost_state + internal: false + invertor_power: + name: AC Invertor Power + id: ac_invertor_power + internal: false + preset_reporter: + name: AC Preset Reporter + id: ac_preset_reporter internal: false visual: min_temperature: 16 max_temperature: 32 - temperature_step: 0.5 + temperature_step: 1 supported_modes: - HEAT_COOL - COOL @@ -111,7 +140,6 @@ climate: - SLEEP custom_presets: - CLEAN - - FEEL - HEALTH - ANTIFUNGUS supported_swing_modes: @@ -131,20 +159,28 @@ climate: - COOLING: AC is cooling the air. The same thing will be in HEAT or COOL modes, with the only difference of the list of actions (IDLE + HEATING or IDLE + COOLING). - **display_inverted** (*Optional*, boolean, default ``false``): It configures display driver logic level. As it turned out in the issue [#31](https://github.com/GrKoR/esphome_aux_ac_component/issues/31), different models of conditioners manage display different way. Rovex ACs powers off display by bit `1` in command packet and power it on by bit `0`. Many other conditioners do this vice versa. -- **indoor_temperature** (*Optional*): The information for the air temperature sensor +- **indoor_temperature** (*Optional*): Parameters of the room air temperature sensor. - **name** (**Required**, string): The name for the temperature sensor. - **id** (*Optional*, [ID](https://esphome.io/guides/configuration-types.html#config-id)): Set the ID of this sensor for use in lambdas. - **internal** (*Optional*, boolean): Mark this component as internal. Internal components will not be exposed to the frontend (like Home Assistant). As opposed to default [Sensor](https://esphome.io/components/sensor/index.html#base-sensor-configuration) behaviour this variable is **always true** except in cases where the user has set it directly. - All other options from [Sensor](https://esphome.io/components/sensor/index.html#base-sensor-configuration). +- **outdoor_temperature** (*Optional*): Parameters of the outdoor temperature sensor. Thay are the same as the **indoor_temperature** (see description above). +- **inbound_temperature** (*Optional*): Parameters of the coolant inbound temperature sensor. Thay are the same as the **indoor_temperature** (see description above). +- **outbound_temperature** (*Optional*): Parameters of the coolant outbound temperature sensor. Thay are the same as the **indoor_temperature** (see description above). +- **compressor_temperature** (*Optional*): Parameters of the compressor temperature sensor. Thay are the same as the **indoor_temperature** (see description above). - **display_state** (*Optional*): The information for the HVAC display state sensor (is display ON or OFF) - **name** (**Required**, string): The name for the display state sensor. - **id** (*Optional*, [ID](https://esphome.io/guides/configuration-types.html#config-id)): Set the ID of this sensor for use in lambdas. - **internal** (*Optional*, boolean): Mark this component as internal. Internal components will not be exposed to the frontend (like Home Assistant). As opposed to default [Binary Sensor](https://esphome.io/components/binary_sensor/index.html#base-binary-sensor-configuration) behaviour this variable is **always true** except in cases where the user has set it directly. - All other options from [Binary Sensor](https://esphome.io/components/binary_sensor/index.html#base-binary-sensor-configuration). +- **defrost_state** (*Optional*): The information for the HVAC defrost function state sensor (is it ON or OFF). All settings are the same as for the **display_state** (see description above). +- **invertor_power** (*Optional*): The information for the invertor power sensor. All settings are the same as for the **display_state** (see description above). +- **preset_reporter** (*Optional*): Parameters of text sensor with current preset. All settings are the same as for the **display_state** (see description above). +ESPHome Climate devices are not report their active presets (from **supported_presets** and **custom_presets** lists) to MQTT. In case you are using mqtt and want to receive information about active preset you should declare this sensor in your yaml. - **supported_modes** (*Optional*, list): List of supported modes. Possible values are: ``HEAT_COOL``, ``COOL``, ``HEAT``, ``DRY``, ``FAN_ONLY``. Please note: some manufacturers call AUTO mode instead of HEAT_COOL. Defaults to ``FAN_ONLY``. - **custom_fan_modes** (*Optional*, list): List of supported custom fan modes. Possible values are: ``MUTE``, ``TURBO``. No custom fan modes by default. - **supported_presets** (*Optional*, list): List of supported presets. Possible values are: ``SLEEP``. No presets by default. -- **custom_presets** (*Optional*, list): List of supported custom presets. Possible values are: ``CLEAN``, ``FEEL``, ``HEALTH``, ``ANTIFUNGUS``. Please note: presets ``FEEL``, ``HEALTH`` and ``ANTIFUNGUS`` have not been implemented yet. No custom presets by default. +- **custom_presets** (*Optional*, list): List of supported custom presets. Possible values are: ``CLEAN``, ``HEALTH``, ``ANTIFUNGUS``. No custom presets by default. - **supported_swing_modes** (*Optional*, list): List of supported swing modes. Possible values are: ``VERTICAL``, ``HORIZONTAL``, ``BOTH``. No swing modes by default. - All other options from [Climate](https://esphome.io/components/climate/index.html#base-climate-configuration). @@ -169,6 +205,77 @@ on_...: ``` - **aux_id** (**Requared**, string): ID of `aux_ac` component. +### ``aux_ac.vlouver_stop`` ### +This action stops vertical swing of louvers. + +```yaml +on_...: + then: + - aux_ac.vlouver_stop: aux_id +``` +- **aux_id** (**Requared**, string): ID of `aux_ac` component. + +### ``aux_ac.vlouver_swing`` ### +This action starts vertical swing of louvers. + +```yaml +on_...: + then: + - aux_ac.vlouver_swing: aux_id +``` +- **aux_id** (**Requared**, string): ID of `aux_ac` component. + +### ``aux_ac.vlouver_top`` ### +This action moves HVAC louvers to the topmost position. + +```yaml +on_...: + then: + - aux_ac.vlouver_top: aux_id +``` +- **aux_id** (**Requared**, string): ID of `aux_ac` component. + +### ``aux_ac.vlouver_middle_above`` ### +This action moves HVAC louvers to the position one step under the topmost. + +```yaml +on_...: + then: + - aux_ac.vlouver_middle_above: aux_id +``` +- **aux_id** (**Requared**, string): ID of `aux_ac` component. + +### ``aux_ac.vlouver_middle`` ### +This action moves HVAC louvers to the middle position. + +```yaml +on_...: + then: + - aux_ac.vlouver_middle: aux_id +``` +- **aux_id** (**Requared**, string): ID of `aux_ac` component. + +### ``aux_ac.vlouver_middle_below`` ### +This action moves HVAC louvers to the position one step under the middle position. + +```yaml +on_...: + then: + - aux_ac.vlouver_middle_below: aux_id +``` +- **aux_id** (**Requared**, string): ID of `aux_ac` component. + +### ``aux_ac.vlouver_bottom`` ### +This action moves HVAC louvers to the lowest position. + +```yaml +on_...: + then: + - aux_ac.vlouver_bottom: aux_id +``` +- **aux_id** (**Requared**, string): ID of `aux_ac` component. + + ## Simple example ## The source code of this example is located in the [aux_ac_simple.yaml](https://github.com/GrKoR/esphome_aux_ac_component/blob/master/examples/simple/aux_ac_simple.yaml) file. diff --git a/README.md b/README.md index 60e4d9c..c9a1cfd 100644 --- a/README.md +++ b/README.md @@ -98,15 +98,44 @@ climate: indoor_temperature: name: AC Indoor Temperature id: ac_indoor_temp - internal: true + accuracy_decimals: 1 + internal: false + outdoor_temperature: + name: AC Outdoor Temperature + id: ac_outdoor_temp + internal: false + outbound_temperature: + name: AC Colant Outbound Temperature + id: ac_outbound_temp + internal: false + inbound_temperature: + name: AC Colant Inbound Temperature + id: ac_inbound_temp + internal: false + compressor_temperature: + name: AC Compressor Temperature + id: ac_strange_temp + internal: false display_state: - name: AC Display - id: ac_display + name: AC Display State + id: ac_display_state + internal: false + defrost_state: + name: AC Defrost State + id: ac_defrost_state + internal: false + invertor_power: + name: AC Invertor Power + id: ac_invertor_power + internal: false + preset_reporter: + name: AC Preset Reporter + id: ac_preset_reporter internal: false visual: min_temperature: 16 max_temperature: 32 - temperature_step: 0.5 + temperature_step: 1 supported_modes: - HEAT_COOL - COOL @@ -120,7 +149,6 @@ climate: - SLEEP custom_presets: - CLEAN - - FEEL - HEALTH - ANTIFUNGUS supported_swing_modes: @@ -145,15 +173,23 @@ climate: - **id** (*Опциональный*, [ID](https://esphome.io/guides/configuration-types.html#config-id)): Можно указать свой ID для датчика для использования в лямбдах. - **internal** (*Опциональный*, логическое): Пометить данный датчик как внутренний. Внутренний датчик не будет передаваться во фронтэнд (такой как Home Assistant). В противоположность стандартному поведению [сенсоров](https://esphome.io/components/sensor/index.html#base-sensor-configuration) этот параметр для датчика в кондиционере **всегда выставлен в true** за исключением случаев, когда пользователь не установил его в `false`. То есть по умолчанию значение сенсора не будет передаваться во фронтенд даже если указано `name` для сенсора. - Все остальные параметры [сенсора](https://esphome.io/components/sensor/index.html#base-sensor-configuration) ESPHome. +- **outdoor_temperature** (*Опциональный*): Параметры создаваемого датчика уличной температуры воздуха, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_temperature** (см. выше). +- **inbound_temperature** (*Опциональный*): Параметры создаваемого датчика температуры на подаче теплоносителя, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_temperature** (см. выше). +- **outbound_temperature** (*Опциональный*): Параметры создаваемого датчика температуры на обратке теплоносителя, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_temperature** (см. выше). +- **compressor_temperature** (*Опциональный*): Параметры создаваемого датчика температуры компрессора, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_temperature** (см. выше). - **display_state** (*Опциональный*): Параметры создаваемого датчика дисплея (включен или выключен), если такой датчик нужен. - **name** (**Обязательный**, строка): Имя датчика дисплея. - **id** (*Опциональный*, [ID](https://esphome.io/guides/configuration-types.html#config-id)): Можно указать свой ID для датчика для использования в лямбдах. - **internal** (*Опциональный*, логическое): Пометить данный датчик как внутренний. Внутренний датчик не будет передаваться во фронтэнд (такой как Home Assistant). В противоположность стандартному поведению [бинарных сенсоров](https://esphome.io/components/binary_sensor/index.html#base-binary-sensor-configuration) этот параметр для датчика в кондиционере **всегда выставлен в true** за исключением случаев, когда пользователь не установил его в `false`. То есть по умолчанию значение сенсора не будет передаваться во фронтенд даже если указано `name` для сенсора. - Все остальные параметры [бинарного сенсора](https://esphome.io/components/binary_sensor/index.html#base-binary-sensor-configuration) ESPHome. +- **defrost_state** (*Опциональный*): Параметры создаваемого датчика состояния разморозки (включена или выключена), если такой датчик нужен. Параметры аналогичны датчику дисплея **display_state**. +- **invertor_power** (*Опциональный*): Параметры создаваемого датчика мощности инвертора, если такой датчик нужен. Параметры аналогичны датчику дисплея **display_state**. +- **preset_reporter** (*Опциональный*): Параметры создаваемого текстового датчика текущего активного пресета. Параметры аналогичны датчику дисплея **display_state**. +Климатические устройства ESPHome не отправляют по MQTT активный пресет (см. **supported_presets** и **custom_presets**), в котором работает устройство. Если вы используете MQTT и хотите получать информацию о пресетах, то пропишите этот датчик в конфигурации. - **supported_modes** (*Опциональный*, список): Список поддерживаемых режимов работы. Возможные значения: ``HEAT_COOL``, ``COOL``, ``HEAT``, ``DRY``, ``FAN_ONLY``. Обратите внимание: некоторые производители кондиционеров указывают на пульте режим AUTO, хотя по факту этот режим не работает по расписанию и только лишь поддерживает целевую температуру. Такой режим в ESPHome называется HEAT_COOL. По умолчанию список содержит только значение ``FAN_ONLY``. - **custom_fan_modes** (*Опциональный*, список): Список поддерживаемых дополнительных режимов вентилятора. Возможные значения: ``MUTE``, ``TURBO``. По умолчанию никакие дополнительные режимы не установлены. - **supported_presets** (*Опциональный*, список): Список поддерживаемых базовых функций кондиционера. Возможные значения: ``SLEEP``. По умолчанию никакие базовые функции не установлены. -- **custom_presets** (*Опциональный*, список): Список поддерживаемых дополнительных функций кондиционера. Возможные значения: ``CLEAN``, ``FEEL``, ``HEALTH``, ``ANTIFUNGUS``. Обратите внимание: функции ``FEEL``, ``HEALTH`` и ``ANTIFUNGUS`` пока не в компоненте реализованы. По умолчанию никакие дополнительные функции не установлены. +- **custom_presets** (*Опциональный*, список): Список поддерживаемых дополнительных функций кондиционера. Возможные значения: ``CLEAN``, ``HEALTH``, ``ANTIFUNGUS``. По умолчанию никакие дополнительные функции не установлены. - **supported_swing_modes** (*Опциональный*, список): Список поддерживаемых режимов качания шторки. Возможные значения: ``VERTICAL``, ``HORIZONTAL``, ``BOTH``. По умолчанию устанавливается, что качание шторки кондиционером не поддерживается. - Все остальные параметры [климатического устройства](https://esphome.io/components/climate/index.html#base-climate-configuration) ESPHome. @@ -178,6 +214,75 @@ on_...: ``` - **aux_id** (**Обязательный**, строка): ID компонента `aux_ac`. +### ``aux_ac.vlouver_stop`` ### +Остановка вертикального движения жалюзи кондиционера. Если жалюзи качались в вертикальном направлении, то можно их остановить в нужном положении. + +```yaml +on_...: + then: + - aux_ac.vlouver_stop: aux_id +``` +- **aux_id** (**Обязательный**, строка): ID компонента `aux_ac`. + +### ``aux_ac.vlouver_swing`` ### +Включение вертикального качания жалюзи кондиционера. + +```yaml +on_...: + then: + - aux_ac.vlouver_swing: aux_id +``` +- **aux_id** (**Обязательный**, строка): ID компонента `aux_ac`. + +### ``aux_ac.vlouver_top`` ### +Установка жалюзи в самое верхнее положение. + +```yaml +on_...: + then: + - aux_ac.vlouver_top: aux_id +``` +- **aux_id** (**Обязательный**, строка): ID компонента `aux_ac`. + +### ``aux_ac.vlouver_middle_above`` ### +Установка жалюзи во второе сверху положение. Это положение между верхним и средним. + +```yaml +on_...: + then: + - aux_ac.vlouver_middle_above: aux_id +``` +- **aux_id** (**Обязательный**, строка): ID компонента `aux_ac`. + +### ``aux_ac.vlouver_middle`` ### +Установка жалюзи в среднее положение. + +```yaml +on_...: + then: + - aux_ac.vlouver_middle: aux_id +``` +- **aux_id** (**Обязательный**, строка): ID компонента `aux_ac`. + +### ``aux_ac.vlouver_middle_below`` ### +Установка жалюзи в положение ниже среднего. + +```yaml +on_...: + then: + - aux_ac.vlouver_middle_below: aux_id +``` +- **aux_id** (**Обязательный**, строка): ID компонента `aux_ac`. + +### ``aux_ac.vlouver_bottom`` ### +Установка жалюзи в самое нижнее положение. + +```yaml +on_...: + then: + - aux_ac.vlouver_bottom: aux_id +``` +- **aux_id** (**Обязательный**, строка): ID компонента `aux_ac`. diff --git a/components/aux_ac/automation.h b/components/aux_ac/automation.h index a62fb27..3af1b52 100644 --- a/components/aux_ac/automation.h +++ b/components/aux_ac/automation.h @@ -7,6 +7,7 @@ namespace esphome { namespace aux_ac { +// **************************************** DISPLAY ACTIONS **************************************** template class AirConDisplayOffAction : public Action { @@ -31,6 +32,96 @@ namespace aux_ac { AirCon *ac_; }; + + +// **************************************** VERTICAL LOUVER ACTIONS **************************************** + template + class AirConVLouverSwingAction : public Action + { + public: + explicit AirConVLouverSwingAction(AirCon *ac) : ac_(ac) {} + + void play(Ts... x) override { this->ac_->setVLouverSwingSequence(); } + + protected: + AirCon *ac_; + }; + + template + class AirConVLouverStopAction : public Action + { + public: + explicit AirConVLouverStopAction(AirCon *ac) : ac_(ac) {} + + void play(Ts... x) override { this->ac_->setVLouverStopSequence(); } + + protected: + AirCon *ac_; + }; + + template + class AirConVLouverTopAction : public Action + { + public: + explicit AirConVLouverTopAction(AirCon *ac) : ac_(ac) {} + + void play(Ts... x) override { this->ac_->setVLouverTopSequence(); } + + protected: + AirCon *ac_; + }; + + template + class AirConVLouverMiddleAboveAction : public Action + { + public: + explicit AirConVLouverMiddleAboveAction(AirCon *ac) : ac_(ac) {} + + void play(Ts... x) override { this->ac_->setVLouverMiddleAboveSequence(); } + + protected: + AirCon *ac_; + }; + + template + class AirConVLouverMiddleAction : public Action + { + public: + explicit AirConVLouverMiddleAction(AirCon *ac) : ac_(ac) {} + + void play(Ts... x) override { this->ac_->setVLouverMiddleSequence(); } + + protected: + AirCon *ac_; + }; + + template + class AirConVLouverMiddleBelowAction : public Action + { + public: + explicit AirConVLouverMiddleBelowAction(AirCon *ac) : ac_(ac) {} + + void play(Ts... x) override { this->ac_->setVLouverMiddleBelowSequence(); } + + protected: + AirCon *ac_; + }; + + template + class AirConVLouverBottomAction : public Action + { + public: + explicit AirConVLouverBottomAction(AirCon *ac) : ac_(ac) {} + + void play(Ts... x) override { this->ac_->setVLouverBottomSequence(); } + + protected: + AirCon *ac_; + }; + + + +// **************************************** SEND TEST PACKET ACTION **************************************** template class AirConSendTestPacketAction : public Action { diff --git a/components/aux_ac/aux_ac.h b/components/aux_ac/aux_ac.h index fcb3fd2..d78ec54 100644 --- a/components/aux_ac/aux_ac.h +++ b/components/aux_ac/aux_ac.h @@ -12,8 +12,23 @@ #include "esphome/components/uart/uart.h" #include "esphome/components/sensor/sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" #include "esphome/core/helpers.h" +// весь функционал сохранения пресетов прячу под дефайн +//#define PRESETS_SAVING +#ifdef PRESETS_SAVING + #ifdef ESP32 + #include "esphome/core/preferences.h" + #else + #warning "Saving presets does not work with ESP8266" + #endif +#endif + +// раскоментируй ключ HOLMS для вывода лога под Эксель, значение ключа - размер пакетов которые будут видны +//#define HOLMS 19 + + namespace esphome { namespace aux_ac { @@ -33,7 +48,6 @@ public: static const std::string MUTE; static const std::string TURBO; static const std::string CLEAN; - static const std::string FEEL; static const std::string HEALTH; static const std::string ANTIFUNGUS; @@ -49,14 +63,19 @@ public: static const uint32_t AC_STATES_REQUEST_INTERVAL; }; -const std::string Constants::AC_FIRMWARE_VERSION = "0.2.2"; +const std::string Constants::AC_FIRMWARE_VERSION = "0.2.4"; const char *const Constants::TAG = "AirCon"; + +// custom fan modes const std::string Constants::MUTE = "mute"; const std::string Constants::TURBO = "turbo"; -const std::string Constants::CLEAN = "clean"; -const std::string Constants::FEEL = "feel"; -const std::string Constants::HEALTH = "health"; -const std::string Constants::ANTIFUNGUS = "antifugnus"; + +// custom presets +const std::string Constants::CLEAN = "Clean"; +const std::string Constants::HEALTH = "Health"; +const std::string Constants::ANTIFUNGUS = "Antifungus"; + +// params const float Constants::AC_MIN_TEMPERATURE = 16.0; const float Constants::AC_MAX_TEMPERATURE = 32.0; const float Constants::AC_TEMPERATURE_STEP = 0.5; @@ -73,80 +92,68 @@ enum acsm_state : uint8_t { ACSM_SENDING_PACKET, // отправляем пакет сплиту }; -/** - * Кондиционер отправляет пакеты следующей структуры: - * HEADER: 8 bytes - * BODY: 0..24 bytes - * CRC: 2 bytes - * Весь пакет максимум 34 байта - * По крайней мере все встреченные мной пакеты имели такой размер и структуру. - **/ +// структура пакета описана тут: +// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_structure #define AC_HEADER_SIZE 8 -#define AC_MAX_BODY_SIZE 24 + +// стандартно длина пакета не более 34 байт +// но встретилось исключение Royal Clima (как минимум, модель CO-D xxHNI) - у них 35 байт +// поэтому буффер увеличен #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 мсек вполне хватает на большие пакеты + * По расчетам выходит: + * - получение и обработка посимвольно не должна длиться дольше 600 мсек. + * - получение и обработка пакетов целиком не должна длиться дольше 150 мсек. + * Мы будем обрабатывать пакетами. **/ -#define AC_PACKET_TIMEOUT 150 // 150 мсек - отработка буфера UART за раз, 600 мсек - отработка буфера UART по 1 байту за вызов loop +#define AC_PACKET_TIMEOUT 150 // типы пакетов -#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 // какой-то странный пакет, отправляемый пультом при инициации и иногда при включении питания... как работает и зачем нужен - не разбирался, сплит на него вроде бы не реагирует +// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_types +#define AC_PTYPE_PING 0x01 // ping-пакет +#define AC_PTYPE_CMD 0x06 // команда сплиту +#define AC_PTYPE_INFO 0x07 // информационный пакет +#define AC_PTYPE_INIT 0x09 // инициирующий пакет +#define AC_PTYPE_UNKN 0x0b // какой-то странный пакет // типы команд -#define AC_CMD_STATUS_BIG 0x21 // большой пакет статуса кондиционера -#define AC_CMD_STATUS_SMALL 0x11 // маленький пакет статуса кондиционера -#define AC_CMD_STATUS_PERIODIC 0x2C // иногда встречается, сплит её рассылает по своему разумению; (вроде бы может быть и другой код! надо больше данных) +// смотреть тут: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_type_cmd #define AC_CMD_SET_PARAMS 0x01 // команда установки параметров кондиционера +#define AC_CMD_STATUS_SMALL 0x11 // маленький пакет статуса кондиционера +#define AC_CMD_STATUS_BIG 0x21 // большой пакет статуса кондиционера +// TODO: Нужно посмотреть, где используется AC_CMD_STATUS_PERIODIC, и изменить логику. +// на сегодня уже известно, что периодически рассылаются команды в диапазоне 0x20..0x2F +#define AC_CMD_STATUS_PERIODIC 0x2C // иногда встречается // значения байтов в пакетах #define AC_PACKET_START_BYTE 0xBB // Стартовый байт любого пакета 0xBB, других не встречал #define AC_PACKET_ANSWER 0x80 // признак ответа wifi-модуля // заголовок пакета +// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_header 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; // не расшифрован + uint8_t start_byte = AC_PACKET_START_BYTE; + uint8_t _unknown1; + uint8_t packet_type; + uint8_t wifi; + uint8_t ping_answer_01; + uint8_t _unknown2; + uint8_t body_length; + uint8_t _unknown3; }; // CRC пакета +// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_crc union packet_crc_t { uint16_t crc16; uint8_t crc[2]; }; +// структура пекета struct packet_t { uint32_t msec; // значение millis в момент определения корректности пакета packet_header_t * header; @@ -157,129 +164,179 @@ struct packet_t { }; // тело ответа на пинг +// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_type_ping struct packet_ping_answer_body_t { - uint8_t byte_1C; // первый байт всегда 0x1C - uint8_t byte_27; // второй байт тела пинг-ответа всегда 0x27 - uint8_t zero1; // всегда 0x00 - uint8_t zero2; // всегда 0x00 - uint8_t zero3; // всегда 0x00 - uint8_t zero4; // всегда 0x00 - uint8_t zero5; // всегда 0x00 - uint8_t zero6; // всегда 0x00 + uint8_t byte_1C = 0x1C; + uint8_t byte_27 = 0x27; + uint8_t zero1 = 0; + uint8_t zero2 = 0; + uint8_t zero3 = 0; + uint8_t zero4 = 0; + uint8_t zero5 = 0; + uint8_t zero6 = 0; }; // тело большого информационного пакета +// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21 struct packet_big_info_body_t { - uint8_t byte_01; // всегда 0x01 - uint8_t cmd_answer; // код команды, ответом на которую пришел данный пакет (0x21); - // пакет может рассылаться и в дежурном режиме (без запроса со стороны wifi-модуля) - // в этом случае тут могут быть значения, отличные от 0x21 - uint8_t byte_C0; // не расшифрован, всегда 0xC0 - // для RoyalClima18HNI: всегда 0xE0 - uint8_t unknown1; // не расшифрован, как-то связан с режимом работы сплита; как вариант, отражает режим работы - // компрессора во внешнем блоке или что-то такое, потому что иногда включение сплита не сразу приводит к изменениям в этом байте - // - // Встречались такие значения: - // 0x04 - сплит выключен, до этого работал (статус держится 1 час после выкл.) - // 0x05 - режим AUTO - // 0x24 - режим OFF - // 0x25 - режим COOL - // 0x39 - ?? - // 0x45 - режим DRY - // 0x85 - режим HEAT - // 0xC4 - режим OFF, выключен давно, зима - // 0xC5 - режим FAN - uint8_t zero1; // всегда 0x00 - // для RoyalClima18HNI: режим разморозки внешнего блока - 0x20, в других случаях 0x00 - uint8_t fanSpeed; // в ответах на команды wifi-модуля в этом байте скорость работы вентилятора - // fanSpeed: OFF=0x00, LOW=0x02, MID=0x04, HIGH=0x06, TURBO=0x07; режим CLEAN=0x01 - // в дежурных пакетах тут похоже что-то другое - uint8_t zero2; // всегда 0x00 - uint8_t ambient_temperature_int; // целая часть комнатной температуры воздуха с датчика на внутреннем блоке сплит-системы - // перевод по формуле T = Тin - 0x20 + Tid/10 - // где - // Tin - целая часть температуры - // Tid - десятичная часть температуры - uint8_t zero3; // всегда 0x00 - uint8_t outdoor_temperature; // этот байт как-то связан с температурой во внешнем блоке. Требуются дополнительные исследования. - // При выключенном сплите характер изменения значения примерно соответствует изменению температуры на улице. - // При включенном сплите значение может очень сильно скакать. - // По схеме wiring diagram сплит-системы, во внешнем блоке есть термодатчик, отслеживающий температуру испарителя. - // Возможно, этот байт как раз и отражает изменение температуры на испарителе. - // Но я не смог разобраться, как именно перевести эти значения в градусы. - // Кроме того, зимой даже в минусовую температуру этот байт не уходит ниже 0x33 по крайней мере - // для температур в диапазоне -5..-10 градусов Цельсия. - uint8_t zero4; // всегда 0x00 - uint8_t zero5; // всегда 0x00 - uint8_t zero6; // всегда 0x00 - // для RoyalClima18HNI: похоже на какую-то температуру, точно неизвестно - // температура внешнего теплообменника влияет на это значение (при работе на обогрев - понижает, при охлаждении или при разморозке - повышает) - uint8_t zero7; // всегда 0x00 - // для RoyalClima18HNI: 0x20 - uint8_t zero8; // всегда 0x00 - // для RoyalClima18HNI: 0x20 - uint8_t zero9; // всегда 0x00 - uint8_t zero10; // всегда 0x00 - // для RoyalClima18HNI: мощность инвертора (от 0 до 100) в % - // например, разморозка внешнего блока происходит при 80% - uint8_t zero11; // всегда 0x00 - uint8_t zero12; // всегда 0x00 - uint8_t zero13; // всегда 0x00 - uint8_t zero14; // всегда 0x00 + // байт 0 тела (байт 8 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b08 + uint8_t byte_01 = 0x01; + + // байт 1 тела (байт 9 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b09 + uint8_t cmd_answer; + + // байт 2 тела (байт 10 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b10 + uint8_t reserv20 :2; + bool is_invertor_periodic :1; // флаг периодического пакета инверторного кондиционера + uint8_t reserv23 :2; + bool is_invertor :1; // флаг инвертора + uint8_t reserv26 :2; + + // байт 3 тела (байт 11 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b11 + bool power :1; + bool sleep :1; + bool v_louver :1; + bool h_louver :1; + bool louvers_on :1; + uint8_t mode :3; + // #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_POWER b00000001 + // #define AC_BIG_MASK_LOUVERS_ON b00010000 + // #define AC_BIG_MASK_LOUVERS_H b00000100 + // #define AC_BIG_MASK_LOUVERS_L b00001000 + // #define AC_BIG_MASK_SLEEP b00000010 + // #define AC_BIG_MASK_COOL b00100000 + + // байт 4 тела (байт 12 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b12 + uint8_t reserv40 :4; + bool needDefrost :1; + bool defrostMode :1; + bool reserv46 :1; + bool clean :1; + // Для кондея старт-стоп + // x xx + // C5 1100 0101 + // C4 1100 0100 + // 85 1000 0101 + // 84 1000 0100 + // 3D 0011 1101 + // 3C 0011 1100 + // 25 0010 0101 + // 24 0010 0100 + // 5 0000 0101 + // 4 0000 0100 + + // байт 5 тела (байт 13 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b13 + uint8_t realFanSpeed:3; // реальная (не заданная) скорость вентилятора + uint8_t reserv53:5; + // в дежурных пакетах тут похоже что-то другое + + // байт 6 тела (байт 14 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b14 + + bool reserv60:1; + uint8_t fanPWM:7; // ШИМ вентилятора + + // байт 7 тела (байт 15 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b15 + uint8_t ambient_temperature_int; + + // байт 8 тела (байт 16 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b16 + uint8_t zero3; // не расшифрован, у кого-то всегда 0x00, у кого-то повторяет значение байта 17 пакета. Непонятно. + + // байт 9 тела (байт 17 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b17 + uint8_t in_temperature_int; // какая-то температура, детали см. в описании на гитхабе + + // байт 10 тела (байт 18 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b18 + uint8_t zero4; // не расшифрован + + // байт 11 тела (байт 19 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b19 + uint8_t zero5; // всегда 0x00 или 0x64 + + // байт 12 тела (байт 20 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b20 + uint8_t outdoor_temperature; // Внешняя температура; формула T - 0x20 + + // байт 13 тела (байт 21 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b21 + uint8_t out_temperature_int; // похоже на температуру обратки, T - 0x20 + + // байт 14 тела (байт 22 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b22 + uint8_t compressor_temperature_int; // от режима не зависит, растет при включении инвертора; температура двигателя? + + // байт 15 тела (байт 23 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b23 + uint8_t zero9; // не расшифрован, подробнее в описании + + // байт 16 тела (байт 24 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b24 + uint8_t invertor_power; // мощность инвертора (от 0 до 100) в % + + // байт 17 тела (байт 25 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b25 + uint8_t zero11; // не расшифрован, подробнее в описании. + + // байт 18 тела (байт 26 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b26 + uint8_t zero12; // не расшифрован, подробнее в описании. + + // байт 19 тела (байт 27 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b27 + uint8_t zero13; // не расшифрован, подробнее в описании. + + // байт 20 тела (байт 28 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b28 + uint8_t zero14; // не расшифрован, подробнее в описании. + + // байт 21 тела (байт 29 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b29 uint8_t zero15; // всегда 0x00 + + // байт 22 тела (байт 30 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b30 uint8_t zero16; // всегда 0x00 - uint8_t ambient_temperature_frac; // младшие 4 бита - дробная часть комнатной температуры воздуха с датчика на внутреннем блоке сплит-системы - // подробнее смотреть ambient_temperature_int - // для RoyalClima18HNI: старшие 4 бита - 0x2 + + // байт 23 тела (байт 31 пакета) + // https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b31 + uint8_t ambient_temperature_frac:4; // дробная часть комнатной температуры воздуха с датчика на внутреннем блоке сплит-системы + uint8_t reserv234:1; + bool unknown:1; // для `Royal Clima 18HNI` в этом бите `1`. Не понятно, что это значит. У других сплитов такое не встречалось. + uint8_t reserv236:2; }; // тело малого информационного пакета +// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11 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; остальные не известны - 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 byte_01; + uint8_t cmd_answer; + uint8_t target_temp_int_and_v_louver; + uint8_t h_louver; + uint8_t target_temp_frac; + uint8_t fan_speed; + uint8_t fan_turbo_and_mute; + uint8_t mode; uint8_t zero1; // всегда 0x00 uint8_t zero2; // всегда 0x00 - uint8_t status; // бит 5 = 1: включен, обычный режим работы (когда можно включить нагрев, охлаждение и т.п.) - // бит 2 = 1: режим самоочистки, должен запускаться только при бит 5 = 0 - // бит 0 и бит 1: активация режима ионизатора воздуха (не проверен, у меня его нет) + uint8_t status; uint8_t zero3; // всегда 0x00 - uint8_t display_and_mildew; // бит4 = 1, чтобы погасить дисплей на внутреннем блоке сплита - // бит3 = 1, чтобы включить функцию "антиплесень" (после отключения как-то прогревает или просушивает теплообменник, чтобы на нем не росла плесень) + uint8_t display_and_mildew; uint8_t zero4; // всегда 0x00 - uint8_t target_temp_frac2; // дробная часть целевой температуры, может быть только 0x00 и 0x05 - // при установке температуры тут 0x00, а заданная температура передается в target_temp_int_and_v_louver и target_temp_frac - // после установки сплит в информационных пакетах тут начинает показывать дробную часть - // не очень понятно, зачем так сделано + uint8_t target_temp_frac2; }; @@ -288,6 +345,7 @@ struct packet_small_info_body_t { //*************************************************** ПАРАМЕТРЫ РАБОТЫ КОНДИЦИОНЕРА ****************************************************************** //**************************************************************************************************************************************************** // для всех параметров ниже вариант 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 }; @@ -296,12 +354,12 @@ enum ac_power : uint8_t { AC_POWER_OFF = 0x00, AC_POWER_ON = 0x20, AC_POWER_UNTO #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 }; @@ -309,6 +367,14 @@ enum ac_health_status : uint8_t { AC_HEALTH_STATUS_OFF = 0x00, AC_HEALTH_STATUS_ #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 }; @@ -320,17 +386,22 @@ enum ac_mode : uint8_t { AC_MODE_AUTO = 0x00, AC_MODE_COOL = 0x20, AC_MODE_DRY = #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 }; +enum ac_louver_V : uint8_t { + AC_LOUVERV_SWING_UPDOWN = 0x00, + AC_LOUVERV_SWING_TOP = 0x01, + AC_LOUVERV_SWING_MIDDLE_ABOVE = 0x02, + AC_LOUVERV_SWING_MIDDLE = 0x03, + AC_LOUVERV_SWING_MIDDLE_BELOW = 0x04, + AC_LOUVERV_SWING_BOTTOM = 0x05, + // 0x06 ничего не даёт, протестировано + 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 }; @@ -351,6 +422,9 @@ enum ac_fanturbo : uint8_t { AC_FANTURBO_OFF = 0x00, AC_FANTURBO_ON = 0x40, AC_F #define AC_FANMUTE_MASK 0b10000000 enum ac_fanmute : uint8_t { AC_FANMUTE_OFF = 0x00, AC_FANMUTE_ON = 0x80, AC_FANMUTE_UNTOUCHED = 0xFF }; +// реальная скорость вентилятора +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 }; + // включение-выключение дисплея на корпусе внутреннего блока #define AC_DISPLAY_MASK 0b00010000 enum ac_display : uint8_t { AC_DISPLAY_OFF = 0x00, AC_DISPLAY_ON = 0x10, AC_DISPLAY_UNTOUCHED = 0xFF }; @@ -361,29 +435,70 @@ enum ac_display : uint8_t { AC_DISPLAY_OFF = 0x00, AC_DISPLAY_ON = 0x10, AC_DISP #define AC_MILDEW_MASK 0b00001000 enum ac_mildew : uint8_t { AC_MILDEW_OFF = 0x00, AC_MILDEW_ON = 0x08, AC_MILDEW_UNTOUCHED = 0xFF }; +// маска счетчика минут прошедших с последней команды +// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b12 +// GK: define убрал, т.к. считаю, что сбрасывать счетчик не надо. +// #define AC_MIN_COUNTER_MASK 0b00111111 + /** команда для кондиционера * * ВАЖНО! В коде используется копирование команд простым присваиванием. * Если в структуру будут введены указатели, то копирование надо будет изменить! */ + +//***************************************************************************** +// структура для сохранения настроек, специально вынесено в макрос, чтобы использовать в нескольких местах +// сделано Brokly для того, чтобы поведение wifi-модуля походило на ИК-пульт (для каждого режима сохранялись свои настройки температуры и прочего) +#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 + +#if defined(PRESETS_SAVING) + // структура для сохранения данных + 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 + }; +#endif +//***************************************************************************** + struct ac_command_t { - ac_power power; - float temp_target; - bool temp_target_matter; // показывает, задана ли температура. Если false, то оставляем уже установленную - float temp_ambient; - float temp_outdoor; - ac_clean clean; - ac_health health; + AC_COMMAND_BASE; ac_health_status health_status; - ac_mode mode; - ac_sleep sleep; - ac_ifeel iFeel; - ac_louver louver; - ac_fanspeed fanSpeed; - ac_fanturbo fanTurbo; - ac_fanmute fanMute; - ac_display display; - ac_mildew mildew; + float temp_ambient; // внутренняя температура + int8_t temp_outdoor; // внешняя температура + int8_t temp_inbound; // температура входящая + int8_t temp_outbound; // температура исходящая + int8_t temp_compressor; // температура компрессора + ac_realFan realFanSpeed; // текущая скорость вентилятора + uint8_t invertor_power; // мощность инвертора + bool defrost; // режим разморозки внешнего блока (накопление тепла + прогрев испарителя) }; typedef ac_command_t ac_state_t; // текущее состояние параметров кондея можно хранить в таком же формате, как и комманды @@ -418,15 +533,12 @@ typedef ac_command_t ac_state_t; // текущее состояние пара // максимальная длина последовательности; больше вроде бы не требовалось #define AC_SEQUENCE_MAX_LEN 0x0F -// в пакетах никогда не встречалось значение 0xFF (только в CRC), поэтому решено его использовать как признак не важного значение байта -//#define AC_SEQUENCE_ANY_BYTE 0xFF - // дефолтный таймаут входящего пакета в миллисекундах // если для входящего пакета в последовательности указан таймаут 0, то используется значение по-умолчанию // если нужный пакет не поступил в течение указанного времени, то последовательность прерывается с ошибкой -#define AC_SEQUENCE_DEFAULT_TIMEOUT 500 - -enum sequence_item_type_t : uint8_t { +#define AC_SEQUENCE_DEFAULT_TIMEOUT 580 // Brokly: пришлось увеличить с 500 до 580 + + enum sequence_item_type_t : uint8_t { AC_SIT_NONE = 0x00, // пустой элемент последовательности AC_SIT_DELAY = 0x01, // пауза в последовательности на нужное количество миллисекунд AC_SIT_FUNC = 0x02 // рабочий элемент последовательности @@ -459,6 +571,25 @@ struct sequence_item_t { class AirCon : public esphome::Component, public esphome::climate::Climate { private: + + #if defined(PRESETS_SAVING) + // массив для сохранения данных глобальных персетов + ac_save_command_t global_presets[POS_MODE_OFF+1]; + + // тут будем хранить данные глобальных пресетов во флеше + // ВНИМАНИЕ на данный момент 22.05.22 ESPHOME 20022.5.0 имеет ошибку + // траблтикет: https://github.com/esphome/issues/issues/3298 + // из-за этого сохранение в энергонезависимую память не работает !!! + ESPPreferenceObject storage = global_preferences->make_preference(this->get_object_id_hash(), true); + + // настройка-ключ, для включения сохранения - восстановления настроек каждого + // режима работы в отдельности, то есть каждый режим работы имеет свои настройки + // температуры, шторок, скорости вентилятора, пресетов + bool _store_settings = false; + // флаги для сохранения пресетов + bool _new_command_set = false; // флаг отправки новой команды, необходимо сохранить данные пресета, если разрешено + #endif + // время последнего запроса статуса у кондея uint32_t _dataMillis; // периодичность обновления статуса кондея, по дефолту AC_STATES_REQUEST_INTERVAL @@ -474,12 +605,23 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // если тут true, то 1 потушит дисплей, а 0 включит. bool _display_inverted = false; + // флаг типа кондиционера. инвертор - true, ON/OFF - false, начальная установка false + // в таком режиме точность и скорость определения реального состояния системы для инвертора, + // будет работать, но будет ниже, переменная устанавливается при первом получении большого пакета; + // если эта переменная установлена, то режим работы не инверторного кондиционера будет распознаваться + // как "в простое" (IDLE) + bool _is_invertor = false; + // поддерживаемые кондиционером опции std::set _supported_modes{}; std::set _supported_swing_modes{}; std::set _supported_presets{}; std::set _supported_custom_presets{}; std::set _supported_custom_fan_modes{}; + + // The capabilities of the climate device + // Шаблон параметров отображения виджета + esphome::climate::ClimateTraits _traits; // состояние конечного автомата acsm_state _ac_state = ACSM_IDLE; @@ -522,6 +664,18 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // флаг успешного выполнения стартовой последовательности команд bool _startupSequenceComlete = false; + + // нормализация показаний температуры, приведение в диапазон + float _temp_target_normalise(float temp){ + auto traits = this->get_traits(); + float temp_min = traits.get_visual_min_temperature(); + float temp_max = traits.get_visual_max_temperature(); + if (temp < temp_min) temp = temp_min; + if (temp > temp_max) temp = temp_max; + if (temp < Constants::AC_MIN_TEMPERATURE) temp = Constants::AC_MIN_TEMPERATURE; + if (temp > Constants::AC_MAX_TEMPERATURE) temp = Constants::AC_MAX_TEMPERATURE; + return temp; + } // очистка последовательности команд void _clearSequence(){ @@ -681,17 +835,23 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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_compressor = 0; + cmd->realFanSpeed = AC_REAL_FAN_UNTOUCHED; }; // очистка буфера размером AC_BUFFER_SIZE @@ -797,18 +957,6 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { _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) @@ -918,8 +1066,8 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { _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.header->ping_answer_01 = 0x01; // магия, детали тут: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_type_ping + _outPacket.header->body_length = 8; _outPacket.body = &(_outPacket.data[AC_HEADER_SIZE]); // заполняем тело пакета @@ -942,15 +1090,8 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { _startupSequenceComlete = startupSequence(); } - // изначально предполагал, что передачу пакета на отправку выполнит обработчик IDLE, но показалось, что слишком долго - // логика отправки через IDLE в том, что получение запросов может быть важнее отправки ответов и IDLE позволяет реализовать такой приоритет - // но потом решил всё же напрямую отправлять в отправку - // в этом случае пинг-ответ заканчивает отправку спустя 144 мсек после стартового байта пинг-запроса - //_setStateMachineState(ACSM_IDLE); _setStateMachineState(ACSM_SENDING_PACKET); - // решил провести эксперимент - // получилось от начала запроса до отправки ответа порядка 165 мсек., если отправка идет не сразу, а через состояние IDLE - // Если сразу отсюда отправляться в обработчик отправки, то время сокращается до 131 мсек. Основные потери идут до входа в парсер пакетов + break; } @@ -963,7 +1104,7 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { break; } - case AC_PTYPE_INFO: { // информационный пакет; бывает 3 видов; один из них рассылается кондиционером самостоятельно раз в 10 мин. и все 3 могут быть ответом на запросы модуля + case AC_PTYPE_INFO: { // информационный пакет // смотрим тип поступившего пакета по второму байту тела _debugMsg(F("Parser: status packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); switch (_inPacket.body[1]) { @@ -1009,10 +1150,6 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { stateChangedFlag = stateChangedFlag || (_current_ac_state.sleep != (ac_sleep)stateByte); _current_ac_state.sleep = (ac_sleep)stateByte; - stateByte = small_info_body->mode & AC_IFEEL_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.iFeel != (ac_ifeel)stateByte); - _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; @@ -1039,6 +1176,7 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // уведомляем об изменении статуса сплита if (stateChangedFlag) stateChanged(); + break; } @@ -1051,21 +1189,54 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // будем обращаться к телу пакета через указатель на структуру 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; // некая температура из наружного блока, скорее всего температура испарителя - // TODO: формула расчета неправильная! Нужно исследовать на опыте, какая температура при каких условиях - //stateFloat = big_info_body->outdoor_temperature - 0x20; - stateFloat = big_info_body->outdoor_temperature; + // GK: фильтрацию тут убрал. Лучше это делать в ESPHome. Для этого у сенсора есть возможности. А тут лучше иметь чистые значения для аналлиза. + stateFloat = big_info_body->outdoor_temperature - 0x20; stateChangedFlag = stateChangedFlag || (_current_ac_state.temp_outdoor != stateFloat); _current_ac_state.temp_outdoor = stateFloat; + + // температура входящей магистрали + 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->compressor_temperature_int - 0x20; + stateChangedFlag = stateChangedFlag || (_current_ac_state.temp_compressor != stateFloat); + _current_ac_state.temp_compressor = 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; } @@ -1075,7 +1246,6 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // но я решил этот момент тут не проверять и не контролировать. // корректную установку параметров можно определить, запросив статус кондиционера сразу после получения этой команды кондея // в настоящий момент проверка сделана в механизме sequences - // TODO: если доводить до идеала, то проверку байтов 2 и 3 можно сделать и тут break; } @@ -1132,17 +1302,12 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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; - - // TODO: Пока сделано через Ж* - сообщение копируется в массив и потом выводится.... - // это костыль, чтобы передать неизвестное количество аргументов - char _msg[128]; - msg.toCharArray(_msg, 128); if (line == 0) line = __LINE__; // если строка не передана, берем текущую строку va_list vl; va_start(vl, line); - esp_log_vprintf_(dbgLevel, Constants::TAG, line, _msg, vl); + esp_log_vprintf_(dbgLevel, Constants::TAG, line, msg.c_str(), vl); va_end(vl); } @@ -1180,26 +1345,44 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { } // формируем данные - for (int i=0; ibytesLoaded; 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; + #ifdef HOLMS + // если этот дефайн объявлен, то в лог попадут только пакеты больше указанного в дефайне размера + // при этом весь вывод будет в десятичном виде, данные будут разделены ";" + // и не будет выделения заголовков и CRC квадратными скобками + dbgLevel = ESPHOME_LOG_LEVEL_ERROR; + if(packet->header->body_length > HOLMS){ + for (int i=0; ibytesLoaded; i++){ + sprintf(textBuf, "%03d;", 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 += "]"; + if (line == 0) line = __LINE__; + _debugMsg(st, dbgLevel, line); + } + #else + // если дефайна HOLMS нет, то выводим пакеты в HEX и все подряд + for (int i=0; ibytesLoaded; 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; - st += " "; - } + // для нормальных пакетов надо заключить заголовок в [] + 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); + if (line == 0) line = __LINE__; + _debugMsg(st, dbgLevel, line); + #endif + } /** расчет CRC16 для блока данных data длиной len @@ -1329,7 +1512,6 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { if (cmd == nullptr) return; // команда указана, дополнительно внесем в пакет те параметры, которые установлены в команде - // присваиваем параметры пакета pack->msec = millis(); pack->header->start_byte = AC_PACKET_START_BYTE; @@ -1344,9 +1526,7 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // целевая температура кондиционера 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; - + cmd->temp_target = _temp_target_normalise(cmd->temp_target); // целая часть температуры pack->body[2] = (pack->body[2] & ~AC_TEMP_TARGET_INT_PART_MASK) | (((uint8_t)(cmd->temp_target) - 8) << 3); @@ -1358,6 +1538,10 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { } } + // обнулить счетчик минут с последней команды + // GK: считаю, что так делать не надо. Штатный wifi-модуль не сбрасывает счетчик минут. + // pack->body[4] &= ~ AC_MIN_COUNTER_MASK ; + // вертикальные жалюзи if (cmd->louver.louver_v != AC_LOUVERV_UNTOUCHED){ pack->body[2] = (pack->body[2] & ~AC_LOUVERV_MASK) | cmd->louver.louver_v; @@ -1390,9 +1574,6 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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){ @@ -1401,10 +1582,17 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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; @@ -1415,6 +1603,8 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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); @@ -1505,7 +1695,7 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // проверяем ответ 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.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)); @@ -1598,13 +1788,19 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { } // сенсоры, отображающие параметры сплита - //esphome::sensor::Sensor *sensor_indoor_temperature = new esphome::sensor::Sensor(); esphome::sensor::Sensor *sensor_indoor_temperature_ = nullptr; - // TODO: если расшифруем формулу для уличной температуры, то можно будет вернуть - //esphome::sensor::Sensor *sensor_outdoor_temperature = new esphome::sensor::Sensor(); - + 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_compressor_temperature_ =nullptr; + // текущая мощность компрессора + esphome::sensor::Sensor *sensor_invertor_power_ = nullptr; // бинарный сенсор, отображающий состояние дисплея esphome::binary_sensor::BinarySensor *sensor_display_ = nullptr; + // бинарный сенсор состония разморозки + esphome::binary_sensor::BinarySensor *sensor_defrost_ = nullptr; + // текстовый сенсор, отображающий текущий режим работы сплита + esphome::text_sensor::TextSensor *sensor_preset_reporter_ = nullptr; // загружает на выполнение последовательность команд на включение/выключение табло с температурой @@ -1627,16 +1823,65 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { return true; } + #if defined(PRESETS_SAVING) + // номер глобального пресета от режима работы + 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(PRESETS_SAVING) + _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); + } + } + #endif + 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; @@ -1651,12 +1896,27 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // выполнена ли уже стартовая последовательность команд (сбор информации о статусе кондея) _startupSequenceComlete = false; + + // первоначальная инициализация + this->preset = climate::CLIMATE_PRESET_NONE; + this->custom_preset = (std::string)""; + this->mode = climate::CLIMATE_MODE_OFF; + this->action = climate::CLIMATE_ACTION_IDLE; + this->fan_mode = climate::CLIMATE_FAN_LOW; + this->custom_fan_mode = (std::string)""; }; 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_compressor_temperature_sensor(sensor::Sensor *temperature_sensor) { sensor_compressor_temperature_ = temperature_sensor; } + void set_defrost_state(binary_sensor::BinarySensor *defrost_state_sensor) { sensor_defrost_ = defrost_state_sensor; } 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; } + void set_preset_reporter_sensor(text_sensor::TextSensor *preset_reporter_sensor) { sensor_preset_reporter_ = preset_reporter_sensor; } bool get_hw_initialized(){ return _hw_initialized; }; bool get_has_connection(){ return _has_connection; }; @@ -1666,43 +1926,110 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { return (_sequence[0].item_type != AC_SIT_NONE); } - // вызывается, если параметры кондиционера изменились + // вызывается для публикации нового состояния кондиционера void stateChanged(){ _debugMsg(F("State changed, let's publish it."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); + // TODO: сейчас экшины рассчётные и могут не отражать реального положения дел. + // В протоколе расшифрованы байты, позволяющие выводить реальный экшн. Требуется исправить. + 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 ***************************/ - this->mode = climate::CLIMATE_MODE_OFF; - this->action = climate::CLIMATE_ACTION_OFF; 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 - // TODO: надо реализовать отображение action - this->action = climate::CLIMATE_ACTION_IDLE; + // по факту режим, названный в AUX как AUTO, является режимом HEAT_COOL + this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; case AC_MODE_COOL: this->mode = climate::CLIMATE_MODE_COOL; - // TODO: надо реализовать отображение action - this->action = climate::CLIMATE_ACTION_COOLING; break; case AC_MODE_DRY: this->mode = climate::CLIMATE_MODE_DRY; - // TODO: надо реализовать отображение action - this->action = climate::CLIMATE_ACTION_DRYING; break; case AC_MODE_HEAT: this->mode = climate::CLIMATE_MODE_HEAT; - // TODO: надо реализовать отображение action - this->action = climate::CLIMATE_ACTION_HEATING; break; case AC_MODE_FAN: this->mode = climate::CLIMATE_MODE_FAN_ONLY; - // TODO: надо реализовать отображение action - this->action = climate::CLIMATE_ACTION_FAN; break; default: @@ -1711,9 +2038,6 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { } } else { this->mode = climate::CLIMATE_MODE_OFF; - // TODO: надо реализовать отображение action - // TODO: возможно, тут некорректно. Сплит может быть выключен, но продолжать крутить вентилятор для просушки (MILDEW preset) или очистки (CLEAN preset) - this->action = climate::CLIMATE_ACTION_OFF; } _debugMsg(F("Climate mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->mode); @@ -1745,12 +2069,13 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { _debugMsg(F("Climate fan mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->fan_mode); /*************************** TURBO FAN MODE ***************************/ - // TURBO работает только в режимах COOL и HEAT + // TURBO работает в режимах FAN, COOL, HEAT, HEAT_COOL + // в режиме DRY изменение скорости вентилятора никак не влияло на его скорость, может сплит просто не вышел еще на режим? Надо попробовать долгую работу в этом режиме. switch (_current_ac_state.fanTurbo) { case AC_FANTURBO_ON: - if ((_current_ac_state.mode == AC_MODE_HEAT) || (_current_ac_state.mode == AC_MODE_COOL)) { - this->custom_fan_mode = Constants::TURBO; - } + //if ((_current_ac_state.mode == AC_MODE_HEAT) || (_current_ac_state.mode == AC_MODE_COOL)) { + this->custom_fan_mode = Constants::TURBO; + //} break; case AC_FANTURBO_OFF: @@ -1762,12 +2087,13 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { _debugMsg(F("Climate fan TURBO mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.fanTurbo); /*************************** MUTE FAN MODE ***************************/ - // MUTE работает только в режиме FAN. В режиме COOL кондей команду принимает, но MUTE не устанавливается + // MUTE работает в режиме FAN. В режимах HEAT, COOL, HEAT_COOL не работает. DRY не проверял. + // проверку на несовместимые режимы выпилили, т.к. нет уверенности, что это поведение одинаково для всех switch (_current_ac_state.fanMute) { case AC_FANMUTE_ON: - if (_current_ac_state.mode == AC_MODE_FAN) { + //if (_current_ac_state.mode == AC_MODE_FAN) { this->custom_fan_mode = Constants::MUTE; - } + //} break; case AC_FANMUTE_OFF: @@ -1778,86 +2104,77 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { _debugMsg(F("Climate fan MUTE mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.fanMute); + //======================== ОТОБРАЖЕНИЕ ПРЕСЕТОВ ================================ + /*************************** 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 ) { + + // AC_HEALTH_OFF + // только в том случае, если до этого пресет был установлен + this->custom_preset = (std::string)""; + + } + + _debugMsg(F("Climate HEALTH preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.health); + /*************************** SLEEP PRESET ***************************/ // Комбинируется только с режимами COOL и HEAT. Автоматически выключается через 7 часов. // COOL: температура +1 градус через час, еще через час дополнительные +1 градус, дальше не меняется. // HEAT: температура -2 градуса через час, еще через час дополнительные -2 градуса, дальше не меняется. // Восстанавливается ли температура через 7 часов при отключении режима - не понятно. - switch (_current_ac_state.sleep) { - case AC_SLEEP_ON: - if ( _current_ac_state.mode == AC_MODE_COOL - or _current_ac_state.mode == AC_MODE_HEAT) { - - this->preset = climate::CLIMATE_PRESET_SLEEP; - - } - break; + 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) { + + // AC_SLEEP_OFF + // только в том случае, если до этого пресет был установлен + this->preset = climate::CLIMATE_PRESET_NONE; - case AC_SLEEP_OFF: - default: - if (this->preset == climate::CLIMATE_PRESET_SLEEP) this->preset = climate::CLIMATE_PRESET_NONE; - break; } _debugMsg(F("Climate preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->preset); /*************************** CLEAN CUSTOM PRESET ***************************/ // режим очистки кондиционера, включается (или должен включаться) при AC_POWER_OFF - switch (_current_ac_state.clean) { - case AC_CLEAN_ON: - if (_current_ac_state.power == AC_POWER_OFF) { - - this->custom_preset = Constants::CLEAN; - - } - break; + 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) { + + // AC_CLEAN_OFF + // только в том случае, если до этого пресет был установлен + this->custom_preset = (std::string)""; - case AC_CLEAN_OFF: - default: - if (this->custom_preset == Constants::CLEAN) this->custom_preset = (std::string)""; - break; } _debugMsg(F("Climate CLEAN preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.clean); - /*************************** iFEEL CUSTOM PRESET ***************************/ - // режим поддержки температуры в районе пульта - // TODO: пока не реализован - switch (_current_ac_state.iFeel) { - case AC_IFEEL_ON: - this->custom_preset = Constants::FEEL; - break; - - case AC_IFEEL_OFF: - default: - if (this->custom_preset == Constants::FEEL) this->custom_preset = (std::string)""; - break; - } - - _debugMsg(F("Climate iFEEL preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.iFeel); - - /*************************** HEALTH CUSTOM PRESET ***************************/ - // режим работы ионизатора - // TODO: не реализован, у меня отсутствует. Смотри комменты в секции define - switch (_current_ac_state.health) { - case AC_HEALTH_ON: - this->custom_preset = Constants::HEALTH; - break; - - case AC_HEALTH_OFF: - default: - if (this->custom_preset == Constants::HEALTH) this->custom_preset = (std::string)""; - break; - } - - _debugMsg(F("Climate HEALTH preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.health); - /*************************** ANTIFUNGUS CUSTOM PRESET ***************************/ // пресет просушки кондиционера после выключения // По факту: после выключения сплита он оставляет минут на 5 открытые жалюзи и глушит вентилятор. // Уличный блок при этом гудит и тарахтит. Возможно, прогревается теплообменник для высыхания. // Через некоторое время внешний блок замолкает и сплит закрывает жалюзи. - // TODO: не реализован, у меня отсутствует + // + // Brokly: + // У меня есть на этот режим, конедй реагирует только в выключеном состоянии. Причем пульт шлет + // 5 посылок и при включении и при выключении. Но каких то видимых отличий в работе или в сценарии + // при выключении кондея, я не наблюдаю. На пульте горит пиктограмма этого режима, но просушки + // я не вижу. После выключения , с активированым режимом Anti-FUNGUS, кондей сразу закрывает хлебало + // и затыкается. + // + // GK: оставил возможность включения функции в работающем состоянии, т.к. установка флага должна быть в работающем состоянии, + // а сама функция отработает при выключении сплита. + // У Brokly возможно какие-то особенности кондея. switch (_current_ac_state.mildew) { case AC_MILDEW_ON: this->custom_preset = Constants::ANTIFUNGUS; @@ -1874,21 +2191,34 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { /*************************** LOUVERs ***************************/ this->swing_mode = climate::CLIMATE_SWING_OFF; - if (_current_ac_state.louver.louver_h == AC_LOUVERH_SWING_LEFTRIGHT){ - this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; - } - if (_current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN){ - if (_current_ac_state.louver.louver_h == AC_LOUVERH_SWING_LEFTRIGHT){ - this->swing_mode = climate::CLIMATE_SWING_BOTH; - } else { + if( _current_ac_state.power == AC_POWER_ON) { + if ( _current_ac_state.louver.louver_h == AC_LOUVERH_SWING_LEFTRIGHT && _current_ac_state.louver.louver_v == AC_LOUVERV_OFF ){ + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } else if ( _current_ac_state.louver.louver_h == AC_LOUVERH_OFF && _current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN ){ this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else if ( _current_ac_state.louver.louver_h == AC_LOUVERH_SWING_LEFTRIGHT && _current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN ){ + this->swing_mode = climate::CLIMATE_SWING_BOTH; } } _debugMsg(F("Climate swing mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->swing_mode); /*************************** TEMPERATURE ***************************/ - this->target_temperature = _current_ac_state.temp_target; + if(_current_ac_state.mode == AC_MODE_FAN || _current_ac_state.power == AC_POWER_OFF){ + // в режиме вентилятора и в выключенном состоянии будем показывать текущую температуру + this->target_temperature = _current_ac_state.temp_ambient; + /* + * принудительная установка целевой температуры для режима AUTO (HEAT_COOL) осознанно выпилена. + * как выяснилось, многие сплиты умеют задавать целевую температуру в этом режиме + * но не все. Кто не умеет, возвращает правильную температуру после установки режима. + * Так что проверка в коде не требуется + */ /* + } else if (_current_ac_state.mode == AC_MODE_AUTO ){ + this->target_temperature = 25; // в AUTO зашита температура 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; @@ -1898,16 +2228,49 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { /*********************************************************************/ /*************************** PUBLISH STATE ***************************/ /*********************************************************************/ + this->publish_all_states(); + } + + // публикуем все состояния сенсоров и сплита + void publish_all_states(){ this->publish_state(); // температура в комнате if (sensor_indoor_temperature_ != nullptr) sensor_indoor_temperature_->publish_state(_current_ac_state.temp_ambient); // температура уличного блока - // TODO: если расшифруем формулу для уличной температуры, то можно будет вернуть - //sensor_outdoor_temperature->publish_state(_current_ac_state.temp_outdoor); + 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_compressor_temperature_ != nullptr) + sensor_compressor_temperature_->publish_state(_current_ac_state.temp_compressor); + // мощность инвертора + 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_preset_reporter_ != nullptr) { + std::string state_str = ""; + if (this->preset == climate::CLIMATE_PRESET_SLEEP) { + state_str += "SLEEP"; + } else if (this->custom_preset.has_value() && this->custom_preset.value().length() > 0) { + state_str += this->custom_preset.value().c_str(); + } else { + state_str += "NONE"; + } + sensor_preset_reporter_->publish_state(state_str.c_str()); + } // состояние дисплея - if (sensor_display_ != nullptr) + if (sensor_display_ != nullptr) { switch (_current_ac_state.display) { case AC_DISPLAY_ON: if (this->get_display_inverted()) { @@ -1929,6 +2292,7 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // могут быть и другие состояния, поэтому так break; } + } } // вывод в дебаг текущей конфигурации компонента @@ -1936,8 +2300,14 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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", this->get_show_action() ? "true" : "false"); - ESP_LOGCONFIG(Constants::TAG, " [x] Display inverted: %s", this->get_display_inverted() ? "true" : "false"); + 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())); + + #if defined(PRESETS_SAVING) + ESP_LOGCONFIG(Constants::TAG, " [x] Save settings %s", TRUEFALSE(this->get_store_settings())); + #endif + + 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()) { @@ -1956,6 +2326,115 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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_compressor_temperature_) != nullptr) { + ESP_LOGCONFIG(Constants::TAG, "%s%s '%s'", " ", LOG_STR_LITERAL("Compressor Temperature"), (this->sensor_compressor_temperature_)->get_name().c_str()); + if (!(this->sensor_compressor_temperature_)->get_device_class().empty()) { + ESP_LOGCONFIG(Constants::TAG, "%s Device Class: '%s'", " ", (this->sensor_compressor_temperature_)->get_device_class().c_str()); + } + ESP_LOGCONFIG(Constants::TAG, "%s State Class: '%s'", " ", state_class_to_string((this->sensor_compressor_temperature_)->get_state_class()).c_str()); + ESP_LOGCONFIG(Constants::TAG, "%s Unit of Measurement: '%s'", " ", (this->sensor_compressor_temperature_)->get_unit_of_measurement().c_str()); + ESP_LOGCONFIG(Constants::TAG, "%s Accuracy Decimals: %d", " ", (this->sensor_compressor_temperature_)->get_accuracy_decimals()); + if (!(this->sensor_compressor_temperature_)->get_icon().empty()) { + ESP_LOGCONFIG(Constants::TAG, "%s Icon: '%s'", " ", (this->sensor_compressor_temperature_)->get_icon().c_str()); + } + if (!(this->sensor_compressor_temperature_)->unique_id().empty()) { + ESP_LOGV(Constants::TAG, "%s Unique ID: '%s'", " ", (this->sensor_compressor_temperature_)->unique_id().c_str()); + } + if ((this->sensor_compressor_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()) { @@ -1976,16 +2455,22 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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; + + #if defined(PRESETS_SAVING) + load_preset(&cmd, POS_MODE_OFF); + #endif + this->mode = mode; break; @@ -1993,6 +2478,11 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { hasCommand = true; cmd.power = AC_POWER_ON; cmd.mode = AC_MODE_COOL; + + #if defined(PRESETS_SAVING) + load_preset(&cmd, POS_MODE_COOL); + #endif + this->mode = mode; break; @@ -2000,6 +2490,11 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { hasCommand = true; cmd.power = AC_POWER_ON; cmd.mode = AC_MODE_HEAT; + + #if defined(PRESETS_SAVING) + load_preset(&cmd, POS_MODE_HEAT); + #endif + this->mode = mode; break; @@ -2007,6 +2502,16 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { hasCommand = true; cmd.power = AC_POWER_ON; cmd.mode = AC_MODE_AUTO; + + #if defined(PRESETS_SAVING) + load_preset(&cmd, POS_MODE_AUTO); + #endif + + /* принудительная установка температуры в этом режиме осознанно выпилена + cmd.temp_target = 25; // зависимость от режима HEAT_COOL + cmd.temp_target_matter = true; + cmd.fanTurbo = AC_FANTURBO_OFF; // зависимость от режима HEAT_COOL + */ this->mode = mode; break; @@ -2014,6 +2519,20 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { hasCommand = true; cmd.power = AC_POWER_ON; cmd.mode = AC_MODE_FAN; + + #if defined(PRESETS_SAVING) + load_preset(&cmd, POS_MODE_FAN); + #endif + + cmd.temp_target = _current_ac_state.temp_ambient; // зависимость от режима FAN + cmd.temp_target_matter = true; + // GK: в режиме FAN работает TURBO, так что отключать не нужно! + //cmd.fanTurbo = AC_FANTURBO_OFF; // зависимость от режима FAN + cmd.sleep = AC_SLEEP_OFF; + // GK: для меня AUTO = HIGH. Скорее всего сплит сам меняет скорость. Поэтому ниже закомментировал + /* if(cmd.fanSpeed == AC_FANSPEED_AUTO || _current_ac_state.fanSpeed == AC_FANSPEED_AUTO){ + cmd.fanSpeed = AC_FANSPEED_LOW; // зависимость от режима FAN + } */ this->mode = mode; break; @@ -2021,10 +2540,18 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { hasCommand = true; cmd.power = AC_POWER_ON; cmd.mode = AC_MODE_DRY; + + #if defined(PRESETS_SAVING) + load_preset(&cmd, POS_MODE_DRY); + #endif + + cmd.fanTurbo = AC_FANTURBO_OFF; // зависимость от режима DRY + cmd.sleep = AC_SLEEP_OFF; // зависимость от режима DRY this->mode = mode; break; - case climate::CLIMATE_MODE_AUTO: // этот режим в будущем можно будет использовать для автоматического пресета (ПИД-регулятора, например) + // другие возможные значения (чтоб не забыть) + //case climate::CLIMATE_MODE_AUTO: // этот режим в будущем можно будет использовать для автоматического пресета (ПИД-регулятора, например) default: break; } @@ -2034,12 +2561,11 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { // User requested fan_mode change 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: 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; @@ -2048,7 +2574,6 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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; @@ -2057,7 +2582,6 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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; @@ -2066,84 +2590,113 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { 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: + // другие возможные значения (чтобы не забыть) + //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. + // TURBO fan mode is suitable in COOL and HEAT modes. // Other modes don't accept TURBO fan mode. - // May be other AUX-based air conditioners do the same. + /* if ( cmd.mode == AC_MODE_COOL or cmd.mode == AC_MODE_HEAT or _current_ac_state.mode == AC_MODE_COOL or _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 - or _current_ac_state.mode == AC_MODE_FAN) { + //if ( cmd.mode == AC_MODE_FAN + // or _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__); - } + //} 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. Автоматически выключается через 7 часов. + // Ночной режим (SLEEP). + // По инструкциям комбинируется только с режимами COOL и HEAT. Автоматически выключается через 7 часов. + // Brokly: вроде как работает еще и с AUTO и DRY // COOL: температура +1 градус через час, еще через час дополнительные +1 градус, дальше не меняется. // HEAT: температура -2 градуса через час, еще через час дополнительные -2 градуса, дальше не меняется. // Восстанавливается ли температура через 7 часов при отключении режима - не понятно. if ( cmd.mode == AC_MODE_COOL or cmd.mode == AC_MODE_HEAT + or cmd.mode == AC_MODE_DRY + or cmd.mode == AC_MODE_AUTO or _current_ac_state.mode == AC_MODE_COOL - or _current_ac_state.mode == AC_MODE_HEAT) { + or _current_ac_state.mode == AC_MODE_HEAT + or _current_ac_state.mode == AC_MODE_DRY + or _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 and HEAT modes only."), ESPHOME_LOG_LEVEL_WARN, __LINE__); } break; - + + case climate::CLIMATE_PRESET_NONE: + // выбран пустой пресет, сбрасываем все настройки + hasCommand = true; + cmd.health = AC_HEALTH_OFF; + //cmd.health_status = AC_HEALTH_STATUS_OFF; // GK: не нужно ставить, т.к. этот флаг устанавливается самим сплитом + cmd.sleep = AC_SLEEP_OFF; + cmd.mildew = AC_MILDEW_OFF; + cmd.clean = AC_CLEAN_OFF; + this->preset = preset; + + _debugMsg(F("Clear all builtin presets."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); + break; default: // никакие другие встроенные пресеты не поддерживаются + _debugMsg(F("Preset %02X is unsupported."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, preset); break; } + } else if (call.get_custom_preset().has_value()) { - std::string custompreset = *call.get_custom_preset(); - if (custompreset == Constants::CLEAN) { + std::string custom_preset = *call.get_custom_preset(); + + if (custom_preset == Constants::CLEAN) { // режим очистки кондиционера, включается (или должен включаться) при AC_POWER_OFF // TODO: надо отдебажить выключение этого режима if ( cmd.power == AC_POWER_OFF @@ -2151,38 +2704,70 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { hasCommand = true; cmd.clean = AC_CLEAN_ON; - this->custom_preset = custompreset; + cmd.mildew = AC_MILDEW_OFF; // для логики пресетов + this->custom_preset = custom_preset; } else { _debugMsg(F("CLEAN preset is suitable in POWER_OFF mode only."), ESPHOME_LOG_LEVEL_WARN, __LINE__); } - } else if (custompreset == Constants::FEEL) { - _debugMsg(F("iFEEL preset has not been implemented yet."), ESPHOME_LOG_LEVEL_INFO, __LINE__); - // TODO: надо подумать, как заставить этот режим работать без пульта - //hasCommand = true; - //this->custom_preset = custompreset; - } else if (custompreset == Constants::HEALTH) { - _debugMsg(F("HEALTH preset has not been implemented yet."), ESPHOME_LOG_LEVEL_INFO, __LINE__); - // TODO: в моём кондиционере этот режим отсутствует, не понятно, как отлаживать - //hasCommand = true; - //this->custom_preset = custompreset; - } else if (custompreset == Constants::ANTIFUNGUS) { + + } else if ( custom_preset == Constants::HEALTH ) { + + if ( cmd.power == AC_POWER_ON || + _current_ac_state.power == AC_POWER_ON ) { + + hasCommand = true; + cmd.health = AC_HEALTH_ON; + //cmd.health_status = AC_HEALTH_STATUS_ON; // GK: статус кондей сам поднимает + 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 { + _debugMsg(F("HEALTH preset is suitable in POWER_ON mode only."), ESPHOME_LOG_LEVEL_WARN, __LINE__); + } + + } else if (custom_preset == Constants::ANTIFUNGUS) { // включение-выключение функции "Антиплесень". // По факту: после выключения сплита он оставляет минут на 5 открытые жалюзи и глушит вентилятор. // Уличный блок при этом гудит и тарахтит. Возможно, прогревается теплообменник для высыхания. // Через некоторое время внешний блок замолкает и сплит закрывает жалюзи. - _debugMsg(F("ANTIFUNGUS preset has not been implemented yet."), ESPHOME_LOG_LEVEL_INFO, __LINE__); + + // Brokly: + // включение-выключение функции "Антиплесень". + // у меня пульт отправляет 5 посылок и на включение и на выключение, но реагирует на эту кнопку + // только в режиме POWER_OFF + // TODO: надо уточнить, в каких режимах штатно включается этот режим у кондиционера - //cmd.mildew = AC_MILDEW_ON; - //hasCommand = true; - //this->custom_preset = custompreset; + cmd.mildew = AC_MILDEW_ON; + cmd.clean = AC_CLEAN_OFF; // для логики пресетов + + hasCommand = true; + this->custom_preset = custom_preset; } } // 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". @@ -2219,61 +2804,31 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { } + // User requested target temperature change if (call.get_target_temperature().has_value()) { - hasCommand = true; - // 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; - cmd.temp_target = temp; - cmd.temp_target_matter = true; + // выставлять температуру в режиме FAN не нужно + if ( cmd.mode != AC_MODE_FAN && _current_ac_state.mode != AC_MODE_FAN ) { + hasCommand = true; + cmd.temp_target = _temp_target_normalise(*call.get_target_temperature());// Send target temp to climate + cmd.temp_target_matter = true; + } } + if (hasCommand) { commandSequence(&cmd); - this->publish_state(); // Publish updated state + this->publish_all_states(); // Publish updated state + + #if defined(PRESETS_SAVING) + // флаг отправки новой команды, для процедуры сохранения пресетов, если есть настройка + _new_command_set = _store_settings; + #endif } } + // как оказалось сюда обращаются каждый раз для получения любого параметра + // по этому имеет смысл держать готовый объект 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; + return _traits; } // запрос маленького пакета статуса кондиционера @@ -2406,12 +2961,12 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { /*************************************** set params request ***********************************************/ if (!_addSequenceFuncStep(&AirCon::sq_requestDoCommand, cmd)) { - _debugMsg(F("commandSequence: getBigInfo request sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__); + _debugMsg(F("commandSequence: 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__); + _debugMsg(F("commandSequence: control sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__); return false; } /**************************************************************************************/ @@ -2461,34 +3016,44 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { } // отправляет сплиту заданный набор байт - // Перед отправкой проверяет пакет на корректность структуры. CRC16 рассчитывает самостоятельно и перезаписывает. + // Перед отправкой: + // устанавливает первый байт в 0xBB + // проверяет, чтобы длина тела пакета в заголовке не превышала длину буфера + // рассчитывает и записывает в конец пакета CRC bool sendTestPacket(const std::vector &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 (data.size() == 0) { + _debugMsg(F("sendTestPacket: no data to send."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); + 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) { + // всё, что не влезет в буфер - игнорируем + if (i >= AC_BUFFER_SIZE) { + _debugMsg(F("sendTestPacket: buffer size = %02d, data length = %02d. Extra data was omitted."), ESPHOME_LOG_LEVEL_ERROR, __LINE__, AC_BUFFER_SIZE, data.size()); + break; + } + // что влезает - копируем в буфер _outTestPacket.data[i] = n; i++; } - // на всякий случай указываем правильные некоторые байты + // на всякий случай указываем правильные некоторые байты: + // - установим стартовый байт _outTestPacket.header->start_byte = AC_PACKET_START_BYTE; - //_outTestPacket.header->wifi = AC_PACKET_ANSWER; + // - установим длину тела, если она больше возможной для нашего буфера + if (_outTestPacket.header->body_length > (AC_BUFFER_SIZE - AC_HEADER_SIZE - 2)) _outTestPacket.header->body_length = AC_BUFFER_SIZE - AC_HEADER_SIZE - 2; _outTestPacket.msec = millis(); _outTestPacket.body = &(_outTestPacket.data[AC_HEADER_SIZE]); @@ -2498,7 +3063,7 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { _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__); + _debugMsg(F("sendTestPacket: test packet loaded:"), ESPHOME_LOG_LEVEL_WARN, __LINE__); _debugPrintPacket(&_outTestPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__); // ниже блок добавления отправки пакета в последовательность команд @@ -2521,6 +3086,37 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { return true; } + // устанавливает жалюзи в нужное положение + bool setVLouverSequence(const ac_louver_V vLouver){ + // нет смысла в последовательности, если нет коннекта с кондиционером + if (!get_has_connection()) { + _debugMsg(F("setVLouverSequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); + return false; + } + if (vLouver == AC_LOUVERV_UNTOUCHED) return false; // выходим, чтобы не тратить время + + if ((vLouver > AC_LOUVERV_OFF) || (vLouver == 0x06)) return false; // нет таких команд + + // формируем команду + ac_command_t cmd; + _clearCommand(&cmd); // не забываем очищать, а то будет мусор + cmd.louver.louver_v = vLouver; + // добавляем команду в последовательность + if (!commandSequence(&cmd)) return false; + + _debugMsg(F("setVLouverSequence: loaded (power = %02X)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, vLouver); + return true; + } + + // установка жалюзи в определенные положения + bool setVLouverSwingSequence() { return setVLouverSequence(AC_LOUVERV_SWING_UPDOWN); } + bool setVLouverStopSequence() { return setVLouverSequence(AC_LOUVERV_OFF); } + bool setVLouverTopSequence() { return setVLouverSequence(AC_LOUVERV_SWING_TOP); } + bool setVLouverMiddleAboveSequence() { return setVLouverSequence(AC_LOUVERV_SWING_MIDDLE_ABOVE); } + bool setVLouverMiddleSequence() { return setVLouverSequence(AC_LOUVERV_SWING_MIDDLE); } + bool setVLouverMiddleBelowSequence() { return setVLouverSequence(AC_LOUVERV_SWING_MIDDLE_BELOW); } + bool setVLouverBottomSequence() { return setVLouverSequence(AC_LOUVERV_SWING_BOTTOM); } + void set_period(uint32_t ms) { this->_update_period = ms; } uint32_t get_period() { return this->_update_period; } @@ -2530,18 +3126,84 @@ class AirCon : public esphome::Component, public esphome::climate::Climate { void set_display_inverted(bool display_inverted) { this->_display_inverted = display_inverted; } bool get_display_inverted() { return this->_display_inverted; } - void set_supported_modes(const std::set &modes) { this->_supported_modes = modes; } - void set_supported_swing_modes(const std::set &modes) { this->_supported_swing_modes = modes; } - void set_supported_presets(const std::set &presets) { this->_supported_presets = presets; } - void set_custom_presets(const std::set &presets) { this->_supported_custom_presets = presets; } - void set_custom_fan_modes(const std::set &modes) { this->_supported_custom_fan_modes = modes; } + // возможно функции get и не нужны, но вроде как должны быть + void set_supported_modes(const std::set &modes) { this->_supported_modes = modes;} + std::setget_supported_modes(){return this->_supported_modes;} + + void set_supported_swing_modes(const std::set &modes) { this->_supported_swing_modes = modes;} + std::set get_supported_swing_modes(){return this->_supported_swing_modes;} + + void set_supported_presets(const std::set &presets) { this->_supported_presets = presets;} + const std::set& get_supported_presets(){return this->_supported_presets;} + + void set_custom_presets(const std::set &presets) { this->_supported_custom_presets = presets;} + const std::set& get_supported_custom_presets(){return this->_supported_custom_presets;} + + void set_custom_fan_modes(const std::set &modes) { this->_supported_custom_fan_modes = modes;} + const std::set& get_supported_custom_fan_modes(){return this->_supported_custom_fan_modes;} + + #if defined(PRESETS_SAVING) + void set_store_settings(bool store_settings) { this->_store_settings = store_settings; } + bool get_store_settings() { return this->_store_settings; } + + uint8_t load_presets_result = 0xFF; + #endif void setup() override { + + #if defined(PRESETS_SAVING) + 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 + + // заполнение шаблона параметров отображения виджета + // GK: всё же похоже правильнее это делать тут, а не в initAC() + // initAC() в формируемом питоном коде вызывается до вызова aux_ac.set_supported_***() с установленными пользователем в конфиге параметрами + _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 + + _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); + + // tells the frontend what range of temperatures the climate device should display (gauge min/max values) + // TODO: GK: а вот здесь похоже неправильно. Похоже, так мы не сможем выставить в конфиге свой диапазон температур - всегда будет от AC_MIN_TEMPERATURE до AC_MAX_TEMPERATURE + _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); + + /* + 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); + + // if the climate device supports reporting the active current action of the device with the action property. + _traits.set_supports_action(this->_show_action); + }; void loop() override { if (!get_hw_initialized()) return; + #if defined(PRESETS_SAVING) + // контролируем сохранение пресета + if(_new_command_set){ //нужно сохранить пресет + _new_command_set = false; + save_preset((ac_command_t *)&_current_ac_state); // переносим текущие данные в массив пресетов + } + #endif + /// отрабатываем состояния конечного автомата switch (_ac_state) { case ACSM_RECEIVING_PACKET: diff --git a/components/aux_ac/climate.py b/components/aux_ac/climate.py index c6ba1d1..651c720 100644 --- a/components/aux_ac/climate.py +++ b/components/aux_ac/climate.py @@ -1,7 +1,7 @@ import logging import esphome.config_validation as cv import esphome.codegen as cg -from esphome.components import climate, uart, sensor, binary_sensor +from esphome.components import climate, uart, sensor, binary_sensor, text_sensor from esphome import automation from esphome.automation import maybe_simple_id from esphome.const import ( @@ -12,9 +12,15 @@ from esphome.const import ( CONF_CUSTOM_PRESETS, CONF_INTERNAL, CONF_DATA, + CONF_SUPPORTED_MODES, + CONF_SUPPORTED_SWING_MODES, + CONF_SUPPORTED_PRESETS, UNIT_CELSIUS, + UNIT_PERCENT, + ICON_POWER, ICON_THERMOMETER, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_POWER_FACTOR, STATE_CLASS_MEASUREMENT, ) from esphome.components.climate import ( @@ -27,17 +33,27 @@ _LOGGER = logging.getLogger(__name__) CODEOWNERS = ["@GrKoR"] DEPENDENCIES = ["climate", "uart"] -AUTO_LOAD = ["sensor", "binary_sensor"] +AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor"] -CONF_SUPPORTED_MODES = 'supported_modes' -CONF_SUPPORTED_SWING_MODES = 'supported_swing_modes' -CONF_SUPPORTED_PRESETS = 'supported_presets' CONF_SHOW_ACTION = 'show_action' CONF_INDOOR_TEMPERATURE = 'indoor_temperature' +CONF_OUTDOOR_TEMPERATURE = 'outdoor_temperature' +ICON_OUTDOOR_TEMPERATURE = 'mdi:home-thermometer-outline' +CONF_INBOUND_TEMPERATURE = 'inbound_temperature' +ICON_INBOUND_TEMPERATURE = 'mdi:thermometer-plus' +CONF_OUTBOUND_TEMPERATURE = 'outbound_temperature' +ICON_OUTBOUND_TEMPERATURE = 'mdi:thermometer-minus' +CONF_COMPRESSOR_TEMPERATURE = 'compressor_temperature' +ICON_COMPRESSOR_TEMPERATURE = 'mdi:thermometer-lines' CONF_DISPLAY_STATE = 'display_state' - +CONF_INVERTOR_POWER = 'invertor_power' +CONF_DEFROST_STATE = 'defrost_state' +ICON_DEFROST = "mdi:snowflake-melt" CONF_DISPLAY_INVERTED = 'display_inverted' -ICON_DISPLAY = "mdi:numeric" +ICON_DISPLAY = "mdi:clock-digital" +CONF_PRESET_REPORTER = "preset_reporter" +ICON_PRESET_REPORTER = "mdi:format-list-group" + aux_ac_ns = cg.esphome_ns.namespace("aux_ac") AirCon = aux_ac_ns.class_("AirCon", climate.Climate, cg.Component) @@ -45,6 +61,13 @@ Capabilities = aux_ac_ns.namespace("Constants") AirConDisplayOffAction = aux_ac_ns.class_("AirConDisplayOffAction", automation.Action) AirConDisplayOnAction = aux_ac_ns.class_("AirConDisplayOnAction", automation.Action) +AirConVLouverSwingAction = aux_ac_ns.class_("AirConVLouverSwingAction", automation.Action) +AirConVLouverStopAction = aux_ac_ns.class_("AirConVLouverStopAction", automation.Action) +AirConVLouverTopAction = aux_ac_ns.class_("AirConVLouverTopAction", automation.Action) +AirConVLouverMiddleAboveAction = aux_ac_ns.class_("AirConVLouverMiddleAboveAction", automation.Action) +AirConVLouverMiddleAction = aux_ac_ns.class_("AirConVLouverMiddleAction", automation.Action) +AirConVLouverMiddleBelowAction = aux_ac_ns.class_("AirConVLouverMiddleBelowAction", automation.Action) +AirConVLouverBottomAction = aux_ac_ns.class_("AirConVLouverBottomAction", automation.Action) AirConSendTestPacketAction = aux_ac_ns.class_("AirConSendTestPacketAction", automation.Action) ALLOWED_CLIMATE_MODES = { @@ -76,7 +99,6 @@ validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True) CUSTOM_PRESETS = { "CLEAN": Capabilities.CLEAN, - "FEEL": Capabilities.FEEL, "HEALTH": Capabilities.HEALTH, "ANTIFUNGUS": Capabilities.ANTIFUNGUS, } @@ -102,6 +124,17 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_PERIOD, default="7s"): cv.time_period, cv.Optional(CONF_SHOW_ACTION, default="true"): cv.boolean, cv.Optional(CONF_DISPLAY_INVERTED, default="false"): cv.boolean, + cv.Optional(CONF_INVERTOR_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_POWER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, + } + ), cv.Optional(CONF_INDOOR_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, icon=ICON_THERMOMETER, @@ -113,11 +146,69 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, } ), - cv.Optional(CONF_DISPLAY_STATE): binary_sensor.binary_sensor_schema( - icon=ICON_DISPLAY + cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_OUTDOOR_TEMPERATURE, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ).extend( { - cv.Optional(CONF_INTERNAL, default="true"): cv.boolean + cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, + } + ), + cv.Optional(CONF_INBOUND_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_INBOUND_TEMPERATURE, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, + } + ), + cv.Optional(CONF_OUTBOUND_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_OUTBOUND_TEMPERATURE, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, + } + ), + cv.Optional(CONF_COMPRESSOR_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_COMPRESSOR_TEMPERATURE, + accuracy_decimals=0, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ).extend( + { + cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, + } + ), + cv.Optional(CONF_DISPLAY_STATE): binary_sensor.binary_sensor_schema( + icon=ICON_DISPLAY, + ).extend( + { + cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, + } + ), + cv.Optional(CONF_DEFROST_STATE): binary_sensor.binary_sensor_schema( + icon=ICON_DEFROST, + ).extend( + { + cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, + } + ), + cv.Optional(CONF_PRESET_REPORTER): text_sensor.text_sensor_schema( + icon=ICON_PRESET_REPORTER, + ).extend( + { + cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, } ), cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes), @@ -146,12 +237,47 @@ async def to_code(config): conf = config[CONF_INDOOR_TEMPERATURE] sens = await sensor.new_sensor(conf) cg.add(var.set_indoor_temperature_sensor(sens)) + + if CONF_OUTDOOR_TEMPERATURE in config: + conf = config[CONF_OUTDOOR_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_outdoor_temperature_sensor(sens)) + + if CONF_OUTBOUND_TEMPERATURE in config: + conf = config[CONF_OUTBOUND_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_outbound_temperature_sensor(sens)) + + if CONF_INBOUND_TEMPERATURE in config: + conf = config[CONF_INBOUND_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_inbound_temperature_sensor(sens)) + + if CONF_COMPRESSOR_TEMPERATURE in config: + conf = config[CONF_COMPRESSOR_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_compressor_temperature_sensor(sens)) if CONF_DISPLAY_STATE in config: conf = config[CONF_DISPLAY_STATE] sens = await binary_sensor.new_binary_sensor(conf) cg.add(var.set_display_sensor(sens)) + if CONF_DEFROST_STATE in config: + conf = config[CONF_DEFROST_STATE] + sens = await binary_sensor.new_binary_sensor(conf) + cg.add(var.set_defrost_state(sens)) + + if CONF_INVERTOR_POWER in config: + conf = config[CONF_INVERTOR_POWER] + sens = await sensor.new_sensor(conf) + cg.add(var.set_invertor_power_sensor(sens)) + + if CONF_PRESET_REPORTER in config: + conf = config[CONF_PRESET_REPORTER] + sens = await text_sensor.new_text_sensor(conf) + cg.add(var.set_preset_reporter_sensor(sens)) + cg.add(var.set_period(config[CONF_PERIOD].total_milliseconds)) cg.add(var.set_show_action(config[CONF_SHOW_ACTION])) cg.add(var.set_display_inverted(config[CONF_DISPLAY_INVERTED])) @@ -185,13 +311,48 @@ async def display_on_to_code(config, action_id, template_arg, args): return cg.new_Pvariable(action_id, template_arg, paren) -SEND_TEST_PACKET_ACTION_SCHEMA = maybe_simple_id( +VLOUVER_ACTION_SCHEMA = maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(AirCon), - cv.Required(CONF_DATA): cv.templatable(validate_raw_data), } ) +@automation.register_action("aux_ac.vlouver_stop", AirConVLouverStopAction, VLOUVER_ACTION_SCHEMA) +async def vlouver_stop_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + +@automation.register_action("aux_ac.vlouver_swing", AirConVLouverSwingAction, VLOUVER_ACTION_SCHEMA) +async def vlouver_swing_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + +@automation.register_action("aux_ac.vlouver_top", AirConVLouverTopAction, VLOUVER_ACTION_SCHEMA) +async def vlouver_top_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + +@automation.register_action("aux_ac.vlouver_middle_above", AirConVLouverMiddleAboveAction, VLOUVER_ACTION_SCHEMA) +async def vlouver_middle_above_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + +@automation.register_action("aux_ac.vlouver_middle", AirConVLouverMiddleAction, VLOUVER_ACTION_SCHEMA) +async def vlouver_middle_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + +@automation.register_action("aux_ac.vlouver_middle_below", AirConVLouverMiddleBelowAction, VLOUVER_ACTION_SCHEMA) +async def vlouver_middle_below_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + +@automation.register_action("aux_ac.vlouver_bottom", AirConVLouverBottomAction, VLOUVER_ACTION_SCHEMA) +async def vlouver_bottom_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + # ********************************************************************************************************* # ВАЖНО! Только для инженеров! @@ -199,6 +360,13 @@ SEND_TEST_PACKET_ACTION_SCHEMA = maybe_simple_id( # кондиционеру всё как есть. Какой эффект получится от передачи кондиционеру рандомных байт, никто не знает. # Вы действуете на свой страх и риск. # ********************************************************************************************************* +SEND_TEST_PACKET_ACTION_SCHEMA = maybe_simple_id( + { + cv.Required(CONF_ID): cv.use_id(AirCon), + cv.Required(CONF_DATA): cv.templatable(validate_raw_data), + } +) + @automation.register_action( "aux_ac.send_packet", AirConSendTestPacketAction, @@ -218,4 +386,4 @@ async def send_packet_to_code(config, action_id, template_arg, args): else: cg.add(var.set_data_static(data)) - return var \ No newline at end of file + return var \ No newline at end of file diff --git a/examples/advanced/ac_common.yaml b/examples/advanced/ac_common.yaml index e012151..a6e9f57 100644 --- a/examples/advanced/ac_common.yaml +++ b/examples/advanced/ac_common.yaml @@ -69,9 +69,37 @@ climate: id: ${devicename}_indoor_temp internal: false display_state: - name: $upper_devicename Display State + name: ${upper_devicename} Display State id: ${devicename}_display_state internal: false + outdoor_temperature: + name: ${upper_devicename} Outdoor Temperature + id: ${devicename}_outdoor_temp + internal: false + outbound_temperature: + name: ${upper_devicename} Colant Outbound Temperature + id: ${devicename}_outbound_temp + internal: false + inbound_temperature: + name: ${upper_devicename} Colant Inbound Temperature + id: ${devicename}_inbound_temp + internal: false + compressor_temperature: + name: ${upper_devicename} Compressor Temperature + id: ${devicename}_strange_temp + internal: false + defrost_state: + name: ${upper_devicename} Defrost State + id: ${devicename}_defrost_state + internal: false + invertor_power: + name: ${upper_devicename} Invertor Power + id: ${devicename}_invertor_power + internal: false + preset_reporter: + name: ${upper_devicename} Preset Reporter + id: ${devicename}_preset_reporter + internal: false visual: min_temperature: 16 max_temperature: 32 @@ -89,7 +117,6 @@ climate: - SLEEP custom_presets: - CLEAN - - FEEL - HEALTH - ANTIFUNGUS supported_swing_modes: @@ -109,7 +136,7 @@ sensor: switch: - platform: template - name: $upper_devicename Display + name: ${upper_devicename} Display lambda: |- if (id(${devicename}_display_state).state) { return true; @@ -120,3 +147,62 @@ switch: - aux_ac.display_on: aux_id turn_off_action: - aux_ac.display_off: aux_id + +button: + - platform: template + name: ${upper_devicename} VLouver Stop + icon: "mdi:circle-small" + on_press: + - aux_ac.vlouver_stop: aux_id + + - platform: template + name: ${upper_devicename} VLouver Swing + icon: "mdi:pan-vertical" + on_press: + - aux_ac.vlouver_swing: aux_id + + - platform: template + name: ${upper_devicename} VLouver Top + icon: "mdi:pan-up" + on_press: + - aux_ac.vlouver_top: aux_id + + - platform: template + name: ${upper_devicename} VLouver Middle Above + icon: "mdi:pan-top-left" + on_press: + - aux_ac.vlouver_middle_above: aux_id + + - platform: template + name: ${upper_devicename} VLouver Middle + icon: "mdi:pan-left" + on_press: + - aux_ac.vlouver_middle: aux_id + + - platform: template + name: ${upper_devicename} VLouver Middle Below + icon: "mdi:pan-bottom-left" + on_press: + - aux_ac.vlouver_middle_below: aux_id + + - platform: template + name: ${upper_devicename} VLouver Bottom + icon: "mdi:pan-down" + on_press: + - aux_ac.vlouver_bottom: aux_id + + +number: + - platform: template + name: ${upper_devicename} Vertical Louver + id: ${devicename}_vlouver + icon: "mdi:circle-small" + mode: "slider" + min_value: 0 + max_value: 6 + step: 1 + set_action: + then: + - lambda: !lambda |- + if (x == 6) x = 7; // 6 is incorrect louver position, 7 is stopped louver + id(aux_id).setVLouverSequence( static_cast(x) ); \ No newline at end of file diff --git a/tests/ac_send_packet_for_engineer.py b/tests/ac_send_packet_for_engineer.py index ccc1571..ce26215 100644 --- a/tests/ac_send_packet_for_engineer.py +++ b/tests/ac_send_packet_for_engineer.py @@ -21,8 +21,6 @@ def createParser (): parent_group.add_argument ('--help', '-h', action='help', help='show this help message and exit') parent_group.add_argument ('-i', '--ip', nargs=1, required=True, help='IP address of the esphome device') parent_group.add_argument ('-p', '--pwd', nargs=1, required=True, help='native API password for the esphome device') - parent_group.add_argument ('-n', '--name', nargs=1, default=['noname'], help='name of this devace in the log') - parent_group.add_argument ('-l', '--logfile', nargs=1, default=['%4d-%02d-%02d %02d-%02d-%02d log.csv' % time.localtime()[0:6]], help='log file name') return parser async def main(): @@ -36,57 +34,77 @@ async def main(): print(api.api_version) - def log_AC(isAirConLog): - parts = re.search("(\d{10}): (\[\S{2}\]) \[([0-9A-F ]{23})\]\s?((?:[0-9A-F]{2}\s*)*) \[([0-9A-F ]{5})\]", isAirConLog.group(1)) - packString = '\n' + namespace.name[0] - packString += ";" + "%4d-%02d-%02d %02d:%02d:%02d" % time.localtime()[0:6] - """millis of message""" - packString += ";" + parts.group(1) - """direction""" - packString += ";" + parts.group(2) - """header""" - packString += ";" + ';'.join(parts.group(3).split(" ")) - """body (may be void)""" - if len(parts.group(4)) > 0: - packString += ";" + ';'.join(parts.group(4).split(" ")) - """crc""" - packString += ";" + ';'.join(parts.group(5).split(" ")) - print(packString) - with open(namespace.logfile[0], 'a+') as file: - file.write( packString ) + async def display_off(): + await api.execute_service( + service, + data={ + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x01, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x97, 0xE0, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0x00], + } + ) - def log_Dallas(isDallasLog): - parts = re.search("'([\w ]+)': Got Temperature=([-]?\d+\.\d+)°C", isDallasLog.group(1)) - packString = '\n' + parts.group(1) - packString += ";" + "%4d-%02d-%02d %02d:%02d:%02d" % time.localtime()[0:6] - """millis of message always empty""" - packString += ";" - """direction""" - packString += ";[<=]" - """additional data flag""" - packString += ";AA" - """dallas temperature""" - packString += ";" + parts.group(2) - print(packString) - with open(namespace.logfile[0], 'a+') as file: - file.write( packString ) + async def display_on(): + await api.execute_service( + service, + data={ + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x01, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x97, 0xE0, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00], + } + ) - def log_callback(log): - """Print the log for AirCon""" - isAirConLog = re.search("\[AirCon:\d+\]: (.+\])", log.message.decode('utf-8')) - if isAirConLog: - log_AC(isAirConLog) - if namespace.logdallas: - isDallasLog = re.search("\[dallas.sensor:\d+\]: (.+C)", log.message.decode('utf-8')) - if isDallasLog: - log_Dallas(isDallasLog) + async def ac_enable(): + await api.execute_service( + service, + data={ + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x00, 0x87, 0xE0, 0x2F, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00], + } + ) + async def ac_disable(): + await api.execute_service( + service, + data={ + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x00, 0x87, 0xE0, 0x2F, 0xA0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], + } + ) - # Subscribe to the log - # await api.subscribe_logs(log_callback, LOG_LEVEL_DEBUG) + async def ac_get11_01(): + await api.execute_service( + service, + data={ + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x02, 0x00, 0x11, 0x01], + } + ) - # print(await api.device_info()) - print(f"%s" % (await api.list_entities_services(),)) + async def ac_get11_00(): + await api.execute_service( + service, + data={ + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x02, 0x00, 0x11, 0x00], + } + ) + + async def ac_set_vlouver(lvr): + await api.execute_service( + service, + data={ + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x01, lvr, 0xE0, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00], + } + ) + + async def ac_set_hlouver(lvr): + await api.execute_service( + service, + data={ + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x97, lvr, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00], + } + ) # key надо искать в выводе list_entities_services service = aioesphomeapi.UserService( @@ -97,27 +115,66 @@ async def main(): ], ) - await api.execute_service( - service, - data={ - # display off - "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x7F, 0xE0, 0x00, 0x20, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00], - } - ) + time.sleep(7) + await ac_get11_00() + time.sleep(7) + await ac_get11_01() - time.sleep(3) + #await ac_set_vlouver( 0b10010000 ) # swing on + #await ac_set_vlouver( 0b10010111 ) # swing off + #await ac_set_vlouver( 0b10010001 ) # 1 + #await ac_set_vlouver( 0b10010010 ) # 2 + #await ac_set_vlouver( 0b10010011 ) # 3 + #await ac_set_vlouver( 0b10010100 ) # 4 + #await ac_set_vlouver( 0b10010101 ) # 5 + #await ac_set_vlouver( 0b10010110 ) # не работает, сбрасывает на swing on + #time.sleep(5) + + #await ac_set_hlouver( 0b00000000 ) # swing on + #await ac_set_hlouver( 0b11100000 ) # swing off + #await ac_set_hlouver( 0b00100000 ) # не работает, сбрасывает в swing off + #await ac_set_hlouver( 0b01000000 ) # не работает, сбрасывает в swing off + #await ac_set_hlouver( 0b01100000 ) # не работает, сбрасывает в swing off + #await ac_set_hlouver( 0b10000000 ) # не работает, сбрасывает в swing off + #await ac_set_hlouver( 0b10100000 ) # не работает, сбрасывает в swing off + #await ac_set_hlouver( 0b11000000 ) # не работает, сбрасывает в swing off + #time.sleep(5) + + async def test_byte(bt): + await api.execute_service( + service, + data={ + #display on + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + #"data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x97, 0xE0, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00], + #display off + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x97, 0xE0, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x20, 0x00, 0x10, 0x00, 0x00], + # swing on + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + #"data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x90, 0xE0, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00], + # swing off + # 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + #"data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x97, 0xE0, 0x00, 0x20, 0x00, 0xC0, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00], + } + ) + ''' + не проходит команда, если байт 1 или 7 не 0x00 + не проходит команда, если байт 3 не 0x80 + + проходит и не меняется, если меняю байт 4 или 5 + ''' + + #await test_byte(0b10000110) + #await test_byte(0b01000110) + #await test_byte(0b00100110) + #await test_byte(0b00010110) + time.sleep(2) - await api.execute_service( - service, - data={ - # display on - "data_buf": [0xBB, 0x00, 0x06, 0x80, 0x00, 0x00, 0x0F, 0x00, 0x01, 0x01, 0x7F, 0xE0, 0x00, 0x20, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], - } - ) parser = createParser() namespace = parser.parse_args() -print(namespace.name[0], namespace.ip[0]) +print("IP: ", namespace.ip[0]) loop = asyncio.get_event_loop() diff --git a/tests/test-ext-esp32.yaml b/tests/test-ext-esp32.yaml index 5b443fa..f8ba75a 100644 --- a/tests/test-ext-esp32.yaml +++ b/tests/test-ext-esp32.yaml @@ -3,9 +3,12 @@ external_components: components: [ aux_ac ] refresh: 0s - +substitutions: + devicename: test_aux_ac_ext_esp32 + upper_devicename: Test AUX + esphome: - name: test_aux_ac_ext_esp32 + name: $devicename platform: ESP32 board: nodemcu-32s @@ -51,21 +54,54 @@ sensor: climate: - platform: aux_ac - name: "AC Name" + name: $upper_devicename id: aux_id uart_id: ac_uart_bus - period: 7s # период опроса состояния сплита, по дефолту 7 сек - show_action: true # надо ли показывать текущий режим работы: при HEAT_COOL mode сплит может греть (HEAT), охлаждать (COOL) или бездействовать (IDLE) - indoor_temperature: # сенсор, показывающий температуру воздуха на внутреннем блоке кондиционера; имеет все те же параметры, как и любой сенсор ESPHome - name: AC Indoor Temperature - id: ac_indoor_temp - internal: true # сенсор установлен как внутренний по дефолту (не попадёт в Home Assistant) + period: 7s + show_action: true + display_inverted: true + indoor_temperature: + name: $upper_devicename Indoor Temperature + id: ${devicename}_indoor_temp + internal: false + display_state: + name: $upper_devicename Display State + id: ${devicename}_display_state + internal: false + outdoor_temperature: + name: $upper_devicename Outdoor Temperature + id: ${devicename}_outdoor_temp + internal: false + outbound_temperature: + name: $upper_devicename Colant Outbound Temperature + id: ${devicename}_outbound_temp + internal: false + inbound_temperature: + name: $upper_devicename Colant Inbound Temperature + id: ${devicename}_inbound_temp + internal: false + compressor_temperature: + name: $upper_devicename Compressor Temperature + id: ${devicename}_strange_temp + internal: false + defrost_state: + name: $upper_devicename Defrost State + id: ${devicename}_defrost_state + internal: false + invertor_power: + name: $upper_devicename Invertor Power + id: ${devicename}_invertor_power + internal: false + preset_reporter: + name: $upper_devicename Preset Reporter + id: ${devicename}_preset_reporter + internal: false visual: min_temperature: 16 max_temperature: 32 temperature_step: 0.5 supported_modes: - - HEAT_COOL # не AUTO, так как только нагревает и остужает. В доках на ESPHome говорится, что AUTO - это если у устройства есть календарь и какие-то установки по расписанию. + - HEAT_COOL - COOL - HEAT - DRY @@ -77,10 +113,9 @@ climate: - SLEEP custom_presets: - CLEAN - - FEEL - HEALTH - ANTIFUNGUS supported_swing_modes: - VERTICAL - HORIZONTAL - - BOTH \ No newline at end of file + - BOTH diff --git a/tests/test-ext-for-engineer.yaml b/tests/test-ext-for-engineer.yaml index 4561a52..e747569 100644 --- a/tests/test-ext-for-engineer.yaml +++ b/tests/test-ext-for-engineer.yaml @@ -3,8 +3,12 @@ external_components: components: [ aux_ac ] refresh: 0s +substitutions: + devicename: test_aux_ac_ext_engeneer + upper_devicename: Test AUX + esphome: - name: test_aux_ac_ext + name: $devicename platform: ESP8266 board: esp12e @@ -69,26 +73,54 @@ sensor: climate: - platform: aux_ac - name: "AC Name" + name: $upper_devicename id: aux_id uart_id: ac_uart_bus - period: 7s # период опроса состояния сплита, по дефолту 7 сек - show_action: true # надо ли показывать текущий режим работы: при HEAT_COOL mode сплит может греть (HEAT), охлаждать (COOL) или бездействовать (IDLE) - display_inverted: true # как отрабатывать вкл/выкл дисплея: у Rovex "1" выключает дисплей, у многих других "1" дисплей включает - indoor_temperature: # сенсор, показывающий температуру воздуха на внутреннем блоке кондиционера; имеет все те же параметры, как и любой сенсор ESPHome - name: AC Indoor Temperature - id: ac_indoor_temp - internal: false # сенсор установлен как внутренний по дефолту (не попадёт в Home Assistant) + period: 7s + show_action: true + display_inverted: true + indoor_temperature: + name: $upper_devicename Indoor Temperature + id: ${devicename}_indoor_temp + internal: false display_state: - name: AC Display State - id: ac_display_state - internal: false # сенсор установлен как внутренний по дефолту (не попадёт в Home Assistant) + name: $upper_devicename Display State + id: ${devicename}_display_state + internal: false + outdoor_temperature: + name: $upper_devicename Outdoor Temperature + id: ${devicename}_outdoor_temp + internal: false + outbound_temperature: + name: $upper_devicename Colant Outbound Temperature + id: ${devicename}_outbound_temp + internal: false + inbound_temperature: + name: $upper_devicename Colant Inbound Temperature + id: ${devicename}_inbound_temp + internal: false + compressor_temperature: + name: $upper_devicename Compressor Temperature + id: ${devicename}_strange_temp + internal: false + defrost_state: + name: $upper_devicename Defrost State + id: ${devicename}_defrost_state + internal: false + invertor_power: + name: $upper_devicename Invertor Power + id: ${devicename}_invertor_power + internal: false + preset_reporter: + name: $upper_devicename Preset Reporter + id: ${devicename}_preset_reporter + internal: false visual: min_temperature: 16 max_temperature: 32 temperature_step: 0.5 supported_modes: - - HEAT_COOL # не AUTO, так как только нагревает и остужает. В доках на ESPHome говорится, что AUTO - это если у устройства есть календарь и какие-то установки по расписанию. + - HEAT_COOL - COOL - HEAT - DRY @@ -100,7 +132,6 @@ climate: - SLEEP custom_presets: - CLEAN - - FEEL - HEALTH - ANTIFUNGUS supported_swing_modes: @@ -113,7 +144,7 @@ switch: - platform: template name: AC Display lambda: |- - if (id(ac_display_state).state) { + if (id(${devicename}_display_state).state) { return true; } else { return false; diff --git a/tests/test-local-airflow-dir.yaml b/tests/test-local-airflow-dir.yaml new file mode 100644 index 0000000..3025e87 --- /dev/null +++ b/tests/test-local-airflow-dir.yaml @@ -0,0 +1,125 @@ +external_components: + - source: + type: local + path: ..\components + +substitutions: + devicename: test_local_airflow_dir + upper_devicename: Test AUX + +esphome: + name: $devicename + platform: ESP8266 + board: esp12e + +wifi: + ssid: !secret wifi_ssid + password: !secret wifi_pass + manual_ip: + static_ip: !secret wifi_ip + gateway: !secret wifi_gateway + subnet: !secret wifi_subnet + dns1: 8.8.8.8 + dns2: 1.1.1.1 + reboot_timeout: 0s + ap: + ssid: $upper_devicename Fallback Hotspot + password: !secret wifi_ap_pass + +logger: + level: DEBUG + baud_rate: 0 + +api: + password: !secret api_pass + reboot_timeout: 0s + +ota: + password: !secret ota_pass + +web_server: + port: 80 + +uart: + id: ac_uart_bus + tx_pin: GPIO1 + rx_pin: GPIO3 + baud_rate: 4800 + data_bits: 8 + parity: EVEN + stop_bits: 1 + + +sensor: + - platform: uptime + name: Uptime Sensor + + +climate: + - platform: aux_ac + name: $upper_devicename + id: aux_id + uart_id: ac_uart_bus + period: 7s + show_action: true + display_inverted: true + + +button: + - platform: template + name: ${upper_devicename} VLouver Stop + icon: "mdi:circle-small" + on_press: + - aux_ac.vlouver_stop: aux_id + + - platform: template + name: ${upper_devicename} VLouver Swing + icon: "mdi:pan-vertical" + on_press: + - aux_ac.vlouver_swing: aux_id + + - platform: template + name: ${upper_devicename} VLouver Top + icon: "mdi:pan-up" + on_press: + - aux_ac.vlouver_top: aux_id + + - platform: template + name: ${upper_devicename} VLouver Middle Above + icon: "mdi:pan-top-left" + on_press: + - aux_ac.vlouver_middle_above: aux_id + + - platform: template + name: ${upper_devicename} VLouver Middle + icon: "mdi:pan-left" + on_press: + - aux_ac.vlouver_middle: aux_id + + - platform: template + name: ${upper_devicename} VLouver Middle Below + icon: "mdi:pan-bottom-left" + on_press: + - aux_ac.vlouver_middle_below: aux_id + + - platform: template + name: ${upper_devicename} VLouver Bottom + icon: "mdi:pan-down" + on_press: + - aux_ac.vlouver_bottom: aux_id + + +number: + - platform: template + name: ${upper_devicename} Vertical Louver + id: ${devicename}_vlouver + icon: "mdi:circle-small" + mode: "slider" + min_value: 0 + max_value: 6 + step: 1 + set_action: + then: + - lambda: !lambda |- + if (x == 6) x = 7; // делаем так, чтобы выключение отрабатывать корректно + id(aux_id).setVLouverSequence( static_cast(x) ); diff --git a/tests/test-local.yaml b/tests/test-local.yaml index 04a71e4..216befb 100644 --- a/tests/test-local.yaml +++ b/tests/test-local.yaml @@ -3,8 +3,12 @@ external_components: type: local path: ..\components +substitutions: + devicename: test_local_airflow_dir + upper_devicename: Test AUX + esphome: - name: test_aux_ac_local + name: $devicename platform: ESP8266 board: esp12e @@ -51,21 +55,54 @@ sensor: climate: - platform: aux_ac - name: "AC Name" + name: $upper_devicename id: aux_id uart_id: ac_uart_bus - period: 7s # период опроса состояния сплита, по дефолту 7 сек - show_action: true # надо ли показывать текущий режим работы: при HEAT_COOL mode сплит может греть (HEAT), охлаждать (COOL) или бездействовать (IDLE) - indoor_temperature: # сенсор, показывающий температуру воздуха на внутреннем блоке кондиционера; имеет все те же параметры, как и любой сенсор ESPHome - name: AC Indoor Temperature - id: ac_indoor_temp - internal: true # сенсор установлен как внутренний по дефолту (не попадёт в Home Assistant) + period: 7s + show_action: true + display_inverted: true + indoor_temperature: + name: $upper_devicename Indoor Temperature + id: ${devicename}_indoor_temp + internal: false + display_state: + name: $upper_devicename Display State + id: ${devicename}_display_state + internal: false + outdoor_temperature: + name: $upper_devicename Outdoor Temperature + id: ${devicename}_outdoor_temp + internal: false + outbound_temperature: + name: $upper_devicename Colant Outbound Temperature + id: ${devicename}_outbound_temp + internal: false + inbound_temperature: + name: $upper_devicename Colant Inbound Temperature + id: ${devicename}_inbound_temp + internal: false + compressor_temperature: + name: $upper_devicename Compressor Temperature + id: ${devicename}_strange_temp + internal: false + defrost_state: + name: $upper_devicename Defrost State + id: ${devicename}_defrost_state + internal: false + invertor_power: + name: $upper_devicename Invertor Power + id: ${devicename}_invertor_power + internal: false + preset_reporter: + name: $upper_devicename Preset Reporter + id: ${devicename}_preset_reporter + internal: false visual: min_temperature: 16 max_temperature: 32 temperature_step: 0.5 supported_modes: - - HEAT_COOL # не AUTO, так как только нагревает и остужает. В доках на ESPHome говорится, что AUTO - это если у устройства есть календарь и какие-то установки по расписанию. + - HEAT_COOL - COOL - HEAT - DRY @@ -77,10 +114,9 @@ climate: - SLEEP custom_presets: - CLEAN - - FEEL - HEALTH - ANTIFUNGUS supported_swing_modes: - VERTICAL - HORIZONTAL - - BOTH \ No newline at end of file + - BOTH diff --git a/tests/test-minimal.yaml b/tests/test-minimal.yaml index 975e2ed..651ccca 100644 --- a/tests/test-minimal.yaml +++ b/tests/test-minimal.yaml @@ -6,8 +6,12 @@ external_components: #components: [ aux_ac ] #refresh: 0s +substitutions: + devicename: test_local_minimal + upper_devicename: Test AUX + esphome: - name: test_aux_ac_minimal + name: $devicename platform: ESP8266 board: esp12e @@ -41,4 +45,4 @@ uart: climate: - platform: aux_ac - name: "AC Name" \ No newline at end of file + name: $upper_devicename \ No newline at end of file