diff --git a/README-EN.md b/README-EN.md index 51fa872..ae2d68a 100644 --- a/README-EN.md +++ b/README-EN.md @@ -87,30 +87,37 @@ climate: id: aux_id uart_id: ac_uart_bus period: 7s - show_action: true display_inverted: false - timeout: 150 + timeout: 300 optimistic: true - indoor_temperature: - name: AC Indoor Temperature - id: ac_indoor_temp + indoor_ambient_temperature: + name: AC Indoor Ambient Temperature + id: ac_indoor_ambient_temp accuracy_decimals: 1 internal: false - outdoor_temperature: - name: AC Outdoor Temperature - id: ac_outdoor_temp + outdoor_ambient_temperature: + name: AC Outdoor Ambient Temperature + id: ac_outdoor_ambient_temp internal: false - outbound_temperature: - name: AC Coolant Outbound Temperature - id: ac_outbound_temp + outdoor_condenser_temperature: + name: AC Outdoor Condenser Temperature + id: ac_outdoor_condenser_temp internal: false - inbound_temperature: - name: AC Coolant Inbound Temperature - id: ac_inbound_temp + compressor_suction_temperature: + name: AC Compressor Suction Temperature + id: ac_compressor_suction_temp internal: false - compressor_temperature: - name: AC Compressor Temperature - id: ac_strange_temp + indoor_coil_temperature: + name: AC Indoor Coil Temperature + id: ac_indoor_coil_temp + internal: false + compressor_discharge_temperature: + name: AC Compressor Discharge Temperature + id: ac_compressor_discharge_temp + internal: false + defrost_temperature: + name: AC Defrost Temperature + id: ac_defrost_temp internal: false display_state: name: AC Display State @@ -175,35 +182,38 @@ climate: - **period** (*Optional*, [time](https://esphome.io/guides/configuration-types.html#config-time), default ``7s``): Period between status requests to the AC. `Aux_ac` will receive the new air conditioner status only after a regular request, even if you change the settings of AC using IR-remote. -- **show_action** (*Optional*, boolean, default ``true``): Whether to show current action of the device (experimental). For example, in the HEAT_COOL mode, AC hardware may be in one of the following actions: - - HEATING: AC is heating the air in the room; - - IDLE: AC is working in the FAN mode, cause the target temperature is reached; - - 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. -- **timeout** (*Optional*, unsigned integer, default ``150``): Packet timeout for `aux_ac` data receiver. +- **timeout** (*Optional*, unsigned integer, default ``300``): Packet timeout for `aux_ac` data receiver. In the most common use of `aux_ac`, it isn't necessary to change this value. This keyword is optional, so you may omit it. The only situation when you can play with timeout is heavily loaded ESP. When you are using your ESP for many hard tasks, it is possible that `aux_ac` does not have enough time to receive AC responses. In this case, you can slightly raise the timeout value. But the best solution would be to remove some of the tasks from the ESP. - The timeout is limited to a range from `150` to `600` milliseconds. Other values are possible only with source code modification. But I don't recommend that. + The timeout is limited to a range from `300` to `800` milliseconds. Other values are possible only with source code modification. But I don't recommend that. - **optimistic** (*Optional*, boolean, default ``true``): Whether entity states should be updated immediately after receiving a command from Home Assistant/ESPHome. -- **indoor_temperature** (*Optional*): Parameters of the room air temperature sensor. +- **indoor_ambient_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). + > **ATTENTION!** The sensor's name was changed in v.1.0.0 to synchronize with AUX service manuals. -- **outdoor_temperature** (*Optional*): Parameters of the outdoor temperature sensor. They are the same as the **indoor_temperature** (see description above). - > **Attention!** When the air conditioner is turned off, the outdoor temperature is updated rarely (every 6-7 hours). This isn't a bug of the component, but a feature of the air conditioner hardware. The only way to get changes more often is to create a template sensor, the temperature of which can be changed manually. When the air conditioner is working, the value of this sensor can be copied from the **outdoor_temperature**. When the air conditioner is turned off, the temperature value should be recalculated according to the dynamics of the **outbound_temperature** sensor (it changes frequently and shows values close to the air temperature when the air conditioner is turned off). You can't copy the value of **outbound_temperature** without changes to the template sensor in AC off mode, because these temperatures are not identical. +- **outdoor_ambient_temperature** (*Optional*): Parameters of the outdoor temperature sensor. They are the same as the **indoor_ambient_temperature** (see description above). + > **Attention!** When the air conditioner is turned off, the outdoor temperature is updated rarely (every 6-7 hours). This isn't a bug of the component, but a feature of the air conditioner hardware. The only way to get changes more often is to create a template sensor, the temperature of which can be changed manually. When the air conditioner is working, the value of this sensor can be copied from the **outdoor_ambient_temperature**. When the air conditioner is turned off, the temperature value should be recalculated according to the dynamics of the **compressor_suction_temperature** sensor (it changes frequently and shows values close to the air temperature when the air conditioner is turned off). You can't copy the value of **compressor_suction_temperature** without changes to the template sensor in AC off mode, because these temperatures are not identical. + > **ATTENTION!** The sensor's name was changed in v.1.0.0 to synchronize with AUX service manuals. -- **inbound_temperature** (*Optional*): Parameters of the coolant inbound temperature sensor. They are the same as the **indoor_temperature** (see description above). +- **outdoor_condenser_temperature** (*Optional*): Parameters of the temperature sensor for condenser in outdoor unit. All the parameters are the same as the **indoor_ambient_temperature** (see description above). -- **outbound_temperature** (*Optional*): Parameters of the coolant outbound temperature sensor. They are the same as the **indoor_temperature** (see description above). +- **indoor_coil_temperature** (*Optional*): Parameters of the temperature sensor for indoor unit coil. All the parameters are the same as the **indoor_ambient_temperature** (see description above). + > **ATTENTION!** The sensor's name was changed in v.1.0.0 to synchronize with AUX service manuals. -- **compressor_temperature** (*Optional*): Parameters of the compressor temperature sensor. They are the same as the **indoor_temperature** (see description above). +- **compressor_suction_temperature** (*Optional*): Parameters of the temperature sensor for compressor suction pipe. All the parameters are the same as the **indoor_ambient_temperature** (see description above). + > **ATTENTION!** The sensor's name was changed in v.1.0.0 to synchronize with AUX service manuals. + +- **compressor_discharge_temperature** (*Optional*): Parameters of the temperature sensor for compressor discharge pipe. All the parameters are the same as the **indoor_ambient_temperature** (see description above). + > **ATTENTION!** The sensor's name was changed in v.1.0.0 to synchronize with AUX service manuals. + +- **defrost_temperature** (*Optional*): Parameters of the defrost temperature sensor in the outdoor unit. All the parameters are the same as the **indoor_ambient_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. @@ -213,12 +223,12 @@ climate: - **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). -- **inverter_power** (*Optional*): The information for the inverter power sensor. All settings are the same as for the **indoor_temperature** (see description above). +- **inverter_power** (*Optional*): The information for the inverter power sensor. All settings are the same as for the **indoor_ambient_temperature** (see description above). > **ATTENTION!** The parameter name was changed in v.0.2.9 due to incorrect spelling. - **inverter_power_limit_state** (*Optional*): Configuration of the power limit state sensor. It displays the state of the power limitation function for the inverter HVAC (is it ON or OFF). All settings are the same as for the **display_state** (see description above). -- **inverter_power_limit_value** (*Optional*): Configuration of the power limit value sensor. All settings are the same as for the **indoor_temperature** (see description above). +- **inverter_power_limit_value** (*Optional*): Configuration of the power limit value sensor. All settings are the same as for the **indoor_ambient_temperature** (see description above). It reports the current value of the power limitation function for the inverter HVAC. This sensor represents the value only after the HVAC confirms the power limitation. The value is always in the range from 30% to 100%. This is the hardware limitation. - **preset_reporter** (*Optional*): Parameters of text sensor with current preset. All settings are the same as for the **display_state** (see description above). diff --git a/README.md b/README.md index 267be8f..1ec746c 100644 --- a/README.md +++ b/README.md @@ -96,29 +96,37 @@ climate: id: aux_id uart_id: ac_uart_bus period: 7s - show_action: true display_inverted: false - timeout: 150 - indoor_temperature: - name: AC Indoor Temperature - id: ac_indoor_temp + timeout: 300 + optimistic: true + indoor_ambient_temperature: + name: AC Indoor Ambient Temperature + id: ac_indoor_ambient_temp accuracy_decimals: 1 internal: false - outdoor_temperature: - name: AC Outdoor Temperature - id: ac_outdoor_temp + outdoor_ambient_temperature: + name: AC Outdoor Ambient Temperature + id: ac_outdoor_ambient_temp internal: false - outbound_temperature: - name: AC Coolant Outbound Temperature - id: ac_outbound_temp + outdoor_condenser_temperature: + name: AC Outdoor Condenser Temperature + id: ac_outdoor_condenser_temp internal: false - inbound_temperature: - name: AC Coolant Inbound Temperature - id: ac_inbound_temp + compressor_suction_temperature: + name: AC Compressor Suction Temperature + id: ac_compressor_suction_temp internal: false - compressor_temperature: - name: AC Compressor Temperature - id: ac_strange_temp + indoor_coil_temperature: + name: AC Indoor Coil Temperature + id: ac_indoor_coil_temp + internal: false + compressor_discharge_temperature: + name: AC Compressor Discharge Temperature + id: ac_compressor_discharge_temp + internal: false + defrost_temperature: + name: AC Defrost Temperature + id: ac_defrost_temp internal: false display_state: name: AC Display State @@ -183,37 +191,40 @@ climate: - **period** (*Опциональный*, [время](https://esphome.io/guides/configuration-types.html#config-time), по умолчанию ``7s``): Период между запросами статуса кондиционера. `Aux_ac` получает новое состояние кондиционера только после регулярного запроса, потому что сам кондиционер об изменении параметров своей работы не уведомляет. Поэтому нужно запрашивать его, вдруг пользователь установил иной режим работы с помощью ИК-пульта. -- **show_action** (*Опциональный*, логическое, по умолчанию ``true``): Показывать ли текущую задачу кондиционера (экспериментальная функция). Например, в режиме HEAT_COOL кондиционер может выполнять одну из следующих задач: - - НАГРЕВ: нагревает воздух в комнате; - - ПРОСТОЙ: кондиционер работает в режиме вентилятора для перемешивания воздуха в комнате, поскольку целевая температура уже достигнута; - - ОХЛАЖДЕНИЕ: кондиционер охлаждает воздух в комнате. - Аналогично будут отображаться действия кондиционера и для режимов ОТОПЛЕНИЕ и ОХЛАЖДЕНИЕ. Единственная разница будет в количестве действий: ПРОСТОЙ+НАГРЕВ для режима отопления и ПРОСТОЙ+ОХЛАЖДЕНИЕ для режима охлаждения комнаты. - - **display_inverted** (*Опциональный*, логическое, по умолчанию ``false``): Настраивает способ управления дисплеем. Как выяснилось (issue [#31](https://github.com/GrKoR/esphome_aux_ac_component/issues/31)), включение-выключение дисплея обрабатывается кондиционерами по разному. Кондиционеры Rovex включают дисплей по `0` в соответствующем бите команды и выключают по биту `1`. Многие другие модели кондиционеров поступают наоборот. -- **timeout** (*Опциональный*, неотрицательное целое, по умолчанию ``150``): Таймаут получения пакета для ресивера данных `aux_ac`. +- **timeout** (*Опциональный*, неотрицательное целое, по умолчанию ``300``): Таймаут получения пакета для ресивера данных `aux_ac`. Чаще всего вам это значение никогда не понадобится. Поскольку этот параметр опционален, то его можно смело пропустить, если нет необходимости менять таймауты. - Единственная ситуация, когда вам может пригодиться этот параметр, - это сильно загруженная ESP. Если по какой-то неподдающейся логике причине вы кроме `aux_ac` нагрузили свою ESP кучей дополнительных ресурсоемких задач, то у компонента может просто не хватать времени для оперативного приёма ответов от кондиционера. В этом в логе будут сообщения о том, что последовательность команд была прервана по таймауту. Чтобы это исправить, лучше, конечно, немного разгрузить ESP. Если это вам не подходит, тогда можно увеличить таймаут. - Значение таймаута в прошивке ограничено диапазоном от `150` до `600` миллисекунд. Устанавливать значения выше можно только отредактировав исходные коды компонента. Но сильно задирать таймаут не стоит. Кондиционер периодически рассылает пакеты без запроса со стороны `aux_ac` и это приводит к сбою в отправке команды. + Единственная ситуация, когда вам может пригодиться этот параметр, - это сильно загруженная ESP. Если по какой-то неподдающейся логике причине вы кроме `aux_ac` нагрузили свою ESP кучей дополнительных ресурсоемких задач, то у компонента может просто не хватать времени для оперативного приёма ответов от кондиционера. В этом случае в логе будут сообщения о том, что последовательность команд была прервана по таймауту. Чтобы это исправить, лучше, конечно, немного разгрузить ESP. Если это вам не подходит, тогда можно увеличить таймаут. + Значение таймаута в прошивке ограничено диапазоном от `300` до `800` миллисекунд. Устанавливать значения выше можно только отредактировав исходные коды компонента. Но сильно задирать таймаут не стоит. Кондиционер периодически рассылает пакеты без запроса со стороны `aux_ac` и это приводит к сбою в отправке команды. - **optimistic** (*Опциональный*, логическое, по умолчанию ``true``) В «оптимистичном» режиме компонент не ждёт от кондиционера изменения параметров работы, а сразу после отправки команды в кондиционер сообщает в Home Assistant о новом состоянии. Если кондиционер команду не принял, то спустя несколько секунд eps получит текущее состояние всех систем и отправит в умный дом реальное состояние кондиционера. В итоге, если подавать в кондиционер неподдерживаемые команды, они будут записываться в историю Home Assistant и спустя время сбрасываться сбрасываться. В «пессимистичном» режиме esp отправляет команду в кондиционер, но об изменении состояний не сообщает до тех пор, пока не получит информацию о фактическом режиме работы кондиционера. В большинстве случаев разница между этими режимами будет практически незаметна. -- **indoor_temperature** (*Опциональный*): Параметры создаваемого датчика температуры воздуха, если такой датчик нужен +- **indoor_ambient_temperature** (*Опциональный*): Параметры создаваемого датчика температуры воздуха, если такой датчик нужен - **name** (**Обязательный**, строка): Имя датчика температуры. - **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. + > **ВНИМАНИЕ!** Название сенсора было изменено в версии v.1.0.0 для синхронизации с сервисными схемами производителя кондиционеров. -- **outdoor_temperature** (*Опциональный*): Параметры создаваемого датчика уличной температуры воздуха, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_temperature** (см. выше). - > **ВНИМАНИЕ!** Когда кондиционер выключен, температура наружного воздуха обновляется редко (раз в 6-7 часов). Это не баг компонента, а особенность работы железа кондиционера. Единственный способ получать изменения чаще - создать шаблонный сенсор, температуру которого изменять вручную. Когда кондиционер работает, значение такого сенсора можно копировать из **outdoor_temperature**. Когда кондиционер выключен, значение температуры пересчитывать по динамике сенсора **outbound_temperature** (он изменяется часто и при выключенном кондее показывает значения близкие к температуре воздуха). Заморочки с пересчетом нужны потому, что показания сенсоров не идентичны и на графике значений шаблонного сенсора могут быть ступеньки при переходе с **outdoor_temperature** на **outbound_temperature** и обратно. +- **outdoor_ambient_temperature** (*Опциональный*): Параметры создаваемого датчика уличной температуры воздуха, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_ambient_temperature** (см. выше). + > **ВНИМАНИЕ!** Когда кондиционер выключен, температура наружного воздуха обновляется редко (раз в 6-7 часов). Это не баг компонента, а особенность работы железа кондиционера. Единственный способ получать изменения чаще - создать шаблонный сенсор, температуру которого изменять вручную. Когда кондиционер работает, значение такого сенсора можно копировать из **outdoor_ambient_temperature**. Когда кондиционер выключен, значение температуры пересчитывать по динамике сенсора **compressor_suction_temperature** (он изменяется часто и при выключенном кондее показывает значения близкие к температуре воздуха). Заморочки с пересчетом нужны потому, что показания сенсоров не идентичны и на графике значений шаблонного сенсора могут быть ступеньки при переходе с **outdoor_ambient_temperature** на **compressor_suction_temperature** и обратно. + > **ВНИМАНИЕ!** Название сенсора было изменено в версии v.1.0.0 для синхронизации с сервисными схемами производителя кондиционеров. -- **inbound_temperature** (*Опциональный*): Параметры создаваемого датчика температуры на подаче теплоносителя, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_temperature** (см. выше). +- **outdoor_condenser_temperature** (*Опциональный*): Параметры создаваемого датчика температуры конденсатора в наружном блоке кондиционера, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_ambient_temperature** (см. выше). -- **outbound_temperature** (*Опциональный*): Параметры создаваемого датчика температуры на обратке теплоносителя, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_temperature** (см. выше). +- **indoor_coil_temperature** (*Опциональный*): Параметры создаваемого датчика температуры на теплообменнике во внутреннем блоке кондиционера, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_ambient_temperature** (см. выше). + > **ВНИМАНИЕ!** Название сенсора было изменено в версии v.1.0.0 для синхронизации с сервисными схемами производителя кондиционеров. -- **compressor_temperature** (*Опциональный*): Параметры создаваемого датчика температуры компрессора, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_temperature** (см. выше). +- **compressor_suction_temperature** (*Опциональный*): Параметры создаваемого датчика температуры на входе в компрессор, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_ambient_temperature** (см. выше). + > **ВНИМАНИЕ!** Название сенсора было изменено в версии v.1.0.0 для синхронизации с сервисными схемами производителя кондиционеров. + +- **compressor_discharge_temperature** (*Опциональный*): Параметры создаваемого датчика температуры на выходе компрессора, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_ambient_temperature** (см. выше). + > **ВНИМАНИЕ!** Название сенсора было изменено в версии v.1.0.0 для синхронизации с сервисными схемами производителя кондиционеров. + +- **defrost_temperature** (*Опциональный*): Параметры создаваемого датчика температуры разморозки в наружном блоке кондиционера, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_ambient_temperature** (см. выше). - **display_state** (*Опциональный*): Параметры создаваемого датчика дисплея (включен или выключен), если такой датчик нужен. - **name** (**Обязательный**, строка): Имя датчика дисплея. @@ -223,12 +234,12 @@ climate: - **defrost_state** (*Опциональный*): Параметры создаваемого датчика состояния разморозки (включена или выключена), если такой датчик нужен. Параметры аналогичны датчику дисплея **display_state**. -- **inverter_power** (*Опциональный*): Параметры создаваемого датчика мощности инвертора, если такой датчик нужен. Параметры аналогичны датчику дисплея **indoor_temperature**. +- **inverter_power** (*Опциональный*): Параметры создаваемого датчика мощности инвертора, если такой датчик нужен. Параметры аналогичны датчику дисплея **display_state**. > **ВНИМАНИЕ!** Название параметра было изменено в версии v.0.2.9 в рамках борьбы с безграмотностью. - **inverter_power_limit_state** (*Опциональный*): Параметры создаваемого датчика состояния функции ограничения мощности. Показывает, включена данная функция в настоящий момент или нет. По очевидным причинам актуально только для инверторных кондиционеров, для "старт-стоп" кондиционеров всегда будет "выключен". Параметры аналогичны датчику дисплея **display_state**. -- **inverter_power_limit_value** (*Опциональный*): Параметры создаваемого датчика текущего ограничения мощности, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_temperature** (см. выше). +- **inverter_power_limit_value** (*Опциональный*): Параметры создаваемого датчика текущего ограничения мощности, если такой датчик нужен. Параметры аналогичны датчику внутренней температуры **indoor_ambient_temperature** (см. выше). Сенсор отображает текущее значение ограничения максимальной мощности для инверторного кондиционера. Значение в процентах. С кондиционерами "старт-стоп" по очевидным причинам не работает, всегда показывая значение `0%`. Заданное пользователем значения лимита будет отображено только после того, как кондиционер подтвердит полученное значение и начнет с ним работать. В силу ограничений на уровне железа лимит мощности может быть задан только в пределах от `30%` до `100%`. diff --git a/components/aux_ac/aircon.cpp b/components/aux_ac/aircon.cpp new file mode 100644 index 0000000..32ed696 --- /dev/null +++ b/components/aux_ac/aircon.cpp @@ -0,0 +1,753 @@ +#include "aircon.h" +#include "helpers.h" +#include "command_builder.h" +#include "frame_processor_manager.h" +#include "esphome/core/log.h" + +namespace esphome +{ + namespace aux_airconditioner + { + /*************************************************************************************************\ + \*************************************************************************************************/ + uint32_t Capabilities::normilize_packet_timeout(uint32_t timeout) + { + uint32_t result = timeout; + if (result > Capabilities::AC_PACKET_TIMEOUT_MAX) + result = Capabilities::AC_PACKET_TIMEOUT_MAX; + else if (result < Capabilities::AC_PACKET_TIMEOUT_MIN) + result = Capabilities::AC_PACKET_TIMEOUT_MIN; + return result; + } + + float Capabilities::normilize_target_temperature(const float target_temperature) + { + float result = target_temperature; + if (result > Capabilities::AC_MAX_TEMPERATURE) + result = Capabilities::AC_MAX_TEMPERATURE; + else if (result < Capabilities::AC_MIN_TEMPERATURE) + result = Capabilities::AC_MIN_TEMPERATURE; + return result; + } + + uint8_t Capabilities::normilize_inverter_power_limit(const uint8_t power_limit_value) + { + uint8_t result = power_limit_value; + if (result > Capabilities::AC_MAX_INVERTER_POWER_LIMIT) + result = Capabilities::AC_MAX_INVERTER_POWER_LIMIT; + else if (result < Capabilities::AC_MIN_INVERTER_POWER_LIMIT) + result = Capabilities::AC_MIN_INVERTER_POWER_LIMIT; + return result; + } + + // ************************************************************************************************** + const std::string Capabilities::AC_FIRMWARE_VERSION = "1.0.0"; + + // ************************************************************************************************** + // custom fan modes + const std::string Capabilities::CUSTOM_FAN_MODE_MUTE = "MUTE"; + const std::string Capabilities::CUSTOM_FAN_MODE_TURBO = "TURBO"; + // ************************************************************************************************** + // custom presets + const std::string Capabilities::CUSTOM_PRESET_CLEAN = "CLEAN"; + const std::string Capabilities::CUSTOM_PRESET_HEALTH = "HEALTH"; + const std::string Capabilities::CUSTOM_PRESET_ANTIFUNGUS = "ANTIFUNGUS"; + // ************************************************************************************************** + // predefined default params + const float Capabilities::AC_MIN_TEMPERATURE = 16.0; + const float Capabilities::AC_MAX_TEMPERATURE = 32.0; + const float Capabilities::AC_TEMPERATURE_STEP_TARGET = 0.5; + const float Capabilities::AC_TEMPERATURE_STEP_CURRENT = 0.1; + const uint8_t Capabilities::AC_MIN_INVERTER_POWER_LIMIT = 30; // 30% + const uint8_t Capabilities::AC_MAX_INVERTER_POWER_LIMIT = 100; // 100% + const uint32_t Capabilities::AC_STATE_REQUEST_INTERVAL = 7000; + const uint32_t Capabilities::AC_CONNECTION_LOST_TIMEOUT = 4000; + const uint32_t Capabilities::AC_PACKET_TIMEOUT_MIN = 300; + const uint32_t Capabilities::AC_PACKET_TIMEOUT_MAX = 800; + + // ************************************************************************************************** + using esphome::helpers::update_property; + + // ************************************************************************************************** + void AirCon::_send_frame_from_tx_queue() + { + if (!this->is_hardware_connected() || !this->has_ping()) + return; + + if (this->_tx_frames.empty()) + return; + + Frame *frame = this->_tx_frames.front(); + this->_tx_frames.pop(); + + frame->send(*this->_uart); + + if (frame->get_frame_type() == FrameType::FRAME_TYPE_COMMAND) + { + _waiting_for_response_timer.reset(); + _waiting_for_response_timer.set_callback([this](TimerInterface *timer) + { ESP_LOGW(TAG, "Command response timeout!"); + timer->stop(); + this->set_receiver_callback(nullptr); }); + switch (frame->get_value(8)) + { + case 0x01: // command: set AC mode + { + uint8_t crc16_1 = 0, crc16_2 = 0; + frame->get_crc(crc16_1, crc16_2); + this->set_receiver_callback([this, crc16_1, crc16_2](Frame &frame) + { if (this->_waiting_for_response_timer.is_enabled() && + frame.get_frame_type() == FrameType::FRAME_TYPE_RESPONSE && + frame.get_value(9) == 0x01) + { + // check the acknowledgement: should be equal to the command CRC + if (frame.get_value(10) != crc16_1 || + frame.get_value(11) != crc16_2) + { + ESP_LOGW(TAG, "Command response acknowledgement error!"); + } + this->_waiting_for_response_timer.stop(); + this->_waiting_for_response_timer.set_callback(helpers::dummy_stopper); + this->set_receiver_callback(nullptr); + if (!this->_command_queue.empty() && + this->_cmd_processor_state == command_processor_state_t::CMD_PROCESSOR_STATE_CMD_WAS_SENT) + { + this->_cmd_processor_state = command_processor_state_t::CMD_PROCESSOR_STATE_POSTCHECK_DONE; + } + } }); + _waiting_for_response_timer.start(this->get_packet_timeout()); + break; + } + + case 0x11: // command: request frame 11 + { + this->set_receiver_callback([this](Frame &frame) + { if (this->_waiting_for_response_timer.is_enabled() && + frame.get_frame_type() == FrameType::FRAME_TYPE_RESPONSE && + frame.get_value(9) == 0x11) + { + this->_waiting_for_response_timer.stop(); + this->_waiting_for_response_timer.set_callback(helpers::dummy_stopper); + this->set_receiver_callback(nullptr); + if (!this->_command_queue.empty() && + this->_cmd_processor_state == command_processor_state_t::CMD_PROCESSOR_STATE_WAITING_FOR_F11) + { + this->_cmd_processor_state = command_processor_state_t::CMD_PROCESSOR_STATE_PRECHECK_DONE; + } + } }); + _waiting_for_response_timer.start(this->get_packet_timeout()); + break; + } + + case 0x21: // command: request frame 21 + { + this->set_receiver_callback([this](Frame &frame) + { if (this->_waiting_for_response_timer.is_enabled() && + frame.get_frame_type() == FrameType::FRAME_TYPE_RESPONSE && + frame.get_value(9) == 0x21) + { + this->_waiting_for_response_timer.stop(); + this->_waiting_for_response_timer.set_callback(helpers::dummy_stopper); + this->set_receiver_callback(nullptr); + } }); + _waiting_for_response_timer.start(this->get_packet_timeout()); + break; + } + + default: + ESP_LOGW(TAG, "Unknown command: 0x%02X", frame->get_value(8)); + this->_waiting_for_response_timer.set_callback(helpers::dummy_stopper); + break; + } + } + + delete frame; + } + + // ************************************************************************************************** + void AirCon::_process_command_queue() + { + if (_command_queue.size() == 0) + return; + + Frame frame; + ClimateCall &cmd = _command_queue.front(); + switch (this->_cmd_processor_state) + { + case CMD_PROCESSOR_STATE_NOT_STARTED: + this->_cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_REQUEST_11).fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + this->_cmd_processor_state = command_processor_state_t::CMD_PROCESSOR_STATE_WAITING_FOR_F11; + break; + + case CMD_PROCESSOR_STATE_WAITING_FOR_F11: + ESP_LOGW(TAG, "It should never have happened: processing the command with state 'CMD_PROCESSOR_STATE_WAITING_FOR_F11'"); + break; + + case CMD_PROCESSOR_STATE_PRECHECK_DONE: + this->_cmd_builder->init_new_command(cmd).fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + this->_cmd_processor_state = command_processor_state_t::CMD_PROCESSOR_STATE_CMD_WAS_SENT; + break; + + case CMD_PROCESSOR_STATE_CMD_WAS_SENT: + ESP_LOGW(TAG, "It should never have happened: processing the command with state 'CMD_PROCESSOR_STATE_CMD_WAS_SENT'"); + break; + + case CMD_PROCESSOR_STATE_POSTCHECK_DONE: + this->schedule_frame_to_send(*_frame_11_request); + this->schedule_frame_to_send(*_frame_2x_request); + this->_cmd_processor_state = command_processor_state_t::CMD_PROCESSOR_STATE_NOT_STARTED; + _command_queue.pop(); + break; + + default: + ESP_LOGW(TAG, "unknown command state '0x%02X'!", this->_cmd_processor_state); + this->_cmd_processor_state = command_processor_state_t::CMD_PROCESSOR_STATE_NOT_STARTED; + _command_queue.pop(); + break; + } + } + + // ************************************************************************************************** + AirCon::AirCon() + { + _incoming_frame = new Frame; + _last_frame_11 = new Frame; + _last_frame_2x = new Frame; + + _frame_processor_manager = new FrameProcessorManager; + _frame_processor_manager->set_aircon(*this); + + _cmd_builder = new CommandBuilder(*this); + + this->set_millis(&millis); + _timer_manager.set_millis_func(&millis); + + _frame_11_request = new Frame; + this->_cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_REQUEST_11).fill_frame_with_command(*_frame_11_request); + + _frame_2x_request = new Frame; + this->_cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_REQUEST_21).fill_frame_with_command(*_frame_2x_request); + + _frame_ping_response = new Frame; + _frame_ping_response->append_data({Frame::get_start_byte(), 0x00, FrameType::FRAME_TYPE_PING, FrameDirection::FRAME_DIR_TO_AC, 0x01, 0x00, 0x08, 0x00}); + _frame_ping_response->append_data({0x1C, 0x27, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}); + _frame_ping_response->update_crc(true); + } + + AirCon::~AirCon() + { + delete _cmd_builder; + delete _frame_processor_manager; + + delete _incoming_frame; + delete _last_frame_11; + delete _last_frame_2x; + delete _frame_11_request; + delete _frame_2x_request; + delete _frame_ping_response; + } + + // ************************************************************************************************** + void AirCon::setup() + { + _traits.set_supports_current_temperature(true); + _traits.set_supports_two_point_target_temperature(false); + + _traits.set_visual_min_temperature(Capabilities::AC_MIN_TEMPERATURE); + _traits.set_visual_max_temperature(Capabilities::AC_MAX_TEMPERATURE); + _traits.set_visual_current_temperature_step(Capabilities::AC_TEMPERATURE_STEP_CURRENT); + _traits.set_visual_target_temperature_step(Capabilities::AC_TEMPERATURE_STEP_TARGET); + + /* + 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_preset(ClimatePreset::CLIMATE_PRESET_NONE); + + // if the climate device supports reporting the active current action of the device with the action property. + //_traits.set_supports_action(this->_show_action); + + _frame11_request_timer.set_callback([this](TimerInterface *timer) + { this->schedule_frame_to_send(*this->_frame_11_request); }); + _timer_manager.register_timer(_frame11_request_timer); + _frame11_request_timer.start(this->get_period()); + + _frame2x_request_timer.set_callback([this](TimerInterface *timer) + { this->schedule_frame_to_send(*this->_frame_2x_request); }); + _timer_manager.register_timer(_frame2x_request_timer); + _frame2x_request_timer.start(this->get_period()); + + _ping_timeout_timer.set_callback([this](TimerInterface *timer) + { this->_has_ping = false; + ESP_LOGW(TAG, "Air conditioner connection lost!"); }); + _timer_manager.register_timer(_ping_timeout_timer); + _ping_timeout_timer.start(Capabilities::AC_CONNECTION_LOST_TIMEOUT); + + _timer_manager.register_timer(_waiting_for_response_timer); + _waiting_for_response_timer.stop(); + + // schedule initial requests + this->schedule_frame_to_send(*this->_frame_11_request); + this->schedule_frame_to_send(*this->_frame_2x_request); + } + + // ************************************************************************************************** + void AirCon::loop() + { + if (!this->is_hardware_connected()) + return; + + _timer_manager.task(); + + FrameState frame_state = _incoming_frame->get_frame_state(); + switch (frame_state) + { + case FRAME_STATE_BLANK: + case FRAME_STATE_PARTIALLY_LOADED: + if (_uart->available() > 0) + { + _incoming_frame->load(*_uart); + } + else if (!this->_waiting_for_response_timer.is_enabled()) + { + if (!this->_tx_frames.empty()) + { + this->_send_frame_from_tx_queue(); + } + else if (!this->_command_queue.empty()) + { + this->_process_command_queue(); + } + } + break; + + case FRAME_STATE_ERROR: + _incoming_frame->set_frame_time(this->ms()); + ESP_LOGW(TAG, "Incorrect frame! Frame state: %s, data: %s", _incoming_frame->state_to_string().c_str(), _incoming_frame->to_string(true).c_str()); + _incoming_frame->clear(); + break; + + case FRAME_STATE_OK: + _incoming_frame->set_frame_time(this->ms()); + ESP_LOGD(TAG, "%s", _incoming_frame->to_string(true).c_str()); + + this->_frame_processor_manager->process_frame(*_incoming_frame); + + if (this->_receiver_callback != nullptr) + this->_receiver_callback(*_incoming_frame); + + _incoming_frame->clear(); + break; + + default: + ESP_LOGW(TAG, "Unknown frame state: %d (0x%02X)", frame_state, frame_state); + break; + } + } + + // ************************************************************************************************** + void AirCon::dump_config() + { + ESP_LOGCONFIG(TAG, "AUX HVAC:"); + ESP_LOGCONFIG(TAG, "firmware version: %s", Capabilities::AC_FIRMWARE_VERSION.c_str()); + this->dump_traits_(TAG); + + LOG_SENSOR(" ", "Vertical louver state", this->_sensor_vlouver_state); + LOG_BINARY_SENSOR(" ", "Display", this->_sensor_display_state); + LOG_BINARY_SENSOR(" ", "Defrost status", this->_sensor_defrost_state); + LOG_TEXT_SENSOR(" ", "Preset Reporter", this->_sensor_preset_reporter); + + ESP_LOGCONFIG(TAG, " Temperatures:"); + LOG_SENSOR(" ", "Indoor Ambient", this->_sensor_temperature_indoor_ambient); + LOG_SENSOR(" ", "Indoor Coil", this->_sensor_temperature_indoor_coil); + LOG_SENSOR(" ", "Outdoor Ambient", this->_sensor_temperature_outdoor_ambient); + LOG_SENSOR(" ", "Outdoor Condenser", this->_sensor_temperature_outdoor_condenser_middle); + LOG_SENSOR(" ", "Outdoor Defrost", this->_sensor_temperature_outdoor_defrost); + LOG_SENSOR(" ", "Outdoor Discharge", this->_sensor_temperature_outdoor_discharge); + LOG_SENSOR(" ", "Outdoor Suction", this->_sensor_temperature_outdoor_suction); + + ESP_LOGCONFIG(TAG, " Inverter Power:"); + LOG_SENSOR(" ", "Actual Value", this->_sensor_inverter_power_actual); + LOG_SENSOR(" ", "Limit Value", this->_sensor_inverter_power_limit_value); + LOG_BINARY_SENSOR(" ", "Limitation State", this->_sensor_inverter_power_limit_state); + }; + + // ************************************************************************************************** + void AirCon::control(const esphome::climate::ClimateCall &call) + { + bool has_command = false; + + // User requested mode change + if (call.get_mode().has_value()) + { + ClimateMode mode = *call.get_mode(); + update_property(this->mode, mode, has_command); + } + + // User requested fan_mode change + if (call.get_fan_mode().has_value()) + { + ClimateFanMode fanmode = *call.get_fan_mode(); + update_property(this->fan_mode, fanmode, has_command); + } + else if (call.get_custom_fan_mode().has_value()) + { + std::string customfanmode = *call.get_custom_fan_mode(); + if ((customfanmode == Capabilities::CUSTOM_FAN_MODE_TURBO) || + (customfanmode == Capabilities::CUSTOM_FAN_MODE_MUTE) || + (customfanmode == "")) + { + update_property(this->custom_fan_mode, customfanmode, has_command); + } + } + + // User selected preset + if (call.get_preset().has_value()) + { + ClimatePreset preset = *call.get_preset(); + update_property(this->preset, preset, has_command); + } + else if (call.get_custom_preset().has_value()) + { + std::string custom_preset = *call.get_custom_preset(); + if ((custom_preset == Capabilities::CUSTOM_PRESET_CLEAN) || + (custom_preset == Capabilities::CUSTOM_PRESET_ANTIFUNGUS) || + (custom_preset == Capabilities::CUSTOM_PRESET_HEALTH) || + (custom_preset == "")) + { + update_property(this->custom_preset, custom_preset, has_command); + } + } + + // User requested swing_mode change + if (call.get_swing_mode().has_value()) + { + ClimateSwingMode swingmode = *call.get_swing_mode(); + update_property(this->swing_mode, swingmode, has_command); + } + + // User requested target temperature change + if (call.get_target_temperature().has_value()) + { + // it isn't allowed in FAN mode + if (this->mode != ClimateMode::CLIMATE_MODE_FAN_ONLY) + update_property(this->target_temperature, *call.get_target_temperature(), has_command); + } + + if (has_command) + { + this->schedule_command(call); + + if (this->get_optimistic()) + this->publish_all_states(); + } + } + + // ************************************************************************************************** + void AirCon::action_display_off() + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_display_state(false); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_display_on() + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_display_state(true); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_set_vlouver_swing() + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_vertical_louver(ac_louver_V::AC_LOUVERV_SWING_UPDOWN); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_set_vlouver_stop() + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_vertical_louver(ac_louver_V::AC_LOUVERV_OFF); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_set_vlouver_top_position() + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_vertical_louver(ac_louver_V::AC_LOUVERV_TOP); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_set_vlouver_middle_above_position() + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_vertical_louver(ac_louver_V::AC_LOUVERV_MIDDLE_ABOVE); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_set_vlouver_middle_position() + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_vertical_louver(ac_louver_V::AC_LOUVERV_MIDDLE); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_set_vlouver_middle_below_position() + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_vertical_louver(ac_louver_V::AC_LOUVERV_MIDDLE_BELOW); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_set_vlouver_bottom() + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_vertical_louver(ac_louver_V::AC_LOUVERV_BOTTOM); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_set_vlouver_position(vlouver_esphome_position_t position) + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_vertical_louver(vlouver_frontend_to_ac_louver_V(position)); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_power_limitation_off() + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE).set_inverter_power_limitation_state(false); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::action_power_limitation_on(uint8_t limit) + { + _cmd_builder->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE) + .set_inverter_power_limitation_state(true) + .set_inverter_power_limitation_value(Capabilities::normilize_inverter_power_limit(limit)); + Frame frame; + _cmd_builder->fill_frame_with_command(frame); + this->schedule_frame_to_send(frame); + } + + // ************************************************************************************************** + void AirCon::schedule_frame_to_send(const Frame &frame) + { + Frame *tx_frame = new Frame(frame); + _tx_frames.push(tx_frame); + } + + // ************************************************************************************************** + void AirCon::schedule_ping_response() + { + this->schedule_frame_to_send(*_frame_ping_response); + } + + // ************************************************************************************************** + void AirCon::schedule_command(const ClimateCall &cmd) + { + _command_queue.push(cmd); + } + + // ************************************************************************************************** + Frame &AirCon::get_last_frame_11() + { + return *(this->_last_frame_11); + } + + // ************************************************************************************************** + Frame &AirCon::get_last_frame_2x() + { + return *(this->_last_frame_2x); + } + + // ************************************************************************************************** + void AirCon::set_last_frame(const Frame &frame) + { + if (frame.get_frame_type() != FrameType::FRAME_TYPE_RESPONSE) + return; + + if (frame.get_body_length() < 2) // filter out frames without CMD byte + return; + + Frame *target_frame = nullptr; + if (frame.get_value(9) == 0x11) + { + target_frame = _last_frame_11; + } + else if (frame.get_value(9, 0b11110000) == 0x20) + { + target_frame = _last_frame_2x; + } + + if (target_frame != nullptr) + { + target_frame->clear(); + target_frame->append_data(frame.data(), frame.size(), true); + target_frame->set_frame_time(this->ms()); + } + } + + // ************************************************************************************************** + void AirCon::_update_sensor_unit_of_measurement(Sensor *sensor) + { + if (sensor == nullptr) + return; + + if (this->temperature_in_fahrenheit && sensor->get_unit_of_measurement() != "°F") + sensor->set_unit_of_measurement("°F"); + else if (!this->temperature_in_fahrenheit && sensor->get_unit_of_measurement() != "°C") + sensor->set_unit_of_measurement("°C"); + } + + void AirCon::update_all_sensors_unit_of_measurement() + { + this->_update_sensor_unit_of_measurement(_sensor_temperature_indoor_ambient); + this->_update_sensor_unit_of_measurement(_sensor_temperature_indoor_coil); + this->_update_sensor_unit_of_measurement(_sensor_temperature_outdoor_condenser_middle); + this->_update_sensor_unit_of_measurement(_sensor_temperature_outdoor_ambient); + this->_update_sensor_unit_of_measurement(_sensor_temperature_outdoor_defrost); + this->_update_sensor_unit_of_measurement(_sensor_temperature_outdoor_discharge); + this->_update_sensor_unit_of_measurement(_sensor_temperature_outdoor_suction); + } + + // ************************************************************************************************** + template + void publish_sensor_state(Sensor *sensor, optional new_state) + { + if (sensor == nullptr) + return; + + if (new_state.has_value() && !std::isnan((float)(new_state.value()))) + { + if (sensor->get_raw_state() == (float)(new_state.value())) + return; + + sensor->publish_state((float)(new_state.value())); + return; + } + + if (std::isnan(sensor->get_raw_state())) + return; + + sensor->publish_state(NAN); + } + + // ************************************************************************************************** + void publish_sensor_state(BinarySensor *sensor, optional new_state) + { + if (sensor == nullptr) + return; + + if (!new_state.has_value()) + return; + + if (sensor->state == new_state.value()) + return; + + sensor->publish_state(new_state.value()); + } + + // ************************************************************************************************** + void publish_sensor_state(TextSensor *sensor, optional new_state) + { + if (sensor == nullptr) + return; + + if (!new_state.has_value()) + return; + + if (sensor->get_raw_state() == new_state.value()) + return; + + sensor->publish_state(new_state.value()); + } + + // ************************************************************************************************** + void AirCon::publish_all_states() + { + this->publish_state(); + + publish_sensor_state(_sensor_temperature_indoor_ambient, optional(this->current_temperature)); + publish_sensor_state(_sensor_temperature_indoor_coil, this->temperature_indoor_coil); + publish_sensor_state(_sensor_temperature_outdoor_condenser_middle, this->temperature_condenser_middle); + publish_sensor_state(_sensor_temperature_outdoor_ambient, this->temperature_outdoor_ambient); + publish_sensor_state(_sensor_temperature_outdoor_defrost, this->temperature_outdoor_defrost); + publish_sensor_state(_sensor_temperature_outdoor_discharge, this->temperature_outdoor_discharge); + publish_sensor_state(_sensor_temperature_outdoor_suction, this->temperature_outdoor_suction); + + publish_sensor_state(_sensor_vlouver_state, optional(this->get_current_vlouver_frontend_state())); + publish_sensor_state(_sensor_display_state, optional(this->display_enabled)); + publish_sensor_state(_sensor_defrost_state, optional(this->defrost_enabled)); + + publish_sensor_state(_sensor_inverter_power_actual, this->inverter_power); + publish_sensor_state(_sensor_inverter_power_limit_value, this->inverter_power_limitation_value); + publish_sensor_state(_sensor_inverter_power_limit_state, this->inverter_power_limitation_on); + + std::string state_str = ""; + if (this->preset == ClimatePreset::CLIMATE_PRESET_SLEEP) + { + state_str += "SLEEP"; + } + else if (this->custom_preset.has_value()) + { + state_str += this->custom_preset.value().c_str(); + } + else + { + state_str += "NONE"; + } + publish_sensor_state(_sensor_preset_reporter, optional(state_str)); + } + + // ************************************************************************************************** + // converts vertical louver state from hardware codes to frontend code + vlouver_esphome_position_t AirCon::aux_vlouver_to_frontend(const ac_louver_V vLouver) + { + return ac_louver_V_to_vlouver_frontend(vLouver); + } + + // ************************************************************************************************** + // current vertical louver position in esphome codes + vlouver_esphome_position_t AirCon::get_current_vlouver_frontend_state() + { + return aux_vlouver_to_frontend(this->louver_vertical); + } + + // ************************************************************************************************** + // converts vertical louver position from frontend codes to hardware code + ac_louver_V AirCon::frontend_vlouver_to_aux(const vlouver_esphome_position_t vLouver) + { + return vlouver_frontend_to_ac_louver_V(vLouver); + } + } // namespace aux_airconditioner +} // namespace esphome diff --git a/components/aux_ac/aircon.h b/components/aux_ac/aircon.h new file mode 100644 index 0000000..8e11d2a --- /dev/null +++ b/components/aux_ac/aircon.h @@ -0,0 +1,276 @@ +#pragma once + +#include // for NAN +#include + +#include "esphome.h" +#include "esphome/components/binary_sensor/binary_sensor.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/sensor/sensor.h" +#include "esphome/components/text_sensor/text_sensor.h" +#include "esphome/components/uart/uart_component.h" +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include "esphome/core/optional.h" + +#include "frame.h" +#include "aircon_common.h" +#include "helpers.h" + +namespace esphome +{ + namespace aux_airconditioner + { + + using esphome::Component; + using esphome::binary_sensor::BinarySensor; + using esphome::climate::Climate; + using esphome::climate::ClimateCall; + using esphome::climate::ClimatePreset; + using esphome::climate::ClimateSwingMode; + using esphome::climate::ClimateTraits; + using esphome::sensor::Sensor; + using esphome::text_sensor::TextSensor; + using esphome::uart::UARTComponent; + + using esphome::helpers::Timer; + using esphome::helpers::TimerInterface; + using esphome::helpers::TimerManager; + + using millis_function_t = uint32_t (*)(); + + static const char *const TAG = "AirCon"; + + /*************************************************************************************************\ + \*************************************************************************************************/ + class Capabilities + { + public: + // ************************************************************************************************** + static const std::string AC_FIRMWARE_VERSION; + // ************************************************************************************************** + // custom fan modes + static const std::string CUSTOM_FAN_MODE_MUTE; + static const std::string CUSTOM_FAN_MODE_TURBO; + // ************************************************************************************************** + // custom presets + static const std::string CUSTOM_PRESET_CLEAN; + static const std::string CUSTOM_PRESET_HEALTH; + static const std::string CUSTOM_PRESET_ANTIFUNGUS; + // ************************************************************************************************** + // predefined default params + static const uint32_t AC_STATE_REQUEST_INTERVAL; + static const uint32_t AC_CONNECTION_LOST_TIMEOUT; + + static const uint32_t AC_PACKET_TIMEOUT_MIN; + static const uint32_t AC_PACKET_TIMEOUT_MAX; + static uint32_t normilize_packet_timeout(uint32_t timeout); + + static const float AC_TEMPERATURE_STEP_TARGET; + static const float AC_TEMPERATURE_STEP_CURRENT; + static const float AC_MIN_TEMPERATURE; + static const float AC_MAX_TEMPERATURE; + static float normilize_target_temperature(const float target_temperature); + + static const uint8_t AC_MIN_INVERTER_POWER_LIMIT; + static const uint8_t AC_MAX_INVERTER_POWER_LIMIT; + static uint8_t normilize_inverter_power_limit(const uint8_t power_limit_value); + }; + + /*************************************************************************************************\ + \*************************************************************************************************/ + class FrameProcessorManager; + class CommandBuilder; + class Frame; + + class AirCon : public Component, + public Climate + { + private: + void _update_sensor_unit_of_measurement(Sensor *sensor); + + protected: + // esphome sensors that display the parameters of the air conditioner + Sensor *_sensor_temperature_indoor_ambient{nullptr}; + Sensor *_sensor_temperature_indoor_coil{nullptr}; + Sensor *_sensor_temperature_outdoor_ambient{nullptr}; + Sensor *_sensor_temperature_outdoor_condenser_middle{nullptr}; + Sensor *_sensor_temperature_outdoor_defrost{nullptr}; + Sensor *_sensor_temperature_outdoor_discharge{nullptr}; + Sensor *_sensor_temperature_outdoor_suction{nullptr}; + + Sensor *_sensor_vlouver_state{nullptr}; + BinarySensor *_sensor_display_state{nullptr}; + BinarySensor *_sensor_defrost_state{nullptr}; + TextSensor *_sensor_preset_reporter{nullptr}; + Sensor *_sensor_inverter_power_actual{nullptr}; + Sensor *_sensor_inverter_power_limit_value{nullptr}; + BinarySensor *_sensor_inverter_power_limit_state{nullptr}; + + ClimateTraits _traits; + UARTComponent *_uart{nullptr}; + bool _display_inverted{false}; + bool _optimistic{true}; // in optimistic mode, the entity states are updated immediately after receiving a command from Home Assistant/ESPHome + uint32_t _update_period{Capabilities::AC_STATE_REQUEST_INTERVAL}; + uint32_t _packet_timeout{Capabilities::AC_PACKET_TIMEOUT_MIN}; + + bool _has_ping{false}; + millis_function_t _millis_func{nullptr}; + + std::queue _tx_frames; + Frame *_incoming_frame{nullptr}; + Frame *_last_frame_11{nullptr}; + Frame *_last_frame_2x{nullptr}; + Frame *_frame_ping_response{nullptr}; + Frame *_frame_11_request{nullptr}; + Frame *_frame_2x_request{nullptr}; + FrameProcessorManager *_frame_processor_manager{nullptr}; + + std::queue _command_queue; + command_processor_state_t _cmd_processor_state{CMD_PROCESSOR_STATE_NOT_STARTED}; + + CommandBuilder *_cmd_builder{nullptr}; + + void _send_frame_from_tx_queue(); + void _process_command_queue(); + + TimerManager _timer_manager; + Timer _frame11_request_timer; + Timer _frame2x_request_timer; + Timer _waiting_for_response_timer; + Timer _ping_timeout_timer; + + std::function _receiver_callback = nullptr; + + public: + AirCon(); + ~AirCon(); + + // ************************************************************************************************** + // derived methods + float get_setup_priority() const override { return esphome::setup_priority::DATA; } + virtual ClimateTraits traits() override { return _traits; } + virtual void setup() override; + virtual void loop() override; + virtual void dump_config() override; + virtual void control(const esphome::climate::ClimateCall &call) override; + + // ************************************************************************************************** + // current state + // ------- derived from Climate parameters ------- + // ClimateMode mode{CLIMATE_MODE_OFF}; /// The active mode of the climate device. + // ClimateAction action{CLIMATE_ACTION_OFF}; /// The active state of the climate device. + // float current_temperature{NAN}; /// The current temperature of the climate device, as reported from the integration. + // float target_temperature; /// The target temperature of the climate device. + // ClimateFanMode fan_mode{CLIMATE_FAN_OFF}; /// The active fan mode of the climate device. + // std::string custom_fan_mode{}; /// The active custom fan mode of the climate device. + // ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; /// The active swing mode of the climate device. + // ClimatePreset preset{CLIMATE_PRESET_NONE}; /// The active preset of the climate device. + // std::string custom_preset{}; /// The active custom preset mode of the climate device. + // ------- own parameters ------- + ac_louver_V louver_vertical{AC_LOUVERV_OFF}; + ac_louver_H louver_horizontal{AC_LOUVERH_OFF}; + bool temperature_in_fahrenheit{false}; + bool display_enabled{true}; + uint8_t last_IR_passed{0}; // time since last IR-remote command passed + + optional inverter_power_limitation_on{false}; + optional inverter_power_limitation_value{100}; + bool ac_type_inverter{false}; + + optional temperature_indoor_coil{}; // byte 17, cmd=0x21 + optional temperature_condenser_middle{}; // byte 20, cmd=0x21 + optional temperature_outdoor_ambient{}; // byte 18, cmd=0x21 + optional temperature_outdoor_suction{}; // byte 21, cmd=0x21 + optional temperature_outdoor_discharge{}; // byte 22, cmd=0x21 + optional temperature_outdoor_defrost{}; // byte 23, cmd=0x21 + ac_fanspeed_real real_fan_speed{AC_REAL_FAN_OFF}; + optional inverter_power{0}; + bool defrost_enabled{false}; + + // ************************************************************************************************** + // settings & config + void set_uart(UARTComponent &uart) { _uart = &uart; } + void set_uart(UARTComponent *uart) { _uart = uart; } + UARTComponent &get_uart() { return *_uart; } + bool is_hardware_connected() { return _uart != nullptr; } + bool has_ping() { return this->_has_ping; } + void reset_ping_timeout() + { + this->_has_ping = true; + this->_ping_timeout_timer.reset(); + } + void set_millis(millis_function_t millis) { _millis_func = millis; } + uint32_t ms() { return (_millis_func != nullptr) ? _millis_func() : 0; } + void set_display_inversion(bool inversion) { _display_inverted = inversion; } + bool get_display_inversion() { return _display_inverted; } + void set_optimistic(bool optimistic) { this->_optimistic = optimistic; } + bool get_optimistic() { return this->_optimistic; } + void set_period(uint32_t ms) { this->_update_period = ms; } + uint32_t get_period() { return this->_update_period; } + void set_packet_timeout(uint32_t ms) { this->_packet_timeout = Capabilities::normilize_packet_timeout(ms); } + uint32_t get_packet_timeout() { return this->_packet_timeout; } + void set_supported_modes(const std::set &modes) { _traits.set_supported_modes(modes); } + void set_supported_swing_modes(const std::set &modes) { _traits.set_supported_swing_modes(modes); } + void set_supported_presets(const std::set &presets) { _traits.set_supported_presets(presets); } + void set_custom_presets(const std::set &presets) { _traits.set_supported_custom_presets(presets); } + void set_custom_fan_modes(const std::set &modes) { _traits.set_supported_custom_fan_modes(modes); } + + // ************************************************************************************************** + // setters for sensors + void set_sensor_temperature_indoor_ambient(Sensor *temperature_sensor) { _sensor_temperature_indoor_ambient = temperature_sensor; } + void set_sensor_temperature_indoor_coil(Sensor *temperature_sensor) { _sensor_temperature_indoor_coil = temperature_sensor; } + void set_sensor_temperature_outdoor_ambient(Sensor *temperature_sensor) { _sensor_temperature_outdoor_ambient = temperature_sensor; } + void set_sensor_temperature_outdoor_condenser_middle(Sensor *temperature_sensor) { _sensor_temperature_outdoor_condenser_middle = temperature_sensor; } + void set_sensor_temperature_outdoor_defrost(Sensor *temperature_sensor) { _sensor_temperature_outdoor_defrost = temperature_sensor; } + void set_sensor_temperature_outdoor_discharge(Sensor *temperature_sensor) { _sensor_temperature_outdoor_discharge = temperature_sensor; } + void set_sensor_temperature_outdoor_suction(Sensor *temperature_sensor) { _sensor_temperature_outdoor_suction = temperature_sensor; } + + void set_sensor_vlouver_state(Sensor *sensor) { _sensor_vlouver_state = sensor; } + void set_sensor_display(BinarySensor *sensor) { _sensor_display_state = sensor; } + void set_sensor_defrost_state(BinarySensor *sensor) { _sensor_defrost_state = sensor; } + void set_sensor_preset_reporter(TextSensor *sensor) { _sensor_preset_reporter = sensor; } + + void set_sensor_inverter_power(Sensor *sensor) { _sensor_inverter_power_actual = sensor; } + void set_sensor_inverter_power_limit_value(Sensor *sensor) { _sensor_inverter_power_limit_value = sensor; } + void set_sensor_inverter_power_limit_state(BinarySensor *sensor) { _sensor_inverter_power_limit_state = sensor; } + + // ************************************************************************************************** + // actions + void action_display_off(); + void action_display_on(); + void action_set_vlouver_swing(); + void action_set_vlouver_stop(); + void action_set_vlouver_top_position(); + void action_set_vlouver_middle_above_position(); + void action_set_vlouver_middle_position(); + void action_set_vlouver_middle_below_position(); + void action_set_vlouver_bottom(); + void action_set_vlouver_position(vlouver_esphome_position_t position); + void action_power_limitation_off(); + void action_power_limitation_on(uint8_t limit); + + // ************************************************************************************************** + // other methods + void schedule_frame_to_send(const Frame &frame); + void schedule_ping_response(); + void schedule_command(const ClimateCall &cmd); + Frame &get_last_frame_11(); + Frame &get_last_frame_2x(); + void set_last_frame(const Frame &frame); + void update_all_sensors_unit_of_measurement(); + void publish_all_states(); + void set_receiver_callback(std::function callback) { this->_receiver_callback = callback; } + + // converts vertical louver state from hardware codes to frontend codes + vlouver_esphome_position_t aux_vlouver_to_frontend(const ac_louver_V vLouver); + + // current vertical louver position in esphome codes + vlouver_esphome_position_t get_current_vlouver_frontend_state(); + + // converts vertical louver position from frontend codes to hardware code + ac_louver_V frontend_vlouver_to_aux(const vlouver_esphome_position_t vLouver); + }; + + } // namespace aux_airconditioner +} // namespace esphome diff --git a/components/aux_ac/aircon_common.cpp b/components/aux_ac/aircon_common.cpp new file mode 100644 index 0000000..2515f71 --- /dev/null +++ b/components/aux_ac/aircon_common.cpp @@ -0,0 +1,275 @@ +#include "aircon_common.h" + +namespace esphome +{ + namespace aux_airconditioner + { + std::string ac_mode_to_string(ac_mode mode) + { + switch (mode) + { + case AC_MODE_AUTO: + return "AC_MODE_AUTO"; + + case AC_MODE_COOL: + return "AC_MODE_COOL"; + + case AC_MODE_DRY: + return "AC_MODE_DRY"; + + case AC_MODE_HEAT: + return "AC_MODE_HEAT"; + + case AC_MODE_FAN: + return "AC_MODE_FAN"; + + default: + return "mode unknown"; + } + } + + ClimateMode ac_mode_to_climate_mode(ac_mode mode) + { + switch (mode) + { + case AC_MODE_AUTO: + return ClimateMode::CLIMATE_MODE_HEAT_COOL; + + case AC_MODE_COOL: + return ClimateMode::CLIMATE_MODE_COOL; + + case AC_MODE_DRY: + return ClimateMode::CLIMATE_MODE_DRY; + + case AC_MODE_HEAT: + return ClimateMode::CLIMATE_MODE_HEAT; + + case AC_MODE_FAN: + return ClimateMode::CLIMATE_MODE_FAN_ONLY; + + default: + return ClimateMode::CLIMATE_MODE_OFF; + } + } + + ac_mode climate_mode_to_ac_mode(ClimateMode mode) + { + switch (mode) + { + case ClimateMode::CLIMATE_MODE_HEAT_COOL: + return AC_MODE_AUTO; + + case ClimateMode::CLIMATE_MODE_COOL: + return AC_MODE_COOL; + + case ClimateMode::CLIMATE_MODE_DRY: + return AC_MODE_DRY; + + case ClimateMode::CLIMATE_MODE_HEAT: + return AC_MODE_HEAT; + + case ClimateMode::CLIMATE_MODE_FAN_ONLY: + return AC_MODE_FAN; + + default: + return AC_MODE_FAN; + } + } + + ac_louver_V vlouver_frontend_to_ac_louver_V(const vlouver_esphome_position_t vlouver_frontend) + { + switch (vlouver_frontend) + { + case AC_VLOUVER_FRONTEND_SWING: + return ac_louver_V::AC_LOUVERV_SWING_UPDOWN; + + case AC_VLOUVER_FRONTEND_STOP: + return ac_louver_V::AC_LOUVERV_OFF; + + case AC_VLOUVER_FRONTEND_TOP: + return ac_louver_V::AC_LOUVERV_SWING_UPDOWN; + + case AC_VLOUVER_FRONTEND_MIDDLE_ABOVE: + return ac_louver_V::AC_LOUVERV_MIDDLE_ABOVE; + + case AC_VLOUVER_FRONTEND_MIDDLE: + return ac_louver_V::AC_LOUVERV_MIDDLE; + + case AC_VLOUVER_FRONTEND_MIDDLE_BELOW: + return ac_louver_V::AC_LOUVERV_MIDDLE_BELOW; + + case AC_VLOUVER_FRONTEND_BOTTOM: + return ac_louver_V::AC_LOUVERV_BOTTOM; + + default: + return ac_louver_V::AC_LOUVERV_OFF; + } + } + + vlouver_esphome_position_t ac_louver_V_to_vlouver_frontend(const ac_louver_V aux_vlouver) + { + switch (aux_vlouver) + { + case AC_LOUVERV_SWING_UPDOWN: + return AC_VLOUVER_FRONTEND_SWING; + + case AC_LOUVERV_OFF: + return AC_VLOUVER_FRONTEND_STOP; + + case AC_LOUVERV_TOP: + return AC_VLOUVER_FRONTEND_TOP; + + case AC_LOUVERV_MIDDLE_ABOVE: + return AC_VLOUVER_FRONTEND_MIDDLE_ABOVE; + + case AC_LOUVERV_MIDDLE: + return AC_VLOUVER_FRONTEND_MIDDLE; + + case AC_LOUVERV_MIDDLE_BELOW: + return AC_VLOUVER_FRONTEND_MIDDLE_BELOW; + + case AC_LOUVERV_BOTTOM: + return AC_VLOUVER_FRONTEND_BOTTOM; + + default: + return AC_VLOUVER_FRONTEND_STOP; + } + } + + std::string ac_louver_V_to_string(ac_louver_V louver) + { + switch (louver) + { + case AC_LOUVERV_SWING_UPDOWN: + return "AC_LOUVERV_SWING_UPDOWN"; + + case AC_LOUVERV_TOP: + return "AC_LOUVERV_TOP"; + + case AC_LOUVERV_MIDDLE_ABOVE: + return "AC_LOUVERV_MIDDLE_ABOVE"; + + case AC_LOUVERV_MIDDLE: + return "AC_LOUVERV_MIDDLE"; + + case AC_LOUVERV_MIDDLE_BELOW: + return "AC_LOUVERV_MIDDLE_BELOW"; + + case AC_LOUVERV_BOTTOM: + return "AC_LOUVERV_BOTTOM"; + + case AC_LOUVERV_OFF: + return "AC_LOUVERV_OFF"; + + default: + return "unknown vertical louver position"; + } + } + + std::string ac_louver_H_to_string(ac_louver_H louver) + { + switch (louver) + { + case AC_LOUVERH_SWING_LEFTRIGHT: + return "AC_LOUVERH_SWING_LEFTRIGHT"; + + case AC_LOUVERH_OFF: + return "AC_LOUVERH_OFF"; + + default: + return "unknown horizontal louver position"; + } + } + + std::string ac_fanspeed_to_string(ac_fanspeed fanspeed) + { + switch (fanspeed) + { + case AC_FANSPEED_HIGH: + return "AC_FANSPEED_HIGH"; + + case AC_FANSPEED_MEDIUM: + return "AC_FANSPEED_MEDIUM"; + + case AC_FANSPEED_LOW: + return "AC_FANSPEED_LOW"; + + case AC_FANSPEED_AUTO: + return "AC_FANSPEED_AUTO"; + + default: + return "unknown"; + } + } + + ClimateFanMode ac_fanspeed_to_climate_fan_mode(ac_fanspeed fanspeed) + { + switch (fanspeed) + { + case AC_FANSPEED_HIGH: + return ClimateFanMode::CLIMATE_FAN_HIGH; + + case AC_FANSPEED_MEDIUM: + return ClimateFanMode::CLIMATE_FAN_MEDIUM; + + case AC_FANSPEED_LOW: + return ClimateFanMode::CLIMATE_FAN_LOW; + + case AC_FANSPEED_AUTO: + return ClimateFanMode::CLIMATE_FAN_AUTO; + + default: + return ClimateFanMode::CLIMATE_FAN_LOW; + } + } + + ac_fanspeed climate_fan_mode_to_ac_fanspeed(ClimateFanMode fanmode) + { + switch (fanmode) + { + case ClimateFanMode::CLIMATE_FAN_AUTO: + return AC_FANSPEED_AUTO; + + case ClimateFanMode::CLIMATE_FAN_LOW: + return AC_FANSPEED_LOW; + + case ClimateFanMode::CLIMATE_FAN_MEDIUM: + return AC_FANSPEED_MEDIUM; + + case ClimateFanMode::CLIMATE_FAN_HIGH: + return AC_FANSPEED_HIGH; + + default: + return ac_fanspeed::AC_FANSPEED_LOW; + } + } + + std::string ac_fanspeed_real_to_string(ac_fanspeed_real real_fanspeed) + { + switch (real_fanspeed) + { + case AC_REAL_FAN_OFF: + return "AC_REAL_FAN_OFF"; + + case AC_REAL_FAN_MUTE: + return "AC_REAL_FAN_MUTE"; + + case AC_REAL_FAN_LOW: + return "AC_REAL_FAN_LOW"; + + case AC_REAL_FAN_MID: + return "AC_REAL_FAN_MID"; + + case AC_REAL_FAN_HIGH: + return "AC_REAL_FAN_HIGH"; + + case AC_REAL_FAN_TURBO: + return "AC_REAL_FAN_TURBO"; + + default: + return "unknown"; + } + } + + } // namespace aux_airconditioner +} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/aircon_common.h b/components/aux_ac/aircon_common.h new file mode 100644 index 0000000..22f47da --- /dev/null +++ b/components/aux_ac/aircon_common.h @@ -0,0 +1,107 @@ +#pragma once + +#include "esphome/components/climate/climate.h" +#include "esphome/core/optional.h" + +namespace esphome +{ + namespace aux_airconditioner + { + 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 + }; + + std::string ac_mode_to_string(ac_mode mode); + + using esphome::climate::ClimateMode; + ClimateMode ac_mode_to_climate_mode(ac_mode mode); + ac_mode climate_mode_to_ac_mode(ClimateMode mode); + + // vertical louvers position in esphome / HA frontend + enum vlouver_esphome_position_t : uint8_t + { + AC_VLOUVER_FRONTEND_SWING = 0x00, + AC_VLOUVER_FRONTEND_STOP = 0x01, + AC_VLOUVER_FRONTEND_TOP = 0x02, + AC_VLOUVER_FRONTEND_MIDDLE_ABOVE = 0x03, + AC_VLOUVER_FRONTEND_MIDDLE = 0x04, + AC_VLOUVER_FRONTEND_MIDDLE_BELOW = 0x05, + AC_VLOUVER_FRONTEND_BOTTOM = 0x06, + }; + + enum ac_louver_V : uint8_t + { + AC_LOUVERV_SWING_UPDOWN = 0x00, + AC_LOUVERV_TOP = 0x01, + AC_LOUVERV_MIDDLE_ABOVE = 0x02, + AC_LOUVERV_MIDDLE = 0x03, + AC_LOUVERV_MIDDLE_BELOW = 0x04, + AC_LOUVERV_BOTTOM = 0x05, + // 0x06 tested and doing nothing + AC_LOUVERV_OFF = 0x07 + }; + + ac_louver_V vlouver_frontend_to_ac_louver_V(const vlouver_esphome_position_t vlouver_frontend); + vlouver_esphome_position_t ac_louver_V_to_vlouver_frontend(const ac_louver_V aux_vlouver); + + std::string ac_louver_V_to_string(ac_louver_V louver); + + enum ac_louver_H : uint8_t + { + AC_LOUVERH_SWING_LEFTRIGHT = 0x00, + // AC_LOUVERH_OFF_AUX = 0x20, // 0b00100000 + AC_LOUVERH_OFF = 0xE0 // 0b11100000 + }; + + std::string ac_louver_H_to_string(ac_louver_H louver); + + enum ac_fanspeed : uint8_t + { + AC_FANSPEED_HIGH = 0x20, + AC_FANSPEED_MEDIUM = 0x40, + AC_FANSPEED_LOW = 0x60, + AC_FANSPEED_AUTO = 0xA0 + }; + + std::string ac_fanspeed_to_string(ac_fanspeed fanspeed); + + using esphome::climate::ClimateFanMode; + ClimateFanMode ac_fanspeed_to_climate_fan_mode(ac_fanspeed fanspeed); + ac_fanspeed climate_fan_mode_to_ac_fanspeed(ClimateFanMode fanmode); + + enum ac_fanspeed_real : 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 + }; + + std::string ac_fanspeed_real_to_string(ac_fanspeed_real real_fanspeed); + + enum command_type_t : uint8_t + { + COMMAND_TYPE_NONE = 0x00, + COMMAND_TYPE_REQUEST_11 = 0x01, + COMMAND_TYPE_REQUEST_21 = 0x02, + COMMAND_TYPE_SET_STATE = 0x03, + }; + + enum command_processor_state_t : uint8_t + { + CMD_PROCESSOR_STATE_NOT_STARTED = 0x00, + CMD_PROCESSOR_STATE_WAITING_FOR_F11 = 0x01, + CMD_PROCESSOR_STATE_PRECHECK_DONE = 0x02, + CMD_PROCESSOR_STATE_CMD_WAS_SENT = 0x03, + CMD_PROCESSOR_STATE_POSTCHECK_DONE = 0x04, + }; + + } // namespace aux_airconditioner +} // namespace GrKoR \ No newline at end of file diff --git a/components/aux_ac/automation.h b/components/aux_ac/automation.h index 50129e7..694097c 100644 --- a/components/aux_ac/automation.h +++ b/components/aux_ac/automation.h @@ -1,186 +1,169 @@ #pragma once -#include "aux_ac.h" +#include "aircon.h" #include "esphome/core/automation.h" #include "esphome/core/component.h" -namespace esphome { -namespace aux_ac { +namespace esphome +{ + namespace aux_airconditioner + { + class AirCon; -// **************************************** DISPLAY ACTIONS **************************************** -template -class AirConDisplayOffAction : public Action { - public: - explicit AirConDisplayOffAction(AirCon *ac) : ac_(ac) {} + // **************************************** DISPLAY ACTIONS **************************************** + template + class AirConDisplayOffAction : public Action + { + public: + explicit AirConDisplayOffAction(AirCon *ac) : ac_(ac) {} - void play(Ts... x) override { this->ac_->displayOffSequence(); } + void play(Ts... x) override { this->ac_->action_display_off(); } - protected: - AirCon *ac_; -}; + protected: + AirCon *ac_; + }; -template -class AirConDisplayOnAction : public Action { - public: - explicit AirConDisplayOnAction(AirCon *ac) : ac_(ac) {} + template + class AirConDisplayOnAction : public Action + { + public: + explicit AirConDisplayOnAction(AirCon *ac) : ac_(ac) {} - void play(Ts... x) override { this->ac_->displayOnSequence(); } + void play(Ts... x) override { this->ac_->action_display_on(); } - protected: - AirCon *ac_; -}; + protected: + AirCon *ac_; + }; -// **************************************** VERTICAL LOUVER ACTIONS **************************************** -template -class AirConVLouverSwingAction : public Action { - public: - explicit AirConVLouverSwingAction(AirCon *ac) : ac_(ac) {} + // **************************************** VERTICAL LOUVER ACTIONS **************************************** + template + class AirConVLouverSwingAction : public Action + { + public: + explicit AirConVLouverSwingAction(AirCon *ac) : ac_(ac) {} - void play(Ts... x) override { this->ac_->setVLouverSwingSequence(); } + void play(Ts... x) override { this->ac_->action_set_vlouver_swing(); } - protected: - AirCon *ac_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverStopAction : public Action { - public: - explicit AirConVLouverStopAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverStopAction : public Action + { + public: + explicit AirConVLouverStopAction(AirCon *ac) : ac_(ac) {} - void play(Ts... x) override { this->ac_->setVLouverStopSequence(); } + void play(Ts... x) override { this->ac_->action_set_vlouver_stop(); } - protected: - AirCon *ac_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverTopAction : public Action { - public: - explicit AirConVLouverTopAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverTopAction : public Action + { + public: + explicit AirConVLouverTopAction(AirCon *ac) : ac_(ac) {} - void play(Ts... x) override { this->ac_->setVLouverTopSequence(); } + void play(Ts... x) override { this->ac_->action_set_vlouver_top_position(); } - protected: - AirCon *ac_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverMiddleAboveAction : public Action { - public: - explicit AirConVLouverMiddleAboveAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverMiddleAboveAction : public Action + { + public: + explicit AirConVLouverMiddleAboveAction(AirCon *ac) : ac_(ac) {} - void play(Ts... x) override { this->ac_->setVLouverMiddleAboveSequence(); } + void play(Ts... x) override { this->ac_->action_set_vlouver_middle_above_position(); } - protected: - AirCon *ac_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverMiddleAction : public Action { - public: - explicit AirConVLouverMiddleAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverMiddleAction : public Action + { + public: + explicit AirConVLouverMiddleAction(AirCon *ac) : ac_(ac) {} - void play(Ts... x) override { this->ac_->setVLouverMiddleSequence(); } + void play(Ts... x) override { this->ac_->action_set_vlouver_middle_position(); } - protected: - AirCon *ac_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverMiddleBelowAction : public Action { - public: - explicit AirConVLouverMiddleBelowAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverMiddleBelowAction : public Action + { + public: + explicit AirConVLouverMiddleBelowAction(AirCon *ac) : ac_(ac) {} - void play(Ts... x) override { this->ac_->setVLouverMiddleBelowSequence(); } + void play(Ts... x) override { this->ac_->action_set_vlouver_middle_below_position(); } - protected: - AirCon *ac_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverBottomAction : public Action { - public: - explicit AirConVLouverBottomAction(AirCon *ac) : ac_(ac) {} + template + class AirConVLouverBottomAction : public Action + { + public: + explicit AirConVLouverBottomAction(AirCon *ac) : ac_(ac) {} - void play(Ts... x) override { this->ac_->setVLouverBottomSequence(); } + void play(Ts... x) override { this->ac_->action_set_vlouver_bottom(); } - protected: - AirCon *ac_; -}; + protected: + AirCon *ac_; + }; -template -class AirConVLouverSetAction : public Action { - public: - AirConVLouverSetAction(AirCon *ac) : ac_(ac) {} - TEMPLATABLE_VALUE(uint8_t, value); + template + class AirConVLouverSetAction : public Action + { + public: + AirConVLouverSetAction(AirCon *ac) : ac_(ac) {} + TEMPLATABLE_VALUE(uint8_t, value); - void play(Ts... x) { - vlpos_ = this->value_.value(x...); - this->ac_->setVLouverFrontendSequence((ac_vlouver_frontend)vlpos_); - } + void play(Ts... x) + { + this->ac_->action_set_vlouver_position((vlouver_esphome_position_t)this->value_.value(x...)); + } - protected: - AirCon *ac_; - uint8_t vlpos_; -}; + protected: + AirCon *ac_; + }; -// **************************************** SEND TEST PACKET ACTION **************************************** -template -class AirConSendTestPacketAction : public Action { - public: - explicit AirConSendTestPacketAction(AirCon *ac) : ac_(ac) {} - void set_data_template(std::function(Ts...)> func) { - this->data_func_ = func; - this->static_ = false; - } - void set_data_static(const std::vector &data) { - this->data_static_ = data; - this->static_ = true; - } + // **************************************** POWER LIMITATION ACTIONS **************************************** + template + class AirConPowerLimitationOffAction : public Action + { + public: + explicit AirConPowerLimitationOffAction(AirCon *ac) : ac_(ac) {} - void play(Ts... x) override { - if (this->static_) { - this->ac_->sendTestPacket(this->data_static_); - } else { - auto val = this->data_func_(x...); - this->ac_->sendTestPacket(val); - } - } + void play(Ts... x) override { this->ac_->action_power_limitation_off(); } - protected: - AirCon *ac_; - bool static_{false}; - std::function(Ts...)> data_func_{}; - std::vector data_static_{}; -}; + protected: + AirCon *ac_; + }; -// **************************************** POWER LIMITATION ACTIONS **************************************** -template -class AirConPowerLimitationOffAction : public Action { - public: - explicit AirConPowerLimitationOffAction(AirCon *ac) : ac_(ac) {} + template + class AirConPowerLimitationOnAction : public Action + { + public: + AirConPowerLimitationOnAction(AirCon *ac) : ac_(ac) {} + TEMPLATABLE_VALUE(uint8_t, value); - void play(Ts... x) override { this->ac_->powerLimitationOffSequence(); } + void play(Ts... x) + { + this->ac_->action_power_limitation_on(this->value_.value(x...)); + } - protected: - AirCon *ac_; -}; + protected: + AirCon *ac_; + }; -template -class AirConPowerLimitationOnAction : public Action { - public: - AirConPowerLimitationOnAction(AirCon *ac) : ac_(ac) {} - TEMPLATABLE_VALUE(uint8_t, value); - - void play(Ts... x) { - this->pwr_lim_ = this->value_.value(x...); - this->ac_->powerLimitationOnSequence(this->pwr_lim_); - } - - protected: - AirCon *ac_; - uint8_t pwr_lim_; -}; - -} // namespace aux_ac -} // namespace esphome \ No newline at end of file + } // namespace aux_airconditioner +} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/aux_ac.h b/components/aux_ac/aux_ac.h deleted file mode 100644 index 4481fa1..0000000 --- a/components/aux_ac/aux_ac.h +++ /dev/null @@ -1,3889 +0,0 @@ -// Custom ESPHome component for AUX-based air conditioners -// Need some soldering skills -// Source code and detailed instructions are available on github: https://github.com/GrKoR/esphome_aux_ac_component -/// немного переработанная версия старого компонента -#pragma once - -#include -#include - -#include "esphome.h" -#include "esphome/components/binary_sensor/binary_sensor.h" -#include "esphome/components/climate/climate.h" -#include "esphome/components/sensor/sensor.h" -#include "esphome/components/text_sensor/text_sensor.h" -#include "esphome/components/uart/uart.h" -#include "esphome/core/component.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 - -namespace esphome -{ - namespace aux_ac - { - - static const char *const TAG = "AirCon"; - - using climate::ClimateFanMode; - using climate::ClimateMode; - using climate::ClimatePreset; - using climate::ClimateSwingMode; - using climate::ClimateTraits; - -//**************************************************************************************************************************************************** -//**************************************************** Packet logger configuration ******************************************************************* -//**************************************************************************************************************************************************** -// v.0.2.9: замена директиве HOLMS -#ifdef HOLMS -#undef HOLMS -#warning "HOLMS was deprecated in v.0.2.9. Use HOLMES_x instead (see below)." -#endif - -// Директива HOLMES_WORKS позволяет включить (true) или выключить (false) вывод пакетов в лог -// Причём отключение вывода пакетов не затронет вывод остальных данных -#define HOLMES_WORKS true - -// Директива HOLMES_BYTE_FORMAT задаёт формат вывод каждого байта пакета в лог в формате sprintf. -// Для вывода в шестнадцатиричном виде с двумя знаками, задайте "%02X". -// Для вывода в десятичном виде с тремя знаками, задайте "%03d". -#define HOLMES_BYTE_FORMAT "%02X" - -// Директива HOLMES_FILTER_LEN обеспечивает фильтрацию вывода пакетов в лог. -// Все корректные пакеты, длина тела которых короче HOLMES_FILTER_LEN, будут проигнорированы. -// Все корректные пакеты, длина тела которых HOLMES_FILTER_LEN и более, попадут в лог. -// Все данные, не являющиеся корректными пакетами, попадут в лог в любом случае. Это нужно для целей отладки. -// В протоколе встречаются пакеты с телом следующей длины: 0, 1, 2, 4, 8, 15, 23 -#define HOLMES_FILTER_LEN 0 - -// Директива HOLMES_DELIMITER позволяет задать разделитель байт при выводе в лог -// Для "классического" вывода задайте " " -// Для вывода "под Excel" задайте ";" -#define HOLMES_DELIMITER " " - -// Директивы HOLMES_x_BRACKET_OPEN и HOLMES_x_BRACKET_CLOSE задают открывающую и -// закрывающую скобки для заголовка и CRC. -// Если вместо скобок указать "", то в логе скобок не будет. -#define HOLMES_HEADER_BRACKET_OPEN "[" -#define HOLMES_HEADER_BRACKET_CLOSE "]" -#define HOLMES_CRC_BRACKET_OPEN "[" -#define HOLMES_CRC_BRACKET_CLOSE "]" - - //**************************************************************************************************************************************************** - //************************************************* Constants for ESPHome integration **************************************************************** - //**************************************************************************************************************************************************** - class Constants - { - public: - static const std::string AC_FIRMWARE_VERSION; - - static const std::string MUTE; - static const std::string TURBO; - static const std::string CLEAN; - static const std::string HEALTH; - static const std::string ANTIFUNGUS; - - /// минимальная и максимальная температура в градусах Цельсия, ограничения самого кондиционера - static const float AC_MIN_TEMPERATURE; - static const float AC_MAX_TEMPERATURE; - /// шаг изменения целевой температуры, градусы Цельсия - static const float AC_TEMPERATURE_STEP; - - /// минимальное и максимальное значение мощности инвертора при установке ограничений - static const uint8_t AC_MIN_INVERTER_POWER_LIMIT; - static const uint8_t AC_MAX_INVERTER_POWER_LIMIT; - - // периодичность опроса кондиционера на предмет изменения состояния - // изменение параметров с пульта не сообщается в UART, поэтому надо запрашивать состояние, чтобы быть в курсе - // значение в миллисекундах - static const uint32_t AC_STATES_REQUEST_INTERVAL; - - // границы допустимого диапазона таймаута загрузки пакета - // таймаут загрузки - через такое количиство миллисекунд конечный автомат перейдет из - // состояния ACSM_RECEIVING_PACKET в ACSM_IDLE, если пакет не будет загружен - static const uint32_t AC_PACKET_TIMEOUT_MAX; - static const uint32_t AC_PACKET_TIMEOUT_MIN; - }; - - const std::string Constants::AC_FIRMWARE_VERSION = "0.2.14"; - - // custom fan modes - const std::string Constants::MUTE = "mute"; - const std::string Constants::TURBO = "turbo"; - - // 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; - const uint8_t Constants::AC_MIN_INVERTER_POWER_LIMIT = 30; // 30% - const uint8_t Constants::AC_MAX_INVERTER_POWER_LIMIT = 100; // 100% - const uint32_t Constants::AC_STATES_REQUEST_INTERVAL = 7000; - // таймаут загрузки пакета - // По расчетам выходит: - // - получение и обработка посимвольно не должна длиться дольше 600 мсек. - // - получение и обработка пакетов целиком не должна длиться дольше 150 мсек. - // Мы будем обрабатывать пакетами, поэтому 150. - // Растягивать приём пакетов очередью команд нельзя, так как кондиционер иногда посылает - // информационные пакеты без запроса. Такие пакеты будут рушить последовательность команд, - // команды будут теряться. От такой коллизии мы не защищены в любом случае. Но чем меньше таймаут, - // тем меньше шансов на коллизию. - // Из этих соображений выбраны границы диапазона (_MIN и _MAX значения). - const uint32_t Constants::AC_PACKET_TIMEOUT_MAX = 600; - const uint32_t Constants::AC_PACKET_TIMEOUT_MIN = 150; - - //**************************************************************************************************************************************************** - //********************************************************* ОСНОВНЫЕ СТРУКТУРЫ *********************************************************************** - //**************************************************************************************************************************************************** - class AirCon; - - // состояния конечного автомата компонента - enum acsm_state : uint8_t - { - ACSM_IDLE = 0, // ничего не делаем, ждем, на что бы среагировать - ACSM_RECEIVING_PACKET, // находимся в процессе получения пакета, никакие отправки в этом состоянии невозможны - ACSM_PARSING_PACKET, // разбираем полученный пакет - ACSM_SENDING_PACKET, // отправляем пакет сплиту - }; - -// структура пакета описана тут: -// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_structure -#define AC_HEADER_SIZE 8 - -// стандартно длина пакета не более 34 байт -// но встретилось исключение Royal Clima (как минимум, модель CO-D xxHNI) - у них 35 байт -// поэтому буффер увеличен -#define AC_BUFFER_SIZE 35 - -// типы пакетов -// 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 // какой-то странный пакет - -// типы команд -// смотреть тут: 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 = 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; - packet_crc_t *crc; - uint8_t *body; // указатель на первый байт тела; можно приведением типов указателей обращаться к отдельным битам как к полям соответсвующей структуры - uint8_t bytesLoaded; // количество загруженных в пакет байт, включая CRC - uint8_t data[AC_BUFFER_SIZE]; - }; - - // тело ответа на пинг - // 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 = 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 - { - // байт 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_inverter_periodic : 1; // флаг периодического пакета инверторного кондиционера - uint8_t reserv23 : 2; - bool is_inverter : 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 inverter_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 - - // байт 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 - { - // байт 8 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b08 - uint8_t byte_01; - - // байт 9 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b09 - uint8_t cmd_answer; - - // байт 10 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b10 - // uint8_t target_temp_int_and_v_louver; - uint8_t v_louver : 3; - uint8_t target_temp_int : 5; - - // байт 11 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b11 - uint8_t h_louver; - - // байт 12 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b12 - // uint8_t target_temp_frac; - uint8_t ir_timer : 6; - bool reserv126 : 1; - bool target_temp_frac_bool : 1; - - // байт 13 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b13 - uint8_t fan_speed; - - // байт 14 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b14 - uint8_t fan_turbo_and_mute; - - // байт 15 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b15 - uint8_t mode; - - // байт 16 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b16 - uint8_t zero1; // всегда 0x00 - - // байт 17 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b17 - uint8_t zero2; // всегда 0x00 - - // байт 18 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b18 - uint8_t status; - - // байт 19 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b19 - uint8_t zero3; // всегда 0x00 - - // байт 20 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b20 - uint8_t display_and_mildew; - - // байт 21 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b21 - uint8_t inverter_power_limitation_value : 7; - bool inverter_power_limitation_enable : 1; - - // байт 22 пакета: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b22 - uint8_t target_temp_frac_dec; - }; - -//**************************************************************************************************************************************************** -//*************************************************** ПАРАМЕТРЫ РАБОТЫ КОНДИЦИОНЕРА ****************************************************************** -//**************************************************************************************************************************************************** -// для всех параметров ниже вариант X_UNTOUCHED = 0xFF означает, что этот параметр команды должен остаться тот, который уже установлен - -// питание кондиционера -#define AC_POWER_MASK 0b00100000 - enum ac_power : uint8_t - { - AC_POWER_OFF = 0x00, - AC_POWER_ON = 0x20, - AC_POWER_UNTOUCHED = 0xFF - }; - -// режим очистки кондиционера, включается (или должен включаться) при AC_POWER_OFF -#define AC_CLEAN_MASK 0b00000100 - enum ac_clean : uint8_t - { - AC_CLEAN_OFF = 0x00, - AC_CLEAN_ON = 0x04, - AC_CLEAN_UNTOUCHED = 0xFF - }; - -// для включения ионизатора нужно установить второй бит в байте -// по результату этот бит останется установленным, но кондиционер еще и установит первый бит -#define AC_HEALTH_MASK 0b00000010 - enum ac_health : uint8_t - { - AC_HEALTH_OFF = 0x00, - AC_HEALTH_ON = 0x02, - AC_HEALTH_UNTOUCHED = 0xFF - }; - -// Статус ионизатора. Если бит поднят, то обнаружена ошибка ключения ионизатора -#define AC_HEALTH_STATUS_MASK 0b00000001 - enum ac_health_status : uint8_t - { - AC_HEALTH_STATUS_OFF = 0x00, - AC_HEALTH_STATUS_ON = 0x01, - AC_HEALTH_STATUS_UNTOUCHED = 0xFF - }; - -// целевая температура -#define AC_TEMP_TARGET_INT_PART_MASK 0b11111000 -#define AC_TEMP_TARGET_FRAC_PART_MASK 0b10000000 - -// задержка отключения кондиционера -#define AC_TIMER_MINUTES_MASK 0b00111111 -#define AC_TIMER_HOURS_MASK 0b00011111 - -// Temperature unit of measurement -// Air conditioner works with Celsius but can convert it to Fahrenheit for display on the LED screen. -#define AC_TEMPERATURE_UNIT_MASK 0b00000010 - enum ac_temperature_unit : uint8_t - { - AC_TEMPERATURE_UNIT_CELSIUS = 0x00, - AC_TEMPERATURE_UNIT_FAHRENHEIT = 0x02, - AC_TEMPERATURE_UNIT_UNTOUCHED = 0xFF - }; - -// включение таймера сна -#define AC_TIMER_MASK 0b01000000 - enum ac_timer : uint8_t - { - AC_TIMER_OFF = 0x00, - AC_TIMER_ON = 0x40, - AC_TIMER_UNTOUCHED = 0xFF - }; - -// основные режимы работы кондиционера -#define AC_MODE_MASK 0b11100000 - enum ac_mode : uint8_t - { - AC_MODE_AUTO = 0x00, - AC_MODE_COOL = 0x20, - AC_MODE_DRY = 0x40, - AC_MODE_HEAT = 0x80, - AC_MODE_FAN = 0xC0, - AC_MODE_UNTOUCHED = 0xFF - }; - -// Ночной режим (SLEEP). Комбинируется только с режимами COOL и HEAT. Автоматически выключается через 7 часов. -// COOL: температура +1 градус через час, еще через час дополнительные +1 градус, дальше не меняется. -// HEAT: температура -2 градуса через час, еще через час дополнительные -2 градуса, дальше не меняется. -// Восстанавливается ли температура через 7 часов при отключении режима - не понятно. -#define AC_SLEEP_MASK 0b00000100 - enum ac_sleep : uint8_t - { - AC_SLEEP_OFF = 0x00, - AC_SLEEP_ON = 0x04, - AC_SLEEP_UNTOUCHED = 0xFF - }; - -// Вертикальные жалюзи. В протоколе зашита возможность двигать ими по всякому, но должна быть такая возможность на уровне железа. -#define AC_LOUVERV_MASK 0b00000111 - 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 - }; - -// Горизонтальные жалюзи. В протоколе зашита возможность двигать ими по всякому, но должна быть такая возможность на уровне железа. -// горизонтальные жалюзи выставлять в определенное положение не вышло, протестировано. -#define AC_LOUVERH_MASK 0b11100000 - enum ac_louver_H : uint8_t - { - AC_LOUVERH_SWING_LEFTRIGHT = 0x00, - AC_LOUVERH_OFF_AUX = 0x20, // 0b00100000 - AC_LOUVERH_OFF_ALTERNATIVE = 0xE0, // 0b11100000 - по коду везде кроме проверок использую его, так как у него все три бита в 1 - AC_LOUVERH_UNTOUCHED = 0xFF - }; - - struct ac_louver - { - ac_louver_H louver_h; - ac_louver_V louver_v; - }; - -// скорость вентилятора -#define AC_FANSPEED_MASK 0b11100000 - enum ac_fanspeed : uint8_t - { - AC_FANSPEED_HIGH = 0x20, - AC_FANSPEED_MEDIUM = 0x40, - AC_FANSPEED_LOW = 0x60, - AC_FANSPEED_AUTO = 0xA0, - AC_FANSPEED_UNTOUCHED = 0xFF - }; - -// TURBO работает только в режимах COOL и HEAT -#define AC_FANTURBO_MASK 0b01000000 - enum ac_fanturbo : uint8_t - { - AC_FANTURBO_OFF = 0x00, - AC_FANTURBO_ON = 0x40, - AC_FANTURBO_UNTOUCHED = 0xFF - }; - -// MUTE работает только в режиме FAN. В режиме COOL кондей команду принимает, но MUTE не устанавливается -#define AC_FANMUTE_MASK 0b10000000 - enum ac_fanmute : uint8_t - { - AC_FANMUTE_OFF = 0x00, - AC_FANMUTE_ON = 0x80, - AC_FANMUTE_UNTOUCHED = 0xFF - }; - - // реальная скорость вентилятора - 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 - }; - -// включение-выключение функции "Антиплесень". -// По факту: после выключения сплита он оставляет минут на 5 открытые жалюзи и глушит вентилятор. Уличный блок при этом гудит и тарахтит. -// Возможно, прогревается теплообменник для высыхания. Через некоторое время внешний блок замолкает и сплит закрывает жалюзи. -#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 - -// включение-выключение функции "Ограничение мощности". -#define AC_POWLIMSTAT_MASK 0b10000000 - enum ac_powLim_state : uint8_t - { - AC_POWLIMSTAT_OFF = 0x00, - AC_POWLIMSTAT_ON = 0x80, - AC_POWLIMSTAT_UNTOUCHED = 0xFF - }; - -// маски ограничения мощности для инверторного кондиционера -#define AC_POWLIMVAL_MASK 0b01111111 -#define AC_POWLIMVAL_UNTOUCHED 0xFF - - // положение вертикальных жалюзи для фронтенда - enum ac_vlouver_frontend : uint8_t - { - AC_VLOUVER_FRONTEND_SWING = 0x00, - AC_VLOUVER_FRONTEND_STOP = 0x01, - AC_VLOUVER_FRONTEND_TOP = 0x02, - AC_VLOUVER_FRONTEND_MIDDLE_ABOVE = 0x03, - AC_VLOUVER_FRONTEND_MIDDLE = 0x04, - AC_VLOUVER_FRONTEND_MIDDLE_BELOW = 0x05, - AC_VLOUVER_FRONTEND_BOTTOM = 0x06, - }; - -/** команда для кондиционера - * - * ВАЖНО! В коде используется копирование команд простым присваиванием. - * Если в структуру будут введены указатели, то копирование надо будет изменить! - */ - -//***************************************************************************** -// структура для сохранения настроек, специально вынесено в макрос, чтобы использовать в нескольких местах -// сделано Brokly для того, чтобы поведение wifi-модуля походило на ИК-пульт (для каждого режима сохранялись свои настройки температуры и прочего) -#define AC_COMMAND_BASE \ - float temp_target; \ - ac_power power; \ - ac_clean clean; \ - ac_health health; \ - ac_mode mode; \ - ac_temperature_unit t_unit; \ - 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_COMMAND_BASE; - ac_health_status health_status; - float temp_ambient; // внутренняя температура - int8_t temp_outdoor; // внешняя температура - int8_t temp_inbound; // температура входящая - int8_t temp_outbound; // температура исходящая - int8_t temp_compressor; // температура компрессора - ac_realFan realFanSpeed; // текущая скорость вентилятора - uint8_t inverter_power; // мощность инвертора - bool defrost; // режим разморозки внешнего блока (накопление тепла + прогрев испарителя) - ac_powLim_state power_lim_state; // статус ограничения мощности инвертора - uint8_t power_lim_value; // значение ограничения мощности инвертора - }; - - typedef ac_command_t ac_state_t; // текущее состояние параметров кондея можно хранить в таком же формате, как и комманды - - // Структура для хранения последних полученных от сплита информационных пакетов в сыром виде - // Нужно до тех пор, пока весь функционал не разберем в структуру статуса. - // Используем для проверки реакции сплита на команды (так отлавливаем разные версии протокола общения wifi-модуля с кондиционером) - // Каждый пакет имеет поле msec. Если оно равно нулю, значит пакеты еще не принимались. По этому же полю можно смотреть, как давно - // принималась информация от кондиционера, делать вывод об отвале и рапортовать об ошибке. - struct ac_last_raw_data - { - packet_t last_small_info_packet; - packet_t last_big_info_packet; - }; - -//**************************************************************************************************************************************************** -//************************************************ КОНЕЦ ПАРАМЕТРОВ РАБОТЫ КОНДИЦИОНЕРА ************************************************************** -//**************************************************************************************************************************************************** - -/***************************************************************************************************************************************************** - * структуры и типы для последовательности команд - ***************************************************************************************************************************************************** - * - * Последовательность команд позволяет выполнить несколько последовательных команд с контролем получаемых в ответ пакетов. - * Если требуется, в получаемых в ответ пакетах можно контролировать значение любых байт. - * Для входящего пакета байт, значение которого не проверяется, должен быть установлен в AC_SEQUENCE_ANY_BYTE. - * Контроль возможен только для входящих пакетов, исходящие отправляются "как есть". - * - * Для исходящих пакетов значения CRC могут не рассчитываться, контрольная сумма будет рассчитана автоматически. - * Для входящих пакетов значение CRC также можно не рассчитывать, установив байты CRC в AC_SEQUENCE_ANY_BYTE, - * так как контроль CRC для получаемых пакетов выполняется автоматически при получении. - * - * Для входящих пакетов в последовательности можно указать таймаут. Если таймаут равен 0, то используется значение AC_SEQUENCE_DEFAULT_TIMEOUT. - * Если в течение указанного времени подходящий пакет не будет получен, то последовательность прерывается с ошибкой. - * Пинг-пакеты в последовательности игнорируются. - * - * Пауза в последовательности задается значением timeout элемента AC_DELAY. Никакие другие параметры такого элемента можно не заполнять. - * - **/ -// максимальная длина последовательности; больше вроде бы не требовалось -#define AC_SEQUENCE_MAX_LEN 0x0F - -// дефолтный таймаут входящего пакета в миллисекундах -// если для входящего пакета в последовательности указан таймаут 0, то используется значение по-умолчанию -// если нужный пакет не поступил в течение указанного времени, то последовательность прерывается с ошибкой -#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 // рабочий элемент последовательности - }; - - // тип пакета в массиве последовательности - // информирует о том, что за пакет лежит в поле packet элемента последовательности - enum sequence_packet_type_t : uint8_t - { - AC_SPT_CLEAR = 0x00, // пустой пакет - AC_SPT_RECEIVED_PACKET = 0x01, // полученный пакет - AC_SPT_SENT_PACKET = 0x02 // отправленный пакет - }; - - /** элемент последовательности - * Поля item_type, func, timeout и cmd устанавливаются ручками и задают параметры выполнения шага последовательности. - * Поля msec, packet_type и packet заполняются движком при обработке последовательности. - **/ - struct sequence_item_t - { - sequence_item_type_t item_type; // тип элемента последовательности - bool (AirCon::*func)(); // указатель на функцию, отрабатывающую шаг последовательности - uint16_t timeout; // допустимый таймаут в ожидании пакета (применим только для входящих пакетов) - ac_command_t cmd; // новое состояние сплита, нужно для передачи кондиционеру команд - //******* поля ниже заполняются функциями обработки последовательности *********** - uint32_t msec; // время старта текущего шага последовательности (для входящего пакета и паузы) - sequence_packet_type_t packet_type; // тип пакета (входящий, исходящий или вовсе не пакет) - packet_t packet; // данные пакета - }; - /*****************************************************************************************************************************************************/ - - class AirCon : public esphome::Component, public esphome::climate::Climate - { - private: -#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 - uint32_t _update_period = Constants::AC_STATES_REQUEST_INTERVAL; - - // надо ли отображать текущий режим работы внешнего блока - // в режиме нагрева, например, кондиционер может как греть воздух, так и работать в режиме вентилятора, если целевая темпреатура достигнута - // по дефолту показываем - bool _show_action = true; - - // как отрабатывается включание-выключение дисплея. - // если тут false, то 1 в соответствующем бите включает дисплей, а 0 выключает. - // если тут true, то 1 потушит дисплей, а 0 включит. - bool _display_inverted = false; - - // in optimistic mode, the entity states are updated immediately after receiving a command - // from Home Assistant/ESPHome - bool _optimistic = true; - - // флаг типа кондиционера. инвертор - true, ON/OFF - false, начальная установка false - // в таком режиме точность и скорость определения реального состояния системы для инвертора, - // будет работать, но будет ниже, переменная устанавливается при первом получении большого пакета; - // если эта переменная установлена, то режим работы не инверторного кондиционера будет распознаваться - // как "в простое" (IDLE) - bool _is_inverter = 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; - - // текущее состояние задаваемых пользователем параметров системы - ac_state_t _current_ac_state; - - // флаг подключения к UART - bool _hw_initialized = false; - // указатель на UART, по которому общаемся с кондиционером - esphome::uart::UARTComponent *_ac_serial; - - // UART wrappers: peek - int peek() - { - uint8_t data; - if (!_ac_serial->peek_byte(&data)) - return -1; - return data; - } - - // UART wrappers: read - int read() - { - uint8_t data; - if (!_ac_serial->read_byte(&data)) - return -1; - return data; - } - - // флаг обмена пакетами с кондиционером (если проходят пинги, значит есть коннект) - bool _has_connection = false; - - // входящий и исходящий пакеты - packet_t _inPacket; - packet_t _outPacket; - - // пакет для тестирования всякой фигни - packet_t _outTestPacket; - - // таймаут загрузки пакета, по дефолту минимальный - uint32_t _packet_timeout = Constants::AC_PACKET_TIMEOUT_MIN; - - // сырые данные последних полученных большого и маленького информационных пакетов - ac_last_raw_data _last_raw_data; - - // нормализация показаний температуры, приведение в диапазон - 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; - } - - // нормализация лимита ограничения мощности инвертора, приведение в диапазон - uint8_t _power_limitation_value_normalise(uint8_t power_limitation_value) - { - if (power_limitation_value < Constants::AC_MIN_INVERTER_POWER_LIMIT) - power_limitation_value = Constants::AC_MIN_INVERTER_POWER_LIMIT; - if (power_limitation_value > Constants::AC_MAX_INVERTER_POWER_LIMIT) - power_limitation_value = Constants::AC_MAX_INVERTER_POWER_LIMIT; - return power_limitation_value; - } - - // последовательность пакетов текущий шаг в последовательности - sequence_item_t _sequence[AC_SEQUENCE_MAX_LEN]; - uint8_t _sequence_current_step; - - // флаг успешного выполнения стартовой последовательности команд - bool _startupSequenceComplete = false; - - // очистка последовательности команд - void _clearSequence() - { - for (uint8_t i = 0; i < AC_SEQUENCE_MAX_LEN; i++) - { - _sequence[i].item_type = AC_SIT_NONE; - _sequence[i].func = nullptr; - _sequence[i].timeout = 0; - _sequence[i].msec = 0; - _sequence[i].packet_type = AC_SPT_CLEAR; - _clearPacket(&_sequence[i].packet); - _clearCommand(&_sequence[i].cmd); - } - _sequence_current_step = 0; - } - - // проверяет, есть ли свободные шаги в последовательности команд - bool _hasFreeSequenceStep() - { - return (_getNextFreeSequenceStep() < AC_SEQUENCE_MAX_LEN); - } - - // возвращает индекс первого пустого шага последовательности команд - uint8_t _getNextFreeSequenceStep() - { - for (size_t i = 0; i < AC_SEQUENCE_MAX_LEN; i++) - { - if (_sequence[i].item_type == AC_SIT_NONE) - { - return i; - } - } - // если свободных слотов нет, то возвращаем значение за пределом диапазона - return AC_SEQUENCE_MAX_LEN; - } - - // возвращает количество свободных шагов в последовательности - uint8_t _getFreeSequenceSpace() - { - return (AC_SEQUENCE_MAX_LEN - _getNextFreeSequenceStep()); - } - - // добавляет шаг в последовательность команд - // возвращает false, если не нашлось места для шага - bool _addSequenceStep(const sequence_item_type_t item_type, bool (AirCon::*func)() = nullptr, ac_command_t *cmd = nullptr, uint16_t timeout = AC_SEQUENCE_DEFAULT_TIMEOUT) - { - if (!_hasFreeSequenceStep()) - return false; // если места нет, то уходим - if (item_type == AC_SIT_NONE) - return false; // глупость какая-то, уходим - if ((item_type == AC_SIT_FUNC) && (func == nullptr)) - return false; // должна быть передана функция для такого типа шага - if ((item_type != AC_SIT_DELAY) && (item_type != AC_SIT_FUNC)) - { - // какой-то неизвестный тип - _debugMsg(F("_addSequenceStep: unknown sequence item type = %u"), ESPHOME_LOG_LEVEL_DEBUG, __LINE__, item_type); - return false; - } - - uint8_t step = _getNextFreeSequenceStep(); - - _sequence[step].item_type = item_type; - - // если задержка нулевая, то присваиваем дефолтную задержку - if (timeout == 0) - timeout = AC_SEQUENCE_DEFAULT_TIMEOUT; - _sequence[step].timeout = timeout; - - _sequence[step].func = func; - if (cmd != nullptr) - _sequence[step].cmd = *cmd; // так как в структуре команды только простые типы, то можно вот так присваивать - - return true; - } - - // добавляет в последовательность шаг с задержкой - bool _addSequenceDelayStep(uint16_t timeout) - { - return this->_addSequenceStep(AC_SIT_DELAY, nullptr, nullptr, timeout); - } - - // добавляет в последовательность функциональный шаг - bool _addSequenceFuncStep(bool (AirCon::*func)(), ac_command_t *cmd = nullptr, uint16_t timeout = AC_SEQUENCE_DEFAULT_TIMEOUT) - { - return this->_addSequenceStep(AC_SIT_FUNC, func, cmd, timeout); - } - - // выполняет всю логику очередного шага последовательности команд - void _doSequence() - { - if (!hasSequence()) - return; - - // если шаг уже максимальный из возможных - if (_sequence_current_step >= AC_SEQUENCE_MAX_LEN) - { - // значит последовательность закончилась, надо её очистить - // при очистке последовательности будет и _sequence_current_step обнулён - _debugMsg(F("Sequence [step %u]: maximum step reached"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); - _clearSequence(); - return; - } - - // смотрим тип текущего элемента в последовательности - switch (_sequence[_sequence_current_step].item_type) - { - case AC_SIT_FUNC: - { - // если указатель на функцию пустой, то прерываем последовательность - if (_sequence[_sequence_current_step].func == nullptr) - { - _debugMsg(F("Sequence [step %u]: function pointer is NULL, sequence broken"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step); - _clearSequence(); - return; - } - - // сохраняем время начала паузы - if (_sequence[_sequence_current_step].msec == 0) - { - _sequence[_sequence_current_step].msec = millis(); - _debugMsg(F("Sequence [step %u]: step started"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); - } - - // если таймаут не указан, берем значение по-умолчанию - if (_sequence[_sequence_current_step].timeout == 0) - _sequence[_sequence_current_step].timeout = AC_SEQUENCE_DEFAULT_TIMEOUT; - - // если время вышло, то отчитываемся в лог и очищаем последовательность - if (millis() - _sequence[_sequence_current_step].msec >= _sequence[_sequence_current_step].timeout) - { - _debugMsg(F("Sequence [step %u]: step timed out (it took %u ms instead of %u ms)"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step, millis() - _sequence[_sequence_current_step].msec, _sequence[_sequence_current_step].timeout); - _clearSequence(); - return; - } - - // можно вызывать функцию - // она самомтоятельно загружает отправляемые/полученные пакеты в packet последовательности - // а также самостоятельно увеличивает счетчик шагов последовательности _sequence_current_step - // единственное исключение - таймауты - if (!(this->*_sequence[_sequence_current_step].func)()) - { - _debugMsg(F("Sequence [step %u]: error was occur in step function"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step, millis() - _sequence[_sequence_current_step].msec); - _clearSequence(); - return; - } - break; - } - - case AC_SIT_DELAY: - { // это пауза в последовательности - // пауза задается параметром timeout элемента последовательности - // начало паузы сохраняется в параметре msec - - // сохраняем время начала паузы - if (_sequence[_sequence_current_step].msec == 0) - { - _sequence[_sequence_current_step].msec = millis(); - _debugMsg(F("Sequence [step %u]: begin delay (%u ms)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step, _sequence[_sequence_current_step].timeout); - } - - // если время вышло, то переходим на следующий шаг - if (millis() - _sequence[_sequence_current_step].msec >= _sequence[_sequence_current_step].timeout) - { - _debugMsg(F("Sequence [step %u]: delay culminated (plan = %u ms, fact = %u ms)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step, _sequence[_sequence_current_step].timeout, millis() - _sequence[_sequence_current_step].msec); - _sequence_current_step++; - } - break; - } - - case AC_SIT_NONE: // шаги закончились - default: // или какой-то мусор в последовательности - // надо очистить последовательность и уходить - _debugMsg(F("Sequence [step %u]: sequence complete"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); - _clearSequence(); - break; - } - } - - // заполняет структуру команды нейтральными значениями - void _clearCommand(ac_command_t *cmd) - { - cmd->clean = AC_CLEAN_UNTOUCHED; - cmd->display = AC_DISPLAY_UNTOUCHED; - cmd->fanMute = AC_FANMUTE_UNTOUCHED; - cmd->fanSpeed = AC_FANSPEED_UNTOUCHED; - cmd->fanTurbo = AC_FANTURBO_UNTOUCHED; - cmd->health = AC_HEALTH_UNTOUCHED; - cmd->health_status = AC_HEALTH_STATUS_UNTOUCHED; - cmd->louver.louver_h = AC_LOUVERH_UNTOUCHED; - cmd->louver.louver_v = AC_LOUVERV_UNTOUCHED; - cmd->mildew = AC_MILDEW_UNTOUCHED; - cmd->mode = AC_MODE_UNTOUCHED; - cmd->t_unit = AC_TEMPERATURE_UNIT_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; - cmd->inverter_power = 0; - cmd->defrost = false; - cmd->power_lim_state = AC_POWLIMSTAT_UNTOUCHED; - cmd->power_lim_value = AC_POWLIMVAL_UNTOUCHED; - }; - - // очистка буфера размером AC_BUFFER_SIZE - void _clearBuffer(uint8_t *buf) - { - memset(buf, 0, AC_BUFFER_SIZE); - } - - // очистка структуры пакета по указателю - void _clearPacket(packet_t *pckt) - { - if (pckt == nullptr) - { - _debugMsg(F("Clear packet error: pointer is NULL!"), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - return; - } - pckt->crc = nullptr; - pckt->header = (packet_header_t *)(pckt->data); // заголовок же всегда стартует с начала пакета - pckt->msec = 0; - pckt->bytesLoaded = 0; - pckt->body = nullptr; - _clearBuffer(pckt->data); - } - - // очистка входящего пакета - void _clearInPacket() - { - _clearPacket(&_inPacket); - } - - // очистка исходящего пакета - void _clearOutPacket() - { - _clearPacket(&_outPacket); - _outPacket.header->start_byte = AC_PACKET_START_BYTE; // для исходящего сразу ставим стартовый байт - _outPacket.header->wifi = AC_PACKET_ANSWER; // для исходящего пакета сразу ставим признак ответа - } - - // копирует пакет из одной структуры в другую с корректным переносом указателей на заголовки и т.п. - bool _copyPacket(packet_t *dest, packet_t *src) - { - if (dest == nullptr) - return false; - if (src == nullptr) - return false; - - dest->msec = src->msec; - dest->bytesLoaded = src->bytesLoaded; - memcpy(dest->data, src->data, AC_BUFFER_SIZE); - dest->header = (packet_header_t *)&dest->data; - if (dest->header->body_length > 0) - dest->body = &dest->data[AC_HEADER_SIZE]; - dest->crc = (packet_crc_t *)&dest->data[AC_HEADER_SIZE + dest->header->body_length]; - - return true; - } - - // устанавливает состояние конечного автомата - // можно и напрямую устанавливать переменную, но для целей отладки лучше так - void _setStateMachineState(acsm_state state = ACSM_IDLE) - { - if (_ac_state == state) - return; // состояние не меняется - - _ac_state = state; - - switch (state) - { - case ACSM_IDLE: - _debugMsg(F("State changed to ACSM_IDLE."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - break; - - case ACSM_RECEIVING_PACKET: - _debugMsg(F("State changed to ACSM_RECEIVING_PACKET."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - break; - - case ACSM_PARSING_PACKET: - _debugMsg(F("State changed to ACSM_PARSING_PACKET."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - break; - - case ACSM_SENDING_PACKET: - _debugMsg(F("State changed to ACSM_SENDING_PACKET."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - break; - - default: - _debugMsg(F("State changed to ACSM_IDLE by default. Given state is %02X."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, state); - _ac_state = ACSM_IDLE; - break; - } - } - - // состояние конечного автомата: IDLE - void _doIdleState() - { - // вначале нужно выполнить очередной шаг последовательности команд - _doSequence(); - - // Если нет входящих данных, значит можно отправить исходящий пакет, если он есть - if (_ac_serial->available() == 0) - { - // если есть пакет на отправку, то надо отправлять - // вначале думал, что сейчас отправка пакетов тут не нужна, т.к. состояние ACSM_SENDING_PACKET устанавливается сразу в парсере пакетов - // но потом понял, что у нас пакеты уходят не только когда надо отвечать, но и мы можем быть инициаторами - // поэтому вызов отправки тут пригодится - if (_outPacket.msec > 0) - _setStateMachineState(ACSM_SENDING_PACKET); - // больше дел нет - выходим - return; - }; - - if (this->peek() == AC_PACKET_START_BYTE) - { - // если во входящий пакет что-то уже загружено, значит это какие-то ошибочные данные или неизвестные пакеты - // надо эту инфу вывалить в лог - if (_inPacket.bytesLoaded > 0) - { - _debugMsg(F("Start byte received but there are some unparsed bytes in the buffer:"), ESPHOME_LOG_LEVEL_DEBUG, __LINE__); - _debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_DEBUG, __LINE__); - } - _clearInPacket(); - _inPacket.msec = millis(); - _setStateMachineState(ACSM_RECEIVING_PACKET); - } - else - { - while (_ac_serial->available() > 0) - { - // если наткнулись на старт пакета, то выходим из while - // если какие-то данные были загружены в буфер, то они будут выгружены в лог при загрузке нового пакета - if (this->peek() == AC_PACKET_START_BYTE) - break; - - // читаем байт в буфер входящего пакета - _inPacket.data[_inPacket.bytesLoaded] = this->read(); - _inPacket.bytesLoaded++; - - // если буфер уже полон, надо его вывалить в лог и очистить - if (_inPacket.bytesLoaded >= AC_BUFFER_SIZE) - { - _debugMsg(F("Some unparsed data on the bus:"), ESPHOME_LOG_LEVEL_DEBUG, __LINE__); - _debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_DEBUG, __LINE__); - _clearInPacket(); - } - } - } - }; - - // состояние конечного автомата: ACSM_RECEIVING_PACKET - void _doReceivingPacketState() - { - while (_ac_serial->available() > 0) - { - // если в буфере пакета данных уже под завязку, то надо сообщить о проблеме и выйти - if (_inPacket.bytesLoaded >= AC_BUFFER_SIZE) - { - _debugMsg(F("Receiver: packet buffer overflow!"), ESPHOME_LOG_LEVEL_WARN, __LINE__); - _debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__); - _clearInPacket(); - _setStateMachineState(ACSM_IDLE); - return; - } - - _inPacket.data[_inPacket.bytesLoaded] = this->read(); - _inPacket.bytesLoaded++; - - // данных достаточно для заголовка - if (_inPacket.bytesLoaded == AC_HEADER_SIZE) - { - // указатель заголовка установлен еще при обнулении пакета, его можно не трогать - //_inPacket.header = (packet_header_t *)(_inPacket.data); - - // уже знаем размер пакета и можем установить указатели на тело пакета и CRC - _inPacket.crc = (packet_crc_t *)&(_inPacket.data[AC_HEADER_SIZE + _inPacket.header->body_length]); - if (_inPacket.header->body_length > 0) - _inPacket.body = &(_inPacket.data[AC_HEADER_SIZE]); - - _debugMsg(F("Header loaded: timestamp = %010u, start byte = %02X, packet type = %02X, body size = %02X"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _inPacket.msec, _inPacket.header->start_byte, _inPacket.header->packet_type, _inPacket.header->body_length); - } - - // если все байты пакета загружены, надо его распарсить - // максимальный по размеру пакет будет упираться в размер буфера. если такой пакет здесь не уйдет на парсинг, - // то на следующей итерации будет ошибка о переполнении буфера, которая в начале цикла while - if (_inPacket.bytesLoaded == AC_HEADER_SIZE + _inPacket.header->body_length + 2) - { - _debugMsg(F("Packet loaded: timestamp = %010u, start byte = %02X, packet type = %02X, body size = %02X, crc = [%02X, %02X]."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _inPacket.msec, _inPacket.header->start_byte, _inPacket.header->packet_type, _inPacket.header->body_length, _inPacket.crc->crc[0], _inPacket.crc->crc[1]); - _debugMsg(F("Loaded %02u bytes for a %u ms."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _inPacket.bytesLoaded, (millis() - _inPacket.msec)); - _debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - _setStateMachineState(ACSM_PARSING_PACKET); - return; - } - } - - // если пакет не загружен, а время вышло, то надо вернуться в IDLE - if (millis() - _inPacket.msec >= this->_packet_timeout) - { - _debugMsg(F("Receiver: packet timed out!"), ESPHOME_LOG_LEVEL_WARN, __LINE__); - _debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__); - _clearInPacket(); - _setStateMachineState(ACSM_IDLE); - return; - } - }; - - // состояние конечного автомата: ACSM_PARSING_PACKET - void _doParsingPacket() - { - if (!_checkCRC(&_inPacket)) - { - _debugMsg(F("Parser: packet CRC fail!"), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - _debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_ERROR, __LINE__); - _clearInPacket(); - _setStateMachineState(ACSM_IDLE); - return; - } - - bool stateChangedFlag = false; // флаг, показывающий, изменилось ли состояние кондиционера - uint8_t stateByte = 0; // переменная для временного сохранения текущих параметров сплита для проверки их изменения - float stateFloat = 0.0; // переменная для временного сохранения текущих параметров сплита для проверки их изменения - - // вначале выводим полученный пакет в лог, чтобы он шел до информации об ответах и т.п. - _debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_DEBUG, __LINE__); - - // разбираем тип пакета - switch (_inPacket.header->packet_type) - { - case AC_PTYPE_PING: - { // ping-пакет, рассылается кондиционером каждые 3 сек.; модуль на него отвечает - - if (_inPacket.header->body_length != 0) - { // у входящего ping-пакета тело должно отсутствовать - // если тело есть, то жалуемся в лог - _debugMsg(F("Parser: ping packet should not to have body. Received one has body length %02X."), ESPHOME_LOG_LEVEL_WARN, __LINE__, _inPacket.header->body_length); - // очищаем пакет - _clearInPacket(); - _setStateMachineState(ACSM_IDLE); - break; - } - - _debugMsg(F("Parser: ping packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - // поднимаем флаг, что есть коннект с кондиционером - _has_connection = true; - - // надо отправлять ответ на пинг - _clearOutPacket(); - _outPacket.msec = millis(); - _outPacket.header->packet_type = AC_PTYPE_PING; - _outPacket.header->ping_answer_01 = 0x01; // магия, детали тут: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_type_ping - _outPacket.header->body_length = 8; - _outPacket.body = &(_outPacket.data[AC_HEADER_SIZE]); - - // заполняем тело пакета - packet_ping_answer_body_t *ping_body; - ping_body = (packet_ping_answer_body_t *)(_outPacket.body); - ping_body->byte_1C = 0x1C; - ping_body->byte_27 = 0x27; - - // расчет контрольной суммы и прописывание её в пакет - _outPacket.crc = (packet_crc_t *)&(_outPacket.data[AC_HEADER_SIZE + _outPacket.header->body_length]); - _setCRC16(&_outPacket); - _outPacket.bytesLoaded = AC_HEADER_SIZE + _outPacket.header->body_length + 2; - - _debugMsg(F("Parser: generated ping answer. Waiting for sending."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - - // до отправки пинг-ответа проверяем, не выполнялась ли стартовая последовательность команд - // по задумке она выполняется после подключения к кондиционеру после ответа на первый пинг - // нужна для максимально быстрого определния текущих параметров кондиционера - if (!_startupSequenceComplete) - { - _startupSequenceComplete = startupSequence(); - } - - _setStateMachineState(ACSM_SENDING_PACKET); - - break; - } - - case AC_PTYPE_CMD: - { // команда сплиту; модуль отправляет такие команды, когда что-то хочет от сплита - // сплит такие команды отправлять не должен, поэтому жалуемся в лог - _debugMsg(F("Parser: packet type=0x06 received from HVAC. This isn't expected."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - // очищаем пакет - _clearInPacket(); - _setStateMachineState(ACSM_IDLE); - break; - } - - case AC_PTYPE_INFO: - { // информационный пакет - _debugMsg(F("Parser: status packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - // смотрим тип поступившего пакета по второму байту тела - // но вначале проверяем, что такое тело вообще есть - if ((_inPacket.body == nullptr) || (_inPacket.bytesLoaded < AC_HEADER_SIZE + 4) || (_inPacket.header->body_length < 2)) - { - _debugMsg(F("Parser: packet type=0x07 without body. Error!"), ESPHOME_LOG_LEVEL_WARN, __LINE__); - _clearInPacket(); - _setStateMachineState(ACSM_IDLE); - break; - } - // теперь можно проверять второй байт тела пакета - switch (_inPacket.body[1]) - { - case AC_CMD_STATUS_SMALL: - { // маленький пакет статуса кондиционера - _debugMsg(F("Parser: status packet type = small"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - stateChangedFlag = false; - - // будем обращаться к телу пакета через указатель на структуру - packet_small_info_body_t *small_info_body; - small_info_body = (packet_small_info_body_t *)(_inPacket.body); - - // в малом пакете передается большое количество установленных пользователем параметров работы - // stateFloat = 8 + (small_info_body->target_temp_int_and_v_louver >> 3) + 0.5 * (float)(small_info_body->target_temp_frac >> 7); - stateFloat = 8.0 + (float)(small_info_body->target_temp_int) + ((small_info_body->target_temp_frac_bool) ? 0.5 : 0.0); - stateChangedFlag = stateChangedFlag || (_current_ac_state.temp_target != stateFloat); - _current_ac_state.temp_target = stateFloat; - _current_ac_state.temp_target_matter = true; - - // stateByte = small_info_body->target_temp_int_and_v_louver & AC_LOUVERV_MASK; - stateByte = small_info_body->v_louver; - stateChangedFlag = stateChangedFlag || (_current_ac_state.louver.louver_v != (ac_louver_V)stateByte); - _current_ac_state.louver.louver_v = (ac_louver_V)stateByte; - - stateByte = small_info_body->h_louver & AC_LOUVERH_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.louver.louver_h != (ac_louver_H)stateByte); - _current_ac_state.louver.louver_h = (ac_louver_H)stateByte; - - stateByte = small_info_body->fan_speed & AC_FANSPEED_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.fanSpeed != (ac_fanspeed)stateByte); - _current_ac_state.fanSpeed = (ac_fanspeed)stateByte; - - stateByte = small_info_body->fan_turbo_and_mute & AC_FANTURBO_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.fanTurbo != (ac_fanturbo)stateByte); - _current_ac_state.fanTurbo = (ac_fanturbo)stateByte; - - stateByte = small_info_body->fan_turbo_and_mute & AC_FANMUTE_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.fanMute != (ac_fanmute)stateByte); - _current_ac_state.fanMute = (ac_fanmute)stateByte; - - stateByte = small_info_body->mode & AC_MODE_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.mode != (ac_mode)stateByte); - _current_ac_state.mode = (ac_mode)stateByte; - - stateByte = small_info_body->mode & AC_TEMPERATURE_UNIT_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.t_unit != (ac_temperature_unit)stateByte); - _current_ac_state.t_unit = (ac_temperature_unit)stateByte; - - stateByte = small_info_body->mode & AC_SLEEP_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.sleep != (ac_sleep)stateByte); - _current_ac_state.sleep = (ac_sleep)stateByte; - - stateByte = small_info_body->status & AC_POWER_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.power != (ac_power)stateByte); - _current_ac_state.power = (ac_power)stateByte; - - stateByte = small_info_body->status & AC_HEALTH_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.health != (ac_health)stateByte); - _current_ac_state.health = (ac_health)stateByte; - - stateByte = small_info_body->status & AC_HEALTH_STATUS_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.health_status != (ac_health_status)stateByte); - _current_ac_state.health_status = (ac_health_status)stateByte; - - stateByte = small_info_body->status & AC_CLEAN_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.clean != (ac_clean)stateByte); - _current_ac_state.clean = (ac_clean)stateByte; - - stateByte = small_info_body->display_and_mildew & AC_DISPLAY_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.display != (ac_display)stateByte); - _current_ac_state.display = (ac_display)stateByte; - - stateByte = small_info_body->display_and_mildew & AC_MILDEW_MASK; - stateChangedFlag = stateChangedFlag || (_current_ac_state.mildew != (ac_mildew)stateByte); - _current_ac_state.mildew = (ac_mildew)stateByte; - - stateByte = AC_POWLIMSTAT_ON * small_info_body->inverter_power_limitation_enable; - stateChangedFlag = stateChangedFlag || (_current_ac_state.power_lim_state != (ac_powLim_state)stateByte); - _current_ac_state.power_lim_state = (ac_powLim_state)stateByte; - - stateByte = small_info_body->inverter_power_limitation_value; - stateChangedFlag = stateChangedFlag || (_current_ac_state.power_lim_value != stateByte); - _current_ac_state.power_lim_value = stateByte; - - // уведомляем об изменении статуса сплита - if (stateChangedFlag) - stateChanged(); - - break; - } - - case AC_CMD_STATUS_BIG: // большой пакет статуса кондиционера - case AC_CMD_STATUS_PERIODIC: - { // раз в 10 минут рассылается сплитом, структура аналогична большому пакету статуса - // TODO: вроде как AC_CMD_STATUS_PERIODIC могут быть и с другими кодами; пока что другие будут игнорироваться; если это будет критично, надо будет поправить - _debugMsg(F("Parser: status packet type = big or periodic"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - stateChangedFlag = false; - - // будем обращаться к телу пакета через указатель на структуру - packet_big_info_body_t *big_info_body; - big_info_body = (packet_big_info_body_t *)(_inPacket.body); - - // тип кондея (инвертор или старт стоп) - _is_inverter = big_info_body->is_inverter; - - // температура воздуха в помещении по версии сплит-системы - 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; - - // некая температура из наружного блока, скорее всего температура испарителя - // 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->inverter_power; - stateChangedFlag = stateChangedFlag || (_current_ac_state.inverter_power != stateFloat); - _current_ac_state.inverter_power = stateFloat; - - // режим разморозки - // bool temp = (big_info_body->needDefrost && big_info_body->defrostMode); - // TODO: need additional info for bit big_info_body->needDefrost - // Some HVACs use it but others don't (they use bit 3 instead of bit 4 (needDefrost)) - bool temp = big_info_body->defrostMode; - stateChangedFlag = stateChangedFlag || (_current_ac_state.defrost != temp); - _current_ac_state.defrost = temp; - - // уведомляем об изменении статуса сплита - if (stateChangedFlag) - stateChanged(); - - break; - } - - case AC_CMD_SET_PARAMS: - { // такой статусный пакет присылается кондиционером в ответ на команду установки параметров - // в теле пакета нет ничего примечательного - // в байтах 2 и 3 тела передается CRC пакета поступившей команды, на которую сплит отвечает - // я решил этот момент тут не проверять и не контролировать. - // корректную установку параметров можно определить, запросив статус кондиционера сразу после получения этой команды кондея - // в настоящий момент проверка сделана в механизме sequences - break; - } - - default: - _debugMsg(F("Parser: status packet type = unknown (%02X)"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _inPacket.body[1]); - break; - } - _setStateMachineState(ACSM_IDLE); - break; - } - - case AC_PTYPE_INIT: // инициирующий пакет; присылается сплитом, если кнопка HEALTH на пульте нажимается 8 раз; как там и что работает - не разбирался. - case AC_PTYPE_UNKN: // какой-то странный пакет, отправляемый пультом при инициации и иногда при включении питания... как работает и зачем нужен - не разбирался, сплит на него вроде бы не реагирует - default: - // игнорируем. Для нашего случая эти пакеты не важны - _setStateMachineState(ACSM_IDLE); - break; - } - - // если есть последовательность команд, то надо отработать проверку последовательности - if (hasSequence()) - _doSequence(); - - // после разбора входящего пакета его надо очистить - _clearInPacket(); - } - - // состояние конечного автомата: ACSM_SENDING_PACKET - void _doSendingPacketState() - { - // если нет исходящего пакета, то выходим - if ((_outPacket.msec == 0) || (_outPacket.crc == nullptr) || (_outPacket.bytesLoaded == 0)) - { - _debugMsg(F("Sender: no packet to send."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - _setStateMachineState(ACSM_IDLE); - return; - } - - _debugMsg(F("Sender: sending packet."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - - _ac_serial->write_array(_outPacket.data, _outPacket.bytesLoaded); - _ac_serial->flush(); - - _debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_DEBUG, __LINE__); - _debugMsg(F("Sender: %u bytes sent (%u ms)."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _outPacket.bytesLoaded, millis() - _outPacket.msec); - _clearOutPacket(); - - _setStateMachineState(ACSM_IDLE); - }; - - /** вывод отладочной информации в лог - * - * dbgLevel - уровень сообщения, определен в ESPHome. За счет его использования можно из ESPHome управлять полнотой сведений в логе. - * msg - сообщение, выводимое в лог - * line - строка, на которой произошел вызов (удобно при отладке) - */ - void _debugMsg(const String &msg, uint8_t dbgLevel = ESPHOME_LOG_LEVEL_DEBUG, unsigned int line = 0, ...) - { - if (dbgLevel < ESPHOME_LOG_LEVEL_NONE) - dbgLevel = ESPHOME_LOG_LEVEL_NONE; - if (dbgLevel > ESPHOME_LOG_LEVEL_VERY_VERBOSE) - dbgLevel = ESPHOME_LOG_LEVEL_VERY_VERBOSE; - - if (line == 0) - line = __LINE__; // если строка не передана, берем текущую строку - - va_list vl; - va_start(vl, line); - esp_log_vprintf_(dbgLevel, TAG, line, msg.c_str(), vl); - va_end(vl); - } - - /** выводим данные пакета в лог для отладки - * - * dbgLevel - уровень сообщения, определен в ESPHome. За счет его использования можно из ESPHome управлять полнотой сведений в логе. - * packet - указатель на пакет для вывода; - * если указатель на crc равен nullptr или первый байт в буфере не AC_PACKET_START_BYTE, то считаем, что передан битый пакет - * или не пакет вовсе. Для такого выводим только массив байт. - * Для нормального пакета данные выводятся с форматированием. - * line - строка, на которой произошел вызов (удобно при отладке) - **/ - void _debugPrintPacket(packet_t *packet, uint8_t dbgLevel = ESPHOME_LOG_LEVEL_DEBUG, unsigned int line = __LINE__) - { - // определяем, полноценный ли пакет нам передан - bool notAPacket = false; - // указатель заголовка всегда установден на начало буфера - notAPacket = notAPacket || (packet->crc == nullptr); - notAPacket = notAPacket || (packet->data[0] != AC_PACKET_START_BYTE); - - // если пакет по длине меньше, чем указано в фильтре, то не выводим. - // если вывод пакетов отключен с помощью директивы HOLMES_WORKS, то тоже не выводим. - // "не пакеты" выводим всегда, так как от них зависит отладка багов - if ((!notAPacket) && (packet->header->body_length < HOLMES_FILTER_LEN)) - return; - if ((!notAPacket) && (!HOLMES_WORKS)) - return; - - String st = ""; - char textBuf[11]; - - // заполняем время получения пакета - memset(textBuf, 0, 11); - sprintf(textBuf, "%010u", packet->msec); - st = st + textBuf + ": "; - - // формируем преамбулы - if (packet == &_inPacket) - { - st += "[<=] "; // преамбула входящего пакета - } - else if (packet == &_outPacket) - { - st += "[=>] "; // преамбула исходящего пакета - } - else - { - st += "[--] "; // преамбула для "непакета" - } - - // формируем данные - for (int i = 0; i < packet->bytesLoaded; i++) - { - // для заголовков нормальных пакетов надо отработать скобки (если они есть) - if ((!notAPacket) && (i == 0)) - st += HOLMES_HEADER_BRACKET_OPEN; - // для CRC нормальных пакетов надо отработать скобки (если они есть) - if ((!notAPacket) && (i == packet->header->body_length + AC_HEADER_SIZE)) - st += HOLMES_CRC_BRACKET_OPEN; - - memset(textBuf, 0, 11); - sprintf(textBuf, HOLMES_BYTE_FORMAT, packet->data[i]); - st += textBuf; - - // для заголовков нормальных пакетов надо отработать скобки (если они есть) - if ((!notAPacket) && (i == AC_HEADER_SIZE - 1)) - st += HOLMES_HEADER_BRACKET_CLOSE; - // для CRC нормальных пакетов надо отработать скобки (если они есть) - if ((!notAPacket) && (i == packet->header->body_length + AC_HEADER_SIZE + 2 - 1)) - st += HOLMES_CRC_BRACKET_CLOSE; - - st += HOLMES_DELIMITER; - } - - _debugMsg(st, dbgLevel, line); - } - - /** расчет CRC16 для блока данных data длиной len - * data - данные для расчета CRC16, указатель на массив байт - * len - длина блока данных для расчета, в байтах - * - * возвращаем uint16_t CRC16 - **/ - uint16_t _CRC16(uint8_t *data, uint8_t len) - { - uint32_t crc = 0; - - // выделяем буфер для расчета CRC и копируем в него переданные данные - // это нужно для того, чтобы в случае нечетной длины данных можно было дополнить тело пакета - // одним нулевым байтом и не попортить загруженный пакет (ведь в загруженном сразу за телом идёт CRC) - uint8_t _crcBuffer[AC_BUFFER_SIZE]; - memset(_crcBuffer, 0, AC_BUFFER_SIZE); - memcpy(_crcBuffer, data, len); - - // если длина данных нечетная, то надо сделать четной, дополнив данные в конце нулевым байтом - // но так как выше буфер заполняли нулями, то отдельно тут присваивать 0x00 нет смысла - if ((len % 2) == 1) - len++; - - // рассчитываем CRC16 - uint32_t word = 0; - for (uint8_t i = 0; i < len; i += 2) - { - word = (_crcBuffer[i] << 8) + _crcBuffer[i + 1]; - crc += word; - } - crc = (crc >> 16) + (crc & 0xFFFF); - crc = ~crc; - - return crc & 0xFFFF; - } - - // расчитываем CRC16 и заполняем эти данные в структуре пакета - void _setCRC16(packet_t *pack = nullptr) - { - // если пакет не указан, то устанавливаем CRC для исходящего пакета - if (pack == nullptr) - pack = &_outPacket; - - packet_crc_t crc; - crc.crc16 = _CRC16(pack->data, AC_HEADER_SIZE + pack->header->body_length); - - // если забыли указатель на crc установить, то устанавливаем - if (pack->crc == nullptr) - pack->crc = (packet_crc_t *)&(pack->data[AC_HEADER_SIZE + pack->header->body_length]); - - pack->crc->crc[0] = crc.crc[1]; - pack->crc->crc[1] = crc.crc[0]; - return; - } - - // проверяет CRC пакета по указателю - bool _checkCRC(packet_t *pack = nullptr) - { - // если пакет не указан, то проверяем входящий - if (pack == nullptr) - pack = &_inPacket; - if (pack->bytesLoaded < AC_HEADER_SIZE) - { - _debugMsg(F("CRC check: incoming packet size error."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - // если забыли указатель на crc установить, то устанавливаем - if (pack->crc == nullptr) - pack->crc = (packet_crc_t *)&(pack->data[AC_HEADER_SIZE + pack->header->body_length]); - - packet_crc_t crc; - crc.crc16 = _CRC16(pack->data, AC_HEADER_SIZE + pack->header->body_length); - - return ((pack->crc->crc[0] == crc.crc[1]) && (pack->crc->crc[1] == crc.crc[0])); - } - - // заполняет пакет по ссылке командой запроса маленького пакета статуса - void _fillStatusSmall(packet_t *pack = nullptr) - { - // по умолчанию заполняем исходящий пакет - if (pack == nullptr) - pack = &_outPacket; - - // присваиваем параметры пакета - pack->msec = millis(); - pack->header->start_byte = AC_PACKET_START_BYTE; - pack->header->wifi = AC_PACKET_ANSWER; // для исходящего пакета ставим признак ответа - pack->header->packet_type = AC_PTYPE_CMD; - pack->header->body_length = 2; // тело команды 2 байта - pack->body = &(pack->data[AC_HEADER_SIZE]); - pack->body[0] = AC_CMD_STATUS_SMALL; - pack->body[1] = 0x01; // он всегда 0x01 - pack->bytesLoaded = AC_HEADER_SIZE + pack->header->body_length + 2; - - // рассчитываем и записываем в пакет CRC - pack->crc = (packet_crc_t *)&(pack->data[AC_HEADER_SIZE + pack->header->body_length]); - _setCRC16(pack); - } - - // заполняет пакет по ссылке командой запроса большого пакета статуса - void _fillStatusBig(packet_t *pack = nullptr) - { - // по умолчанию заполняем исходящий пакет - if (pack == nullptr) - pack = &_outPacket; - - // присваиваем параметры пакета - pack->msec = millis(); - pack->header->start_byte = AC_PACKET_START_BYTE; - pack->header->wifi = AC_PACKET_ANSWER; // для исходящего пакета ставим признак ответа - pack->header->packet_type = AC_PTYPE_CMD; - pack->header->body_length = 2; // тело команды 2 байта - pack->body = &(pack->data[AC_HEADER_SIZE]); - pack->body[0] = AC_CMD_STATUS_BIG; - pack->body[1] = 0x01; // он всегда 0x01 - pack->bytesLoaded = AC_HEADER_SIZE + pack->header->body_length + 2; - - // рассчитываем и записываем в пакет CRC - pack->crc = (packet_crc_t *)&(pack->data[AC_HEADER_SIZE + pack->header->body_length]); - _setCRC16(pack); - } - - /** заполняет пакет по ссылке командой установки параметров - * - * указатель на пакет может отсутствовать, тогда заполняется _outPacket - * указатель на команду также может отсутствовать, тогда используется текущее состояние из _current_ac_state - * все *__UNTOUCHED параметры заполняются из _current_ac_state - **/ - void _fillSetCommand(bool clrPacket = false, packet_t *pack = nullptr, ac_state_t *cmd = nullptr) - { - // по умолчанию заполняем исходящий пакет - if (pack == nullptr) - pack = &_outPacket; - - // очищаем пакет, если это указано - if (clrPacket) - _clearPacket(pack); - - // заполняем его параметрами из _current_ac_state - if (cmd != &_current_ac_state) - _fillSetCommand(false, pack, &_current_ac_state); - - // если команда не указана, значит выходим - if (cmd == nullptr) - return; - - // команда указана, дополнительно внесем в пакет те параметры, которые установлены в команде - // присваиваем параметры пакета - pack->msec = millis(); - pack->header->start_byte = AC_PACKET_START_BYTE; - pack->header->wifi = AC_PACKET_ANSWER; // для исходящего пакета ставим признак ответа - pack->header->packet_type = AC_PTYPE_CMD; - pack->header->body_length = 0x0F; // тело команды 15 (0x0F) байт, как у Small status - pack->body = &(pack->data[AC_HEADER_SIZE]); - pack->body[0] = AC_CMD_SET_PARAMS; // устанавливаем параметры - pack->body[1] = 0x01; // он всегда 0x01 - pack->bytesLoaded = AC_HEADER_SIZE + pack->header->body_length + 2; - - // целевая температура кондиционера - if (cmd->temp_target_matter) - { - // устраняем выход за границы диапазона (это ограничение самого кондиционера) - 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); - - // дробная часть температуры - if (cmd->temp_target - (uint8_t)(cmd->temp_target) >= 0.5) - { - pack->body[4] = (pack->body[4] | AC_TEMP_TARGET_FRAC_PART_MASK); - } - else - { - pack->body[4] = (pack->body[4] & ~AC_TEMP_TARGET_FRAC_PART_MASK); - } - } - - // значение ограничения мощности инвертора - if ((cmd->power_lim_value != AC_POWLIMVAL_UNTOUCHED)) - { - cmd->power_lim_value = _power_limitation_value_normalise(cmd->power_lim_value); - pack->body[13] = (pack->body[13] & ~AC_POWLIMVAL_MASK) | (cmd->power_lim_value & AC_POWLIMVAL_MASK); - } - - // включение/выключение ограничения мощности инвертора - if ((cmd->power_lim_state != AC_POWLIMSTAT_UNTOUCHED)) - { - pack->body[13] = (pack->body[13] & ~AC_POWLIMSTAT_MASK) | (cmd->power_lim_state & AC_POWLIMSTAT_MASK); - } - - // обнулить счетчик минут с последней команды - // 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; - } - - // горизонтальные жалюзи - if (cmd->louver.louver_h != AC_LOUVERH_UNTOUCHED) - { - pack->body[3] = (pack->body[3] & ~AC_LOUVERH_MASK) | cmd->louver.louver_h; - } - - // скорость вентилятора - if (cmd->fanSpeed != AC_FANSPEED_UNTOUCHED) - { - pack->body[5] = (pack->body[5] & ~AC_FANSPEED_MASK) | cmd->fanSpeed; - } - - // спец.режимы вентилятора: TURBO - if (cmd->fanTurbo != AC_FANTURBO_UNTOUCHED) - { - pack->body[6] = (pack->body[6] & ~AC_FANTURBO_MASK) | cmd->fanTurbo; - } - - // спец.режимы вентилятора: MUTE - if (cmd->fanMute != AC_FANMUTE_UNTOUCHED) - { - pack->body[6] = (pack->body[6] & ~AC_FANMUTE_MASK) | cmd->fanMute; - } - - // режим кондея - if (cmd->mode != AC_MODE_UNTOUCHED) - { - pack->body[7] = (pack->body[7] & ~AC_MODE_MASK) | cmd->mode; - } - if (cmd->t_unit != AC_TEMPERATURE_UNIT_UNTOUCHED) - { - pack->body[7] = (pack->body[7] & ~AC_TEMPERATURE_UNIT_MASK) | cmd->t_unit; - } - if (cmd->sleep != AC_SLEEP_UNTOUCHED) - { - pack->body[7] = (pack->body[7] & ~AC_SLEEP_MASK) | cmd->sleep; - } - - // питание вкл/выкл - if (cmd->power != AC_POWER_UNTOUCHED) - { - pack->body[10] = (pack->body[10] & ~AC_POWER_MASK) | cmd->power; - } - - // просушка - if (cmd->clean != AC_CLEAN_UNTOUCHED) - { - pack->body[10] = (pack->body[10] & ~AC_CLEAN_MASK) | cmd->clean; - } - - // ионизатор - if (cmd->health != AC_HEALTH_UNTOUCHED) - { - pack->body[10] = (pack->body[10] & ~AC_HEALTH_MASK) | cmd->health; - } - - // какой то флаг ионизатора - if (cmd->health_status != AC_HEALTH_STATUS_UNTOUCHED) - { - pack->body[10] = (pack->body[10] & ~AC_HEALTH_STATUS_MASK) | cmd->health_status; - } - - // дисплей - if (cmd->display != AC_DISPLAY_UNTOUCHED) - { - pack->body[12] = (pack->body[12] & ~AC_DISPLAY_MASK) | cmd->display; - } - - // антиплесень - if (cmd->mildew != AC_MILDEW_UNTOUCHED) - { - pack->body[12] = (pack->body[12] & ~AC_MILDEW_MASK) | cmd->mildew; - } - - // рассчитываем и записываем в пакет CRC - pack->crc = (packet_crc_t *)&(pack->data[AC_HEADER_SIZE + pack->header->body_length]); - _setCRC16(pack); - } - - // отправка запроса на маленький статусный пакет - bool sq_requestSmallStatus() - { - // если исходящий пакет не пуст, то выходим и ждем освобождения - if (_outPacket.bytesLoaded > 0) - return true; - - _fillStatusSmall(&_outPacket); - _fillStatusSmall(&_sequence[_sequence_current_step].packet); - _sequence[_sequence_current_step].packet_type = AC_SPT_SENT_PACKET; - - // Отчитываемся в лог - _debugMsg(F("Sequence [step %u]: small status request generated:"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); - _debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - - // увеличиваем текущий шаг - _sequence_current_step++; - return true; - } - - // проверка ответа на запрос маленького статусного пакета - bool sq_controlSmallStatus() - { - // если по каким-то причинам нет входящего пакета, значит проверять нам нечего - просто выходим - if (_inPacket.bytesLoaded == 0) - return true; - - // Пинги игнорируем - if (_inPacket.header->packet_type == AC_PTYPE_PING) - return true; - - // сохраняем полученный пакет в последовательность, чтобы на возможных следующих шагах с ним можно было работать - _copyPacket(&_sequence[_sequence_current_step].packet, &_inPacket); - _sequence[_sequence_current_step].packet_type = AC_SPT_RECEIVED_PACKET; - - // проверяем ответ - bool relevant = true; - relevant = (relevant && (_inPacket.header->packet_type == AC_PTYPE_INFO)); - relevant = (relevant && (_inPacket.header->body_length == 0x0F)); - relevant = (relevant && (_inPacket.body[0] == 0x01)); - relevant = (relevant && (_inPacket.body[1] == AC_CMD_STATUS_SMALL)); - - // если пакет подходит... - if (relevant) - { - // ...значит можно переходить к следующему шагу - // так как пакет корректный, то его можно скопировать в последние полученные пакеты - _copyPacket(&_last_raw_data.last_small_info_packet, &_inPacket); - - // отчитываемся в лог и переходим к следующему шагу - _debugMsg(F("Sequence [step %u]: correct small status packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); - _sequence_current_step++; - } - else - { - // если пакет не подходящий, то отчитываемся в лог... - _debugMsg(F("Sequence [step %u]: irrelevant incoming packet"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step); - _debugMsg(F("Incoming packet:"), ESPHOME_LOG_LEVEL_WARN, __LINE__); - _debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__); - _debugMsg(F("Sequence packet needed: PACKET_TYPE = %02X, CMD = %02X"), ESPHOME_LOG_LEVEL_WARN, __LINE__, AC_PTYPE_INFO, AC_CMD_STATUS_SMALL); - // ...и прерываем последовательность, так как вернем false - } - return relevant; - } - - // отправка запроса на большой статусный пакет - bool sq_requestBigStatus() - { - // если исходящий пакет не пуст, то выходим и ждем освобождения - if (_outPacket.bytesLoaded > 0) - return true; - - _fillStatusBig(&_outPacket); - _fillStatusBig(&_sequence[_sequence_current_step].packet); - _sequence[_sequence_current_step].packet_type = AC_SPT_SENT_PACKET; - - // Отчитываемся в лог - _debugMsg(F("Sequence [step %u]: big status request generated:"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); - _debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - - // увеличиваем текущий шаг - _sequence_current_step++; - return true; - } - - // проверка ответа на запрос большого статусного пакета - bool sq_controlBigStatus() - { - // если по каким-то причинам нет входящего пакета, значит проверять нам нечего - просто выходим - if (_inPacket.bytesLoaded == 0) - return true; - - // Пинги игнорируем - if (_inPacket.header->packet_type == AC_PTYPE_PING) - return true; - - // сохраняем полученный пакет в последовательность, чтобы на возможных следующих шагах с ним можно было работать - _copyPacket(&_sequence[_sequence_current_step].packet, &_inPacket); - _sequence[_sequence_current_step].packet_type = AC_SPT_RECEIVED_PACKET; - - // проверяем ответ - bool relevant = true; - relevant = (relevant && (_inPacket.header->packet_type == AC_PTYPE_INFO)); - relevant = (relevant && (_inPacket.header->body_length == 0x18 || _inPacket.header->body_length == 0x19)); // канальник Royal Clima отвечает пакетом длиной 0x19 - relevant = (relevant && (_inPacket.body[0] == 0x01)); - relevant = (relevant && (_inPacket.body[1] == AC_CMD_STATUS_BIG)); - - // если пакет подходит... - if (relevant) - { - // ...значит можно переходить к следующему шагу - // так как пакет корректный, то его можно скопировать в последние полученные пакеты - _copyPacket(&_last_raw_data.last_big_info_packet, &_inPacket); - - // отчитываемся в лог и переходим к следующему шагу - _debugMsg(F("Sequence [step %u]: correct big status packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); - _sequence_current_step++; - } - else - { - // если пакет не подходящий, то отчитываемся в лог... - _debugMsg(F("Sequence [step %u]: irrelevant incoming packet"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step); - _debugMsg(F("Incoming packet:"), ESPHOME_LOG_LEVEL_WARN, __LINE__); - _debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__); - _debugMsg(F("Sequence packet needed: PACKET_TYPE = %02X, CMD = %02X"), ESPHOME_LOG_LEVEL_WARN, __LINE__, AC_PTYPE_INFO, AC_CMD_STATUS_BIG); - // ...и прерываем последовательность - } - return relevant; - } - - // отправка запроса на выполнение команды - bool sq_requestDoCommand() - { - // если исходящий пакет не пуст, то выходим и ждем освобождения - if (_outPacket.bytesLoaded > 0) - return true; - - _fillSetCommand(true, &_outPacket, &_sequence[_sequence_current_step].cmd); - _fillSetCommand(true, &_sequence[_sequence_current_step].packet, &_sequence[_sequence_current_step].cmd); - _sequence[_sequence_current_step].packet_type = AC_SPT_SENT_PACKET; - - // Отчитываемся в лог - _debugMsg(F("Sequence [step %u]: doCommand request generated:"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); - _debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - - // увеличиваем текущий шаг - _sequence_current_step++; - return true; - } - - // проверка ответа на выполнение команды - bool sq_controlDoCommand() - { - // если по каким-то причинам нет входящего пакета, значит проверять нам нечего - просто выходим - if (_inPacket.bytesLoaded == 0) - return true; - - // Пинги игнорируем - if (_inPacket.header->packet_type == AC_PTYPE_PING) - return true; - - // сохраняем полученный пакет в последовательность, чтобы на возможных следующих шагах с ним можно было работать - _copyPacket(&_sequence[_sequence_current_step].packet, &_inPacket); - _sequence[_sequence_current_step].packet_type = AC_SPT_RECEIVED_PACKET; - - // проверяем ответ - bool relevant = true; - relevant = (relevant && (_inPacket.header->packet_type == AC_PTYPE_INFO)); - relevant = (relevant && (_inPacket.header->body_length == 0x04)); - relevant = (relevant && (_inPacket.body[0] == 0x01)); - relevant = (relevant && (_inPacket.body[1] == AC_CMD_SET_PARAMS)); - // байты 2 и 3 обычно равны CRC отправленного пакета с командой - relevant = (relevant && (_inPacket.body[2] == _sequence[_sequence_current_step - 1].packet.crc->crc[0])); - relevant = (relevant && (_inPacket.body[3] == _sequence[_sequence_current_step - 1].packet.crc->crc[1])); - - // если пакет подходит, значит можно переходить к следующему шагу - if (relevant) - { - _debugMsg(F("Sequence [step %u]: correct doCommand packet received"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); - _sequence_current_step++; - } - else - { - // если пакет не подходящий, то отчитываемся в лог... - _debugMsg(F("Sequence [step %u]: irrelevant incoming packet"), ESPHOME_LOG_LEVEL_WARN, __LINE__, _sequence_current_step); - _debugMsg(F("Incoming packet:"), ESPHOME_LOG_LEVEL_WARN, __LINE__); - _debugPrintPacket(&_inPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__); - _debugMsg(F("Sequence packet needed: PACKET_TYPE = %02X, CMD = %02X"), ESPHOME_LOG_LEVEL_WARN, __LINE__, AC_PTYPE_INFO, AC_CMD_STATUS_BIG); - // ...и прерываем последовательность - } - return relevant; - } - - // отправка запроса с тестовым пакетом - bool sq_requestTestPacket() - { - // если исходящий пакет не пуст, то выходим и ждем освобождения - if (_outPacket.bytesLoaded > 0) - return true; - - _copyPacket(&_outPacket, &_outTestPacket); - _copyPacket(&_sequence[_sequence_current_step].packet, &_outTestPacket); - _sequence[_sequence_current_step].packet_type = AC_SPT_SENT_PACKET; - - // Отчитываемся в лог - _debugMsg(F("Sequence [step %u]: Test Packet request generated:"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _sequence_current_step); - _debugPrintPacket(&_outPacket, ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - - // увеличиваем текущий шаг - _sequence_current_step++; - return true; - } - - // сенсоры, отображающие параметры сплита - esphome::sensor::Sensor *sensor_indoor_temperature_ = nullptr; - esphome::sensor::Sensor *sensor_outdoor_temperature_ = nullptr; - esphome::sensor::Sensor *sensor_inbound_temperature_ = nullptr; - esphome::sensor::Sensor *sensor_outbound_temperature_ = nullptr; - esphome::sensor::Sensor *sensor_compressor_temperature_ = nullptr; - esphome::sensor::Sensor *sensor_inverter_power_ = nullptr; - esphome::sensor::Sensor *sensor_vlouver_state_ = nullptr; - esphome::binary_sensor::BinarySensor *sensor_display_ = nullptr; - esphome::binary_sensor::BinarySensor *sensor_defrost_ = nullptr; - esphome::text_sensor::TextSensor *sensor_preset_reporter_ = nullptr; - esphome::sensor::Sensor *sensor_inverter_power_limit_value_ = nullptr; - esphome::binary_sensor::BinarySensor *sensor_inverter_power_limit_state_ = nullptr; - - // загружает на выполнение последовательность команд на включение/выключение табло с температурой - bool _displaySequence(ac_display dsp = AC_DISPLAY_ON) - { - // нет смысла в последовательности, если нет коннекта с кондиционером - if (!get_has_connection()) - { - _debugMsg(F("displaySequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - return false; - } - if (dsp == AC_DISPLAY_UNTOUCHED) - return false; // выходим, чтобы не тратить время - - // формируем команду - ac_command_t cmd; - _clearCommand(&cmd); // не забываем очищать, а то будет мусор - cmd.display = dsp; - // добавляем команду в последовательность - if (!commandSequence(&cmd)) - return false; - - _debugMsg(F("displaySequence: loaded (display = %02X)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, dsp); - return true; - } - -#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); // копируем пресет в массив - - _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); - } - } - 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); - _clearPacket(&_last_raw_data.last_big_info_packet); - _clearPacket(&_last_raw_data.last_small_info_packet); - - _setStateMachineState(ACSM_IDLE); - _ac_serial = parent; - _hw_initialized = (_ac_serial != nullptr); - _has_connection = false; - _packet_timeout = Constants::AC_PACKET_TIMEOUT_MIN; - - // заполняем структуру состояния начальными значениями - _clearCommand((ac_command_t *)&_current_ac_state); - - // очищаем последовательность пакетов - _clearSequence(); - - // выполнена ли уже стартовая последовательность команд (сбор информации о статусе кондея) - _startupSequenceComplete = 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_vlouver_state_sensor(sensor::Sensor *vlouver_state_sensor) { sensor_vlouver_state_ = vlouver_state_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_inverter_power_sensor(sensor::Sensor *inverter_power_sensor) { sensor_inverter_power_ = inverter_power_sensor; } - void set_preset_reporter_sensor(text_sensor::TextSensor *preset_reporter_sensor) { sensor_preset_reporter_ = preset_reporter_sensor; } - void set_inverter_power_limit_value_sensor(sensor::Sensor *inverter_power_limit_value_sensor) { sensor_inverter_power_limit_value_ = inverter_power_limit_value_sensor; } - void set_inverter_power_limit_state_sensor(binary_sensor::BinarySensor *inverter_power_limit_state_sensor) { sensor_inverter_power_limit_state_ = inverter_power_limit_state_sensor; } - - bool get_hw_initialized() { return _hw_initialized; }; - bool get_has_connection() { return _has_connection; }; - - // возвращает, есть ли елементы в последовательности команд - bool hasSequence() - { - return (_sequence[0].item_type != AC_SIT_NONE); - } - - // вызывается для публикации нового состояния кондиционера - void stateChanged() - { - _debugMsg(F("State changed, let's publish it."), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - - // экшины кондиционера (информация для пользователя, что кондиционер сейчас делает) - // сейчас экшины рассчётные и могут не отражать реального положения дел, но других вариантов не придумалось - if (_is_inverter) - { - // анализ режима для инвертора точнее потому, что использует показания мощности инвертора - static uint32_t timerInv = 0; - if (_current_ac_state.inverter_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(_is_inverter) - // для on-off сплита рассчет экшена упрощен - 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)) - { - this->action = climate::CLIMATE_ACTION_DRYING; // ОСУШЕНИЕ - } - else if (_current_ac_state.realFanSpeed != AC_REAL_FAN_OFF && - _current_ac_state.realFanSpeed != AC_REAL_FAN_MUTE) - { - if (delta_temp > 2) - { - this->action = climate::CLIMATE_ACTION_COOLING; - } - else if (delta_temp < -2) - { - this->action = climate::CLIMATE_ACTION_HEATING; - } - else - { - this->action = climate::CLIMATE_ACTION_FAN; // другие режимы - вентиляция - } - } - else - { - this->action = climate::CLIMATE_ACTION_IDLE; - } - } - } - - _debugMsg(F("Action mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->action); - - /*************************** POWER & MODE ***************************/ - if (_current_ac_state.power == AC_POWER_ON) - { - switch (_current_ac_state.mode) - { - case AC_MODE_AUTO: - // по факту режим, названный в AUX как AUTO, является режимом HEAT_COOL - this->mode = climate::CLIMATE_MODE_HEAT_COOL; - break; - - case AC_MODE_COOL: - this->mode = climate::CLIMATE_MODE_COOL; - break; - - case AC_MODE_DRY: - this->mode = climate::CLIMATE_MODE_DRY; - break; - - case AC_MODE_HEAT: - this->mode = climate::CLIMATE_MODE_HEAT; - break; - - case AC_MODE_FAN: - this->mode = climate::CLIMATE_MODE_FAN_ONLY; - break; - - default: - _debugMsg(F("Warning: unknown air conditioner mode."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - break; - } - } - else - { - this->mode = climate::CLIMATE_MODE_OFF; - } - - _debugMsg(F("Climate mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->mode); - - /*************************** FAN SPEED ***************************/ - this->fan_mode = climate::CLIMATE_FAN_OFF; - switch (_current_ac_state.fanSpeed) - { - case AC_FANSPEED_HIGH: - this->fan_mode = climate::CLIMATE_FAN_HIGH; - break; - - case AC_FANSPEED_MEDIUM: - this->fan_mode = climate::CLIMATE_FAN_MEDIUM; - break; - - case AC_FANSPEED_LOW: - this->fan_mode = climate::CLIMATE_FAN_LOW; - break; - - case AC_FANSPEED_AUTO: - this->fan_mode = climate::CLIMATE_FAN_AUTO; - break; - - default: - _debugMsg(F("Warning: unknown fan speed."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - break; - } - - _debugMsg(F("Climate fan mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->fan_mode); - - /*************************** TURBO FAN MODE ***************************/ - // 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; - //} - break; - - case AC_FANTURBO_OFF: - default: - if (this->custom_fan_mode == Constants::TURBO) - this->custom_fan_mode = (std::string) ""; - break; - } - - _debugMsg(F("Climate fan TURBO mode: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.fanTurbo); - - /*************************** MUTE FAN MODE ***************************/ - // MUTE работает в режиме FAN. В режимах HEAT, COOL, HEAT_COOL не работает. DRY не проверял. - // проверку на несовместимые режимы выпилили, т.к. нет уверенности, что это поведение одинаково для всех - switch (_current_ac_state.fanMute) - { - case AC_FANMUTE_ON: - // if (_current_ac_state.mode == AC_MODE_FAN) { - this->custom_fan_mode = Constants::MUTE; - //} - break; - - case AC_FANMUTE_OFF: - default: - if (this->custom_fan_mode == Constants::MUTE) - this->custom_fan_mode = (std::string) ""; - break; - } - - _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 часов при отключении режима - не понятно. - 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; - } - - _debugMsg(F("Climate preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->preset); - - /*************************** CLEAN CUSTOM PRESET ***************************/ - // режим очистки кондиционера, включается (или должен включаться) при AC_POWER_OFF - if (_current_ac_state.clean == AC_CLEAN_ON && - _current_ac_state.power == AC_POWER_OFF) - { - this->custom_preset = Constants::CLEAN; - } - else if (this->custom_preset == Constants::CLEAN) - { - // AC_CLEAN_OFF - // только в том случае, если до этого пресет был установлен - this->custom_preset = (std::string) ""; - } - - _debugMsg(F("Climate CLEAN preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.clean); - - /*************************** ANTIFUNGUS CUSTOM PRESET ***************************/ - // пресет просушки кондиционера после выключения - // По факту: после выключения сплита он оставляет минут на 5 открытые жалюзи и глушит вентилятор. - // Уличный блок при этом гудит и тарахтит. Возможно, прогревается теплообменник для высыхания. - // Через некоторое время внешний блок замолкает и сплит закрывает жалюзи. - // - // Brokly: - // У меня есть на этот режим, конедй реагирует только в выключеном состоянии. Причем пульт шлет - // 5 посылок и при включении и при выключении. Но каких то видимых отличий в работе или в сценарии - // при выключении кондея, я не наблюдаю. На пульте горит пиктограмма этого режима, но просушки - // я не вижу. После выключения , с активированым режимом Anti-FUNGUS, кондей сразу закрывает хлебало - // и затыкается. - // - // GK: оставил возможность включения функции в работающем состоянии, т.к. установка флага должна быть в работающем состоянии, - // а сама функция отработает при выключении сплита. - // У Brokly возможно какие-то особенности кондея. - switch (_current_ac_state.mildew) - { - case AC_MILDEW_ON: - this->custom_preset = Constants::ANTIFUNGUS; - break; - - case AC_MILDEW_OFF: - default: - if (this->custom_preset == Constants::ANTIFUNGUS) - this->custom_preset = (std::string) ""; - break; - } - - _debugMsg(F("Climate ANTIFUNGUS preset: %i"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, _current_ac_state.mildew); - - /*************************** LOUVERs ***************************/ - this->swing_mode = climate::CLIMATE_SWING_OFF; - 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_AUX && _current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN) - { - // TODO: КОСТЫЛЬ! - this->swing_mode = climate::CLIMATE_SWING_VERTICAL; - } - else if (_current_ac_state.louver.louver_h == AC_LOUVERH_OFF_ALTERNATIVE && _current_ac_state.louver.louver_v == AC_LOUVERV_SWING_UPDOWN) - { - // TODO: КОСТЫЛЬ! - // временно сделал так. Сделать нормально - это надо подумать. - // На AUX и многих других марках выключенный режим горизонтальных жалюзи равен 0x20, а на ROVEX и Royal Clima 0xE0 - // Из-за этого происходил сброс на OFF во фронтенде Home Assistant. Пришлось городить это. - // Надо как-то изящнее решить эту историю - 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; - _debugMsg(F("Target temperature: %f"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->target_temperature); - - this->current_temperature = _current_ac_state.temp_ambient; - _debugMsg(F("Room temperature: %f"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, this->current_temperature); - - /*********************************************************************/ - /*************************** PUBLISH STATE ***************************/ - /*********************************************************************/ - this->publish_all_states(); - } - - // публикуем все состояния сенсоров и сплита - void publish_all_states() - { - this->publish_state(); - // температура в комнате - if (sensor_indoor_temperature_ != nullptr) - sensor_indoor_temperature_->publish_state(_current_ac_state.temp_ambient); - // температура уличного блока - if (sensor_outdoor_temperature_ != nullptr) - sensor_outdoor_temperature_->publish_state(_current_ac_state.temp_outdoor); - // температура подводящей магистрали - if (sensor_inbound_temperature_ != nullptr) - sensor_inbound_temperature_->publish_state(_current_ac_state.temp_inbound); - // температура отводящей магистрали - if (sensor_outbound_temperature_ != nullptr) - sensor_outbound_temperature_->publish_state(_current_ac_state.temp_outbound); - // температура странного датчика - if (sensor_compressor_temperature_ != nullptr) - sensor_compressor_temperature_->publish_state(_current_ac_state.temp_compressor); - // мощность инвертора - if (sensor_inverter_power_ != nullptr) - sensor_inverter_power_->publish_state(_current_ac_state.inverter_power); - // флаг режима разморозки - if (sensor_defrost_ != nullptr) - sensor_defrost_->publish_state(_current_ac_state.defrost); - // положение вертикальных жалюзи - if (sensor_vlouver_state_ != nullptr) - sensor_vlouver_state_->publish_state((float)this->getCurrentVlouverFrontendState()); - // флаг включенного ограничения мощности инвертора - if (sensor_inverter_power_limit_state_ != nullptr) - sensor_inverter_power_limit_state_->publish_state(_current_ac_state.power_lim_state == AC_POWLIMSTAT_ON); - // значение ограничения мощности инвертора - if (sensor_inverter_power_limit_value_ != nullptr) - sensor_inverter_power_limit_value_->publish_state(_current_ac_state.power_lim_value); - // сенсор состояния сплита - 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) - { - sensor_display_->publish_state((_current_ac_state.display == AC_DISPLAY_ON) ^ this->get_display_inverted()); - } - } - - // вывод в дебаг текущей конфигурации компонента - void dump_config() - { - ESP_LOGCONFIG(TAG, "AUX HVAC:"); - ESP_LOGCONFIG(TAG, " [x] Firmware version: %s", Constants::AC_FIRMWARE_VERSION.c_str()); - ESP_LOGCONFIG(TAG, " [x] Period: %dms", this->get_period()); - ESP_LOGCONFIG(TAG, " [x] Show action: %s", TRUEFALSE(this->get_show_action())); - ESP_LOGCONFIG(TAG, " [x] Display inverted: %s", TRUEFALSE(this->get_display_inverted())); - ESP_LOGCONFIG(TAG, " [x] Optimistic: %s", TRUEFALSE(this->get_optimistic())); - ESP_LOGCONFIG(TAG, " [x] Packet timeout: %dms", this->get_packet_timeout()); - -#if defined(PRESETS_SAVING) - ESP_LOGCONFIG(TAG, " [x] Save settings %s", TRUEFALSE(this->get_store_settings())); -#endif - - ESP_LOGCONFIG(TAG, " [?] Is inverter %s", millis() > _update_period + 1000 ? YESNO(_is_inverter) : "pending..."); - LOG_SENSOR(" ", "Inverter Power", this->sensor_inverter_power_); - LOG_SENSOR(" ", "Inverter Power Limit Value", this->sensor_inverter_power_limit_value_); - LOG_BINARY_SENSOR(" ", "Inverter Power Limit State", this->sensor_inverter_power_limit_state_); - LOG_SENSOR(" ", "Indoor Temperature", this->sensor_indoor_temperature_); - LOG_SENSOR(" ", "Outdoor Temperature", this->sensor_outdoor_temperature_); - LOG_SENSOR(" ", "Inbound Temperature", this->sensor_inbound_temperature_); - LOG_SENSOR(" ", "Outbound Temperature", this->sensor_outbound_temperature_); - LOG_SENSOR(" ", "Compressor Temperature", this->sensor_compressor_temperature_); - LOG_BINARY_SENSOR(" ", "Defrost Status", this->sensor_defrost_); - LOG_BINARY_SENSOR(" ", "Display", this->sensor_display_); - LOG_TEXT_SENSOR(" ", "Preset Reporter", this->sensor_preset_reporter_); - this->dump_traits_(TAG); - } - - // вызывается пользователем из интерфейса ESPHome или Home Assistant - void control(const esphome::climate::ClimateCall &call) override - { - bool hasCommand = false; - ac_command_t cmd; - - _clearCommand(&cmd); // не забываем очищать, а то будет мусор - - // User requested mode change - if (call.get_mode().has_value()) - { - ClimateMode mode = *call.get_mode(); - - 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; - - case climate::CLIMATE_MODE_COOL: - 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; - - case climate::CLIMATE_MODE_HEAT: - 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; - - case climate::CLIMATE_MODE_HEAT_COOL: - hasCommand = true; - cmd.power = AC_POWER_ON; - cmd.mode = AC_MODE_AUTO; - -#if defined(PRESETS_SAVING) - load_preset(&cmd, POS_MODE_AUTO); -#endif - - this->mode = mode; - break; - - case climate::CLIMATE_MODE_FAN_ONLY: - hasCommand = true; - cmd.power = AC_POWER_ON; - cmd.mode = AC_MODE_FAN; - -#if defined(PRESETS_SAVING) - load_preset(&cmd, POS_MODE_FAN); -#endif - - cmd.sleep = AC_SLEEP_OFF; - this->mode = mode; - break; - - case climate::CLIMATE_MODE_DRY: - 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: // этот режим в будущем можно будет использовать для автоматического пресета (ПИД-регулятора, например) - default: - break; - } - } - - // User requested fan_mode change - if (call.get_fan_mode().has_value()) - { - ClimateFanMode fanmode = *call.get_fan_mode(); - - switch (fanmode) - { - case climate::CLIMATE_FAN_AUTO: - hasCommand = true; - cmd.fanSpeed = AC_FANSPEED_AUTO; - cmd.fanTurbo = AC_FANTURBO_OFF; - cmd.fanMute = AC_FANMUTE_OFF; - this->fan_mode = fanmode; - break; - - case climate::CLIMATE_FAN_LOW: - hasCommand = true; - cmd.fanSpeed = AC_FANSPEED_LOW; - cmd.fanTurbo = AC_FANTURBO_OFF; - cmd.fanMute = AC_FANMUTE_OFF; - this->fan_mode = fanmode; - break; - - case climate::CLIMATE_FAN_MEDIUM: - hasCommand = true; - cmd.fanSpeed = AC_FANSPEED_MEDIUM; - cmd.fanTurbo = AC_FANTURBO_OFF; - cmd.fanMute = AC_FANMUTE_OFF; - this->fan_mode = fanmode; - break; - - case climate::CLIMATE_FAN_HIGH: - hasCommand = true; - cmd.fanSpeed = AC_FANSPEED_HIGH; - cmd.fanTurbo = AC_FANTURBO_OFF; - cmd.fanMute = AC_FANMUTE_OFF; - this->fan_mode = fanmode; - break; - - // другие возможные значения (чтобы не забыть) - // case climate::CLIMATE_FAN_ON: - // case climate::CLIMATE_FAN_OFF: - // case climate::CLIMATE_FAN_MIDDLE: - // case climate::CLIMATE_FAN_FOCUS: - // case climate::CLIMATE_FAN_DIFFUSE: - default: - break; - } - } - else if (call.get_custom_fan_mode().has_value()) - { - std::string customfanmode = *call.get_custom_fan_mode(); - - if (customfanmode == Constants::TURBO) - { - // TURBO fan mode is suitable in COOL and HEAT modes. - // Other modes don't accept TURBO fan mode. - /* - 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) { - - hasCommand = true; - cmd.fanMute = AC_FANMUTE_ON; - cmd.fanTurbo = AC_FANTURBO_OFF; // зависимость от fanmute - this->custom_fan_mode = customfanmode; - //} else { - // _debugMsg(F("MUTE fan mode is suitable in FAN mode only."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - //} - } - } - - // Пользователь выбрал пресет - if (call.get_preset().has_value()) - { - ClimatePreset preset = *call.get_preset(); - - switch (preset) - { - case climate::CLIMATE_PRESET_SLEEP: - // Ночной режим (SLEEP). - // По инструкциям комбинируется только с режимами COOL и HEAT. Автоматически выключается через 7 часов. - // Brokly: вроде как работает еще и с AUTO и DRY - // COOL: температура +1 градус через час, еще через час дополнительные +1 градус, дальше не меняется. - // HEAT: температура -2 градуса через час, еще через час дополнительные -2 градуса, дальше не меняется. - // Восстанавливается ли температура через 7 часов при отключении режима - не понятно. - if (cmd.mode == AC_MODE_COOL or _current_ac_state.mode == AC_MODE_COOL or - cmd.mode == AC_MODE_HEAT or _current_ac_state.mode == AC_MODE_HEAT or - cmd.mode == AC_MODE_DRY or _current_ac_state.mode == AC_MODE_DRY or - cmd.mode == AC_MODE_AUTO 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 custom_preset = *call.get_custom_preset(); - - if (custom_preset == Constants::CLEAN) - { - // режим очистки кондиционера, включается (или должен включаться) при AC_POWER_OFF - // TODO: надо отдебажить выключение этого режима - if (cmd.power == AC_POWER_OFF or _current_ac_state.power == AC_POWER_OFF) - { - hasCommand = true; - cmd.clean = AC_CLEAN_ON; - 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 (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 открытые жалюзи и глушит вентилятор. - // Уличный блок при этом гудит и тарахтит. Возможно, прогревается теплообменник для высыхания. - // Через некоторое время внешний блок замолкает и сплит закрывает жалюзи. - - // Brokly: - // включение-выключение функции "Антиплесень". - // у меня пульт отправляет 5 посылок и на включение и на выключение, но реагирует на эту кнопку - // только в режиме POWER_OFF - - // TODO: надо уточнить, в каких режимах штатно включается этот режим у кондиционера - 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(); - - switch (swingmode) - { - // The protocol allows other combinations for SWING. - // For example "turn the louvers to the desired position or "spread to the sides" / "concentrate in the center". - // But the ROVEX IR-remote does not provide this features. Therefore this features haven't been tested. - // May be suitable for other models of AUX-based ACs. - case climate::CLIMATE_SWING_OFF: - cmd.louver.louver_h = AC_LOUVERH_OFF_ALTERNATIVE; - cmd.louver.louver_v = AC_LOUVERV_OFF; - hasCommand = true; - this->swing_mode = swingmode; - break; - - case climate::CLIMATE_SWING_BOTH: - cmd.louver.louver_h = AC_LOUVERH_SWING_LEFTRIGHT; - cmd.louver.louver_v = AC_LOUVERV_SWING_UPDOWN; - hasCommand = true; - this->swing_mode = swingmode; - break; - - case climate::CLIMATE_SWING_VERTICAL: - cmd.louver.louver_h = AC_LOUVERH_OFF_ALTERNATIVE; - cmd.louver.louver_v = AC_LOUVERV_SWING_UPDOWN; - hasCommand = true; - this->swing_mode = swingmode; - break; - - case climate::CLIMATE_SWING_HORIZONTAL: - cmd.louver.louver_h = AC_LOUVERH_SWING_LEFTRIGHT; - cmd.louver.louver_v = AC_LOUVERV_OFF; - hasCommand = true; - this->swing_mode = swingmode; - break; - } - } - - // User requested target temperature change - if (call.get_target_temperature().has_value()) - { - // выставлять температуру в режиме 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); - if (this->get_optimistic()) - { - this->publish_all_states(); // Publish updated state - } - -#if defined(PRESETS_SAVING) - // флаг отправки новой команды, для процедуры сохранения пресетов, если есть настройка - _new_command_set = _store_settings; -#endif - } - } - - // как оказалось сюда обращаются каждый раз для получения любого параметра - // по этому имеет смысл держать готовый объект - esphome::climate::ClimateTraits traits() override - { - return _traits; - } - - // запрос маленького пакета статуса кондиционера - bool getStatusSmall() - { - // нет смысла в последовательности, если нет коннекта с кондиционером - if (!get_has_connection()) - { - _debugMsg(F("getStatusSmall: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - return false; - } - // есть ли место на запрос в последовательности команд? - if (_getFreeSequenceSpace() < 2) - { - _debugMsg(F("getStatusSmall: not enough space in command sequence. Sequence steps doesn't loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - - /*************************************** getSmallInfo request ***********************************************/ - if (!_addSequenceFuncStep(&AirCon::sq_requestSmallStatus)) - { - _debugMsg(F("getStatusSmall: getSmallInfo request sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - /*************************************** getSmallInfo control ***********************************************/ - if (!_addSequenceFuncStep(&AirCon::sq_controlSmallStatus)) - { - _debugMsg(F("getStatusSmall: getSmallInfo control sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - /**************************************************************************************/ - - _debugMsg(F("getStatusSmall: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - return true; - } - - // запрос большого пакета статуса кондиционера - bool getStatusBig() - { - // нет смысла в последовательности, если нет коннекта с кондиционером - if (!get_has_connection()) - { - _debugMsg(F("getStatusBig: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - return false; - } - // есть ли место на запрос в последовательности команд? - if (_getFreeSequenceSpace() < 2) - { - _debugMsg(F("getStatusBig: not enough space in command sequence. Sequence steps doesn't loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - - /*************************************** getBigInfo request ***********************************************/ - if (!_addSequenceFuncStep(&AirCon::sq_requestBigStatus)) - { - _debugMsg(F("getStatusBig: getBigInfo request sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - /*************************************** getBigInfo control ***********************************************/ - if (!_addSequenceFuncStep(&AirCon::sq_controlBigStatus)) - { - _debugMsg(F("getStatusBig: getBigInfo control sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - /**************************************************************************************/ - - _debugMsg(F("getStatusBig: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - return true; - } - - // запрос большого и малого пакетов статуса последовательно - bool getStatusBigAndSmall() - { - // нет смысла в последовательности, если нет коннекта с кондиционером - if (!get_has_connection()) - { - _debugMsg(F("getStatusBigAndSmall: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - return false; - } - - if (!getStatusSmall()) - { - _debugMsg(F("getStatusBigAndSmall: error with small status sequence."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - - if (!getStatusBig()) - { - _debugMsg(F("getStatusBigAndSmall: error with big status sequence."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - - _debugMsg(F("getStatusBigAndSmall: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - return true; - } - - /** стартовая последовательность пакетов - * - * нужна, чтобы не ждать долго обновления статуса кондиционера - * запускаем сразу, как только удалось подключиться к кондиционеру и прошел первый пинг-пакет - * возвращаемое значение будет присвоено флагу выполнения последовательности - * то есть при возврате false последовательность считается не запущенной и будет вызоваться до тех пор, пока не вернет true - **/ - bool startupSequence() - { - // нет смысла в последовательности, если нет коннекта с кондиционером - if (!get_has_connection()) - { - _debugMsg(F("startupSequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - return false; - } - - // по сути на старте надо получить от кондиционера два статуса - if (!getStatusBigAndSmall()) - { - _debugMsg(F("startupSequence: error with big&small status sequence."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - }; - - _debugMsg(F("startupSequence: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - return true; - } - - /** загружает на выполнение команду - * - * стандартная последовательность - это запрос маленького статусного пакета, выполнение команды и повторный запрос - * такого же статуса для проверки, что всё включилось, ну и для обновления интерфейсов всяких связанных компонентов - **/ - bool commandSequence(ac_command_t *cmd) - { - // нет смысла в последовательности, если нет коннекта с кондиционером - if (!get_has_connection()) - { - _debugMsg(F("commandSequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - return false; - } - - // добавление начального запроса маленького статусного пакета в последовательность команд - if (!getStatusSmall()) - { - _debugMsg(F("commandSequence: error with first small status sequence."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - - // есть ли место на запрос в последовательности команд? - if (_getFreeSequenceSpace() < 2) - { - _debugMsg(F("commandSequence: not enough space in command sequence. Sequence steps doesn't loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - - /*************************************** set params request ***********************************************/ - if (!_addSequenceFuncStep(&AirCon::sq_requestDoCommand, cmd)) - { - _debugMsg(F("commandSequence: request sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - /*************************************** set params control ***********************************************/ - if (!_addSequenceFuncStep(&AirCon::sq_controlDoCommand)) - { - _debugMsg(F("commandSequence: control sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - /**************************************************************************************/ - - // добавление финального запроса маленького статусного пакета в последовательность команд - if (!getStatusSmall()) - { - _debugMsg(F("commandSequence: error with last small status sequence."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - - _debugMsg(F("commandSequence: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - return true; - } - - // загружает на выполнение последовательность команд на включение/выключение - bool powerSequence(ac_power pwr = AC_POWER_ON) - { - // нет смысла в последовательности, если нет коннекта с кондиционером - if (!get_has_connection()) - { - _debugMsg(F("powerSequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - return false; - } - if (pwr == AC_POWER_UNTOUCHED) - return false; // выходим, чтобы не тратить время - - // формируем команду - ac_command_t cmd; - _clearCommand(&cmd); // не забываем очищать, а то будет мусор - cmd.power = pwr; - // добавляем команду в последовательность - if (!commandSequence(&cmd)) - return false; - - _debugMsg(F("powerSequence: loaded (power = %02X)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, pwr); - return true; - } - - // выключает экран - bool displayOffSequence() - { - ac_display dsp = AC_DISPLAY_OFF; - if (this->get_display_inverted()) - dsp = AC_DISPLAY_ON; - return _displaySequence(dsp); - } - - // включает экран - bool displayOnSequence() - { - ac_display dsp = AC_DISPLAY_ON; - if (this->get_display_inverted()) - dsp = AC_DISPLAY_OFF; - return _displaySequence(dsp); - } - - // отправляет сплиту заданный набор байт - // Перед отправкой: - // устанавливает первый байт в 0xBB - // проверяет, чтобы длина тела пакета в заголовке не превышала длину буфера - // рассчитывает и записывает в конец пакета CRC - bool sendTestPacket(const std::vector &data) - { - 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); - - // копируем данные в пакет - 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; - // - установим длину тела, если она больше возможной для нашего буфера - 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]); - _outTestPacket.bytesLoaded = AC_HEADER_SIZE + _outTestPacket.header->body_length + 2; - - // рассчитываем и записываем в пакет CRC - _outTestPacket.crc = (packet_crc_t *)&(_outTestPacket.data[AC_HEADER_SIZE + _outTestPacket.header->body_length]); - _setCRC16(&_outTestPacket); - - _debugMsg(F("sendTestPacket: test packet loaded:"), ESPHOME_LOG_LEVEL_WARN, __LINE__); - _debugPrintPacket(&_outTestPacket, ESPHOME_LOG_LEVEL_WARN, __LINE__); - - // ниже блок добавления отправки пакета в последовательность команд - //***************************************************************** - // есть ли место на запрос в последовательности команд? - if (_getFreeSequenceSpace() < 1) - { - _debugMsg(F("sendTestPacket: not enough space in command sequence. Sequence steps doesn't loaded."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - - /*************************************** sendTestPacket request ***********************************************/ - if (!_addSequenceFuncStep(&AirCon::sq_requestTestPacket)) - { - _debugMsg(F("sendTestPacket: sendTestPacket request sequence step fail."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - /**************************************************************************************/ - - _debugMsg(F("sendTestPacket: loaded to sequence"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__); - - return true; - } - - // устанавливает ограничение мощности сплита на нужный уровень - bool powerLimitationSetSequence(uint8_t power_limit, bool set_on=false) - { - // нет смысла в последовательности, если нет коннекта с кондиционером - if (!get_has_connection()) - { - _debugMsg(F("powerLimitationSetSequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - return false; - } - - if (!this->_is_inverter) - { // если кондиционер не инверторный, то выходим - _debugMsg(F("powerLimitationSetSequence: unsupported for noninverter AC."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - - if(power_limit != this->_power_limitation_value_normalise(power_limit)) - { - _debugMsg(F("powerLimitationSetSequence: incorrect power limit value."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; - } - - // формируем команду - ac_command_t cmd; - _clearCommand(&cmd); // не забываем очищать, а то будет мусор - cmd.power_lim_value = power_limit; - if (set_on) - { - cmd.power_lim_state = AC_POWLIMSTAT_ON; - } - // добавляем команду в последовательность - if (!commandSequence(&cmd)) - return false; - - if (set_on) - { - _debugMsg(F("powerLimitationSetSequence: loaded (state = %02X, power limit = %02X)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, cmd.power_lim_state, power_limit); - } else { - _debugMsg(F("powerLimitationSetSequence: loaded (power limit = %02X)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, power_limit); - } - return true; - } - - // включает/выключает ограничение мощности сплита - bool powerLimitationOnOffSequence(bool enable_limit) - { - // нет смысла в последовательности, если нет коннекта с кондиционером - if (!get_has_connection()) - { - _debugMsg(F("powerLimitationOnOffSequence: no pings from HVAC. It seems like no AC connected."), ESPHOME_LOG_LEVEL_ERROR, __LINE__); - return false; - } - - if (!this->_is_inverter) - { - _debugMsg(F("powerLimitationOnSequence: unsupported for noninverter AC."), ESPHOME_LOG_LEVEL_WARN, __LINE__); - return false; // если кондиционер не инверторный, то выходим - } - - // формируем команду - ac_command_t cmd; - _clearCommand(&cmd); // не забываем очищать, а то будет мусор - if(enable_limit){ - cmd.power_lim_state = AC_POWLIMSTAT_ON; // включить ограничение мощности - } else { - cmd.power_lim_state = AC_POWLIMSTAT_OFF; // отключить ограничение мощности - } - // добавляем команду в последовательность - if (!commandSequence(&cmd)) - return false; - - _debugMsg(F("powerLimitationOnOffSequence: loaded (state = %02X)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, cmd.power_lim_state); - return true; - } - - // включает ограничение мощности сплита - bool powerLimitationOnSequence() - { - return powerLimitationOnOffSequence(true); - } - - // включает ограничение мощности сплита на нужный уровень - bool powerLimitationOnSequence(uint8_t power_limit) - { - return powerLimitationSetSequence(power_limit, true); - } - - // выключает ограничение мощности сплита - bool powerLimitationOffSequence() - { - return powerLimitationOnOffSequence(false); - } - - // конвертирует состояние жалюзи из кодов сплита в коды для фронтенда - ac_vlouver_frontend AUXvlouverToVlouverFrontend(const ac_louver_V vLouver) - { - switch (vLouver) - { - case AC_LOUVERV_SWING_UPDOWN: - return AC_VLOUVER_FRONTEND_SWING; - - case AC_LOUVERV_OFF: - return AC_VLOUVER_FRONTEND_STOP; - - case AC_LOUVERV_SWING_TOP: - return AC_VLOUVER_FRONTEND_TOP; - - case AC_LOUVERV_SWING_MIDDLE_ABOVE: - return AC_VLOUVER_FRONTEND_MIDDLE_ABOVE; - - case AC_LOUVERV_SWING_MIDDLE: - return AC_VLOUVER_FRONTEND_MIDDLE; - - case AC_LOUVERV_SWING_MIDDLE_BELOW: - return AC_VLOUVER_FRONTEND_MIDDLE_BELOW; - - case AC_LOUVERV_SWING_BOTTOM: - return AC_VLOUVER_FRONTEND_BOTTOM; - - default: - _debugMsg(F("AUXvlouverToVlouverFrontend: unknown vertical louver state = %u"), ESPHOME_LOG_LEVEL_DEBUG, __LINE__, _current_ac_state.louver.louver_v); - return AC_VLOUVER_FRONTEND_STOP; - } - } - - // возвращает текущее положение шторок в кодах для фронтенда - ac_vlouver_frontend getCurrentVlouverFrontendState() - { - return AUXvlouverToVlouverFrontend(_current_ac_state.louver.louver_v); - } - - // конвертирует состояние жалюзи из кодов для фронтенда в коды сплита - ac_louver_V vlouverFrontendToAUXvlouver(const ac_vlouver_frontend vLouver) - { - switch (vLouver) - { - case AC_VLOUVER_FRONTEND_SWING: - return AC_LOUVERV_SWING_UPDOWN; - - case AC_VLOUVER_FRONTEND_STOP: - return AC_LOUVERV_OFF; - - case AC_VLOUVER_FRONTEND_TOP: - return AC_LOUVERV_SWING_TOP; - - case AC_VLOUVER_FRONTEND_MIDDLE_ABOVE: - return AC_LOUVERV_SWING_MIDDLE_ABOVE; - - case AC_VLOUVER_FRONTEND_MIDDLE: - return AC_LOUVERV_SWING_MIDDLE; - - case AC_VLOUVER_FRONTEND_MIDDLE_BELOW: - return AC_LOUVERV_SWING_MIDDLE_BELOW; - - case AC_VLOUVER_FRONTEND_BOTTOM: - return AC_LOUVERV_SWING_BOTTOM; - - default: - _debugMsg(F("vlouverFrontendToAUXvlouver: unknown vertical louver state = %u"), ESPHOME_LOG_LEVEL_DEBUG, __LINE__, _current_ac_state.louver.louver_v); - return AC_LOUVERV_OFF; - } - } - - // устанавливает жалюзи в нужное положение по коду сплита - 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 (vLouver = %02X)"), ESPHOME_LOG_LEVEL_VERBOSE, __LINE__, vLouver); - return true; - } - - // устанавливает жалюзи в нужное положение по коду для фронтенда - bool setVLouverFrontendSequence(const ac_vlouver_frontend vLouver) - { - return setVLouverSequence(vlouverFrontendToAUXvlouver(vLouver)); - } - - // установка жалюзи в определенные положения - 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; } - - void set_show_action(bool show_action) { this->_show_action = show_action; } - bool get_show_action() { return this->_show_action; } - - void set_display_inverted(bool display_inverted) { this->_display_inverted = display_inverted; } - bool get_display_inverted() { return this->_display_inverted; } - - void set_packet_timeout(uint32_t ms) - { - if (ms < Constants::AC_PACKET_TIMEOUT_MIN) - ms = Constants::AC_PACKET_TIMEOUT_MIN; - if (ms > Constants::AC_PACKET_TIMEOUT_MAX) - ms = Constants::AC_PACKET_TIMEOUT_MIN; - this->_packet_timeout = ms; - } - uint32_t get_packet_timeout() { return this->_packet_timeout; } - - void set_optimistic(bool optimistic) { this->_optimistic = optimistic; } - bool get_optimistic() { return this->_optimistic; } - - // возможно функции get и не нужны, но вроде как должны быть - void set_supported_modes(const std::set &modes) { this->_supported_modes = modes; } - std::set get_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: - // находимся в процессе получения пакета, никакие отправки в этом состоянии невозможны - _doReceivingPacketState(); - break; - - case ACSM_PARSING_PACKET: - // разбираем полученный пакет - _doParsingPacket(); - break; - - case ACSM_SENDING_PACKET: - // отправляем пакет сплиту - _doSendingPacketState(); - break; - - case ACSM_IDLE: // ничего не делаем, ждем, на что бы среагировать - default: // если состояние какое-то посторонее, то считаем, что IDLE - _doIdleState(); - break; - } - - // раз в заданное количество миллисекунд запрашиваем обновление статуса кондиционера - if ((millis() - _dataMillis) > _update_period) - { - _dataMillis = millis(); - - // обычный wifi-модуль запрашивает маленький пакет статуса - // но нам никто не мешает запрашивать и большой и маленький, чтобы чаще обновлять комнатную температуру - // делаем этот запрос только в случае, если есть коннект с кондиционером - if (get_has_connection()) - getStatusBigAndSmall(); - } - }; - }; - - } // namespace aux_ac -} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/climate.py b/components/aux_ac/climate.py index a9699b2..d4e5377 100644 --- a/components/aux_ac/climate.py +++ b/components/aux_ac/climate.py @@ -38,30 +38,43 @@ CODEOWNERS = ["@GrKoR"] DEPENDENCIES = ["climate", "uart"] AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor"] -CONF_SHOW_ACTION = "show_action" +CONF_SHOW_ACTION_DEPRICATED = "show_action" +CONF_INDOOR_TEMPERATURE_DEPRICATED = "indoor_temperature" +CONF_INBOUND_TEMPERATURE_DEPRICATED = "inbound_temperature" +CONF_OUTDOOR_TEMPERATURE_DEPRICATED = "outdoor_temperature" +CONF_OUTBOUND_TEMPERATURE_DEPRICATED = "outbound_temperature" +CONF_COMPRESSOR_TEMPERATURE_DEPRICATED = "compressor_temperature" -CONF_INDOOR_TEMPERATURE = "indoor_temperature" -CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" -ICON_OUTDOOR_TEMPERATURE = "mdi:home-thermometer-outline" +CONF_INDOOR_AMBIENT_TEMPERATURE = "indoor_ambient_temperature" -CONF_INBOUND_TEMPERATURE = "inbound_temperature" -ICON_INBOUND_TEMPERATURE = "mdi:thermometer-plus" +CONF_INDOOR_COIL_TEMPERATURE = "indoor_coil_temperature" +ICON_INDOOR_COIL_TEMPERATURE = "mdi:thermometer-plus" -CONF_OUTBOUND_TEMPERATURE = "outbound_temperature" -ICON_OUTBOUND_TEMPERATURE = "mdi:thermometer-minus" +CONF_OUTDOOR_AMBIENT_TEMPERATURE = "outdoor_ambient_temperature" +ICON_OUTDOOR_AMBIENT_TEMPERATURE = "mdi:home-thermometer-outline" -CONF_COMPRESSOR_TEMPERATURE = "compressor_temperature" -ICON_COMPRESSOR_TEMPERATURE = "mdi:thermometer-lines" +CONF_OUTDOOR_CONDENSER_TEMPERATURE = "outdoor_condenser_temperature" +ICON_OUTDOOR_CONDENSER_TEMPERATURE = "mdi:thermometer-minus" + +CONF_DEFROST_TEMPERATURE = "defrost_temperature" +ICON_DEFROST_TEMPERATURE = "mdi:thermometer-lines" + +CONF_COMPRESSOR_DISCHARGE_TEMPERATURE = "compressor_discharge_temperature" +ICON_COMPRESSOR_DISCHARGE_TEMPERATURE = "mdi:thermometer-lines" + +CONF_COMPRESSOR_SUCTION_TEMPERATURE = "compressor_suction_temperature" +ICON_COMPRESSOR_SUCTION_TEMPERATURE = "mdi:thermometer-lines" CONF_DISPLAY_STATE = "display_state" +ICON_DISPLAY_STATE = "mdi:clock-digital" + CONF_INVERTER_POWER = "inverter_power" CONF_INVERTER_POWER_DEPRICATED = "invertor_power" CONF_DEFROST_STATE = "defrost_state" -ICON_DEFROST = "mdi:snowflake-melt" +ICON_DEFROST_STATE = "mdi:snowflake-melt" CONF_DISPLAY_INVERTED = "display_inverted" -ICON_DISPLAY = "mdi:clock-digital" CONF_PRESET_REPORTER = "preset_reporter" ICON_PRESET_REPORTER = "mdi:format-list-group" @@ -76,52 +89,45 @@ CONF_INVERTER_POWER_LIMIT_STATE = "inverter_power_limit_state" ICON_INVERTER_POWER_LIMIT_STATE = "mdi:meter-electric-outline" -aux_ac_ns = cg.esphome_ns.namespace("aux_ac") +aux_ac_ns = cg.esphome_ns.namespace("aux_airconditioner") AirCon = aux_ac_ns.class_("AirCon", climate.Climate, cg.Component) -Capabilities = aux_ac_ns.namespace("Constants") +Capabilities = aux_ac_ns.namespace("Capabilities") # Display actions -AirConDisplayOffAction = aux_ac_ns.class_("AirConDisplayOffAction", automation.Action) -AirConDisplayOnAction = aux_ac_ns.class_("AirConDisplayOnAction", automation.Action) - -# test packet action -AirConSendTestPacketAction = aux_ac_ns.class_( - "AirConSendTestPacketAction", automation.Action -) +AirConDisplayOffAction = aux_ac_ns.class_( + "AirConDisplayOffAction", automation.Action) +AirConDisplayOnAction = aux_ac_ns.class_( + "AirConDisplayOnAction", automation.Action) # vertical louvers actions AirConVLouverSwingAction = aux_ac_ns.class_( - "AirConVLouverSwingAction", automation.Action -) -AirConVLouverStopAction = aux_ac_ns.class_("AirConVLouverStopAction", automation.Action) -AirConVLouverTopAction = aux_ac_ns.class_("AirConVLouverTopAction", automation.Action) + "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 -) + "AirConVLouverMiddleAboveAction", automation.Action) AirConVLouverMiddleAction = aux_ac_ns.class_( - "AirConVLouverMiddleAction", automation.Action -) + "AirConVLouverMiddleAction", automation.Action) AirConVLouverMiddleBelowAction = aux_ac_ns.class_( - "AirConVLouverMiddleBelowAction", automation.Action -) + "AirConVLouverMiddleBelowAction", automation.Action) AirConVLouverBottomAction = aux_ac_ns.class_( - "AirConVLouverBottomAction", automation.Action -) + "AirConVLouverBottomAction", automation.Action) AirConVLouverSetAction = aux_ac_ns.class_( - "AirConVLouverSetAction", automation.Action -) + "AirConVLouverSetAction", automation.Action) # power limitation actions AirConPowerLimitationOffAction = aux_ac_ns.class_( - "AirConPowerLimitationOffAction", automation.Action -) + "AirConPowerLimitationOffAction", automation.Action) AirConPowerLimitationOnAction = aux_ac_ns.class_( - "AirConPowerLimitationOnAction", automation.Action -) + "AirConPowerLimitationOnAction", automation.Action) + + +AC_PACKET_TIMEOUT_MIN = 300 +AC_PACKET_TIMEOUT_MAX = 800 -AC_PACKET_TIMEOUT_MIN = 150 -AC_PACKET_TIMEOUT_MAX = 600 def validate_packet_timeout(value): minV = AC_PACKET_TIMEOUT_MIN maxV = AC_PACKET_TIMEOUT_MAX @@ -132,6 +138,8 @@ def validate_packet_timeout(value): AC_POWER_LIMIT_MIN = 30 AC_POWER_LIMIT_MAX = 100 + + def validate_power_limit_range(value): minV = AC_POWER_LIMIT_MIN maxV = AC_POWER_LIMIT_MAX @@ -162,27 +170,21 @@ ALLOWED_CLIMATE_SWING_MODES = { validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True) CUSTOM_FAN_MODES = { - "MUTE": Capabilities.MUTE, - "TURBO": Capabilities.TURBO, + "MUTE": Capabilities.CUSTOM_FAN_MODE_MUTE, + "TURBO": Capabilities.CUSTOM_FAN_MODE_TURBO, } validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True) CUSTOM_PRESETS = { - "CLEAN": Capabilities.CLEAN, - "HEALTH": Capabilities.HEALTH, - "ANTIFUNGUS": Capabilities.ANTIFUNGUS, + "CLEAN": Capabilities.CUSTOM_PRESET_CLEAN, + "HEALTH": Capabilities.CUSTOM_PRESET_HEALTH, + "ANTIFUNGUS": Capabilities.CUSTOM_PRESET_ANTIFUNGUS, } validate_custom_presets = cv.enum(CUSTOM_PRESETS, upper=True) -def validate_raw_data(value): - if isinstance(value, list): - return cv.Schema([cv.hex_uint8_t])(value) - raise cv.Invalid("data must be a list of bytes") - - def output_info(config): - """_LOGGER.info(config.items())""" + # _LOGGER.info(config.items()) return config @@ -191,12 +193,15 @@ CONFIG_SCHEMA = cv.All( { cv.GenerateID(): cv.declare_id(AirCon), cv.Optional(CONF_PERIOD, default="7s"): cv.time_period, - cv.Optional(CONF_SHOW_ACTION, default="true"): cv.boolean, + # cv.Optional(CONF_SHOW_ACTION, default="true"): cv.boolean, + cv.Optional(CONF_SHOW_ACTION_DEPRICATED): cv.invalid( + f"Parameter '{CONF_SHOW_ACTION_DEPRICATED}' was deleted in v.1.0.0. Update your config please." + ), cv.Optional(CONF_DISPLAY_INVERTED, default="false"): cv.boolean, cv.Optional(CONF_TIMEOUT, default=AC_PACKET_TIMEOUT_MIN): validate_packet_timeout, cv.Optional(CONF_OPTIMISTIC, default="true"): cv.boolean, cv.Optional(CONF_INVERTER_POWER_DEPRICATED): cv.invalid( - "The name of sensor was changed in v.0.2.9 from 'invertor_power' to 'inverter_power'. Update your config please." + f"The name of sensor was changed in v.0.2.9 from '{CONF_INVERTER_POWER_DEPRICATED}' to '{CONF_INVERTER_POWER}'. Update your config please." ), cv.Optional(CONF_INVERTER_POWER): sensor.sensor_schema( unit_of_measurement=UNIT_PERCENT, @@ -210,7 +215,10 @@ CONFIG_SCHEMA = cv.All( } ), - cv.Optional(CONF_INDOOR_TEMPERATURE): sensor.sensor_schema( + cv.Optional(CONF_INDOOR_TEMPERATURE_DEPRICATED): cv.invalid( + f"Parameter '{CONF_INDOOR_TEMPERATURE_DEPRICATED}' was deleted in v.1.0.0, use '{CONF_INDOOR_AMBIENT_TEMPERATURE}' instead." + ), + cv.Optional(CONF_INDOOR_AMBIENT_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, icon=ICON_THERMOMETER, accuracy_decimals=1, @@ -221,9 +229,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, } ), - cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema( + + cv.Optional(CONF_INBOUND_TEMPERATURE_DEPRICATED): cv.invalid( + f"Parameter '{CONF_INBOUND_TEMPERATURE_DEPRICATED}' was deleted in v.1.0.0, use '{CONF_INDOOR_COIL_TEMPERATURE}' instead." + ), + cv.Optional(CONF_INDOOR_COIL_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, - icon=ICON_OUTDOOR_TEMPERATURE, + icon=ICON_INDOOR_COIL_TEMPERATURE, accuracy_decimals=0, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, @@ -232,9 +244,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, } ), - cv.Optional(CONF_INBOUND_TEMPERATURE): sensor.sensor_schema( + + cv.Optional(CONF_OUTDOOR_TEMPERATURE_DEPRICATED): cv.invalid( + f"Parameter '{CONF_OUTDOOR_TEMPERATURE_DEPRICATED}' was deleted in v.1.0.0, use '{CONF_OUTDOOR_AMBIENT_TEMPERATURE}' instead." + ), + cv.Optional(CONF_OUTDOOR_AMBIENT_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, - icon=ICON_INBOUND_TEMPERATURE, + icon=ICON_OUTDOOR_AMBIENT_TEMPERATURE, accuracy_decimals=0, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, @@ -243,9 +259,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, } ), - cv.Optional(CONF_OUTBOUND_TEMPERATURE): sensor.sensor_schema( + + cv.Optional(CONF_OUTDOOR_CONDENSER_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, - icon=ICON_OUTBOUND_TEMPERATURE, + icon=ICON_OUTDOOR_CONDENSER_TEMPERATURE, accuracy_decimals=0, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, @@ -254,9 +271,13 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, } ), - cv.Optional(CONF_COMPRESSOR_TEMPERATURE): sensor.sensor_schema( + + cv.Optional(CONF_COMPRESSOR_TEMPERATURE_DEPRICATED): cv.invalid( + f"Parameter '{CONF_COMPRESSOR_TEMPERATURE_DEPRICATED}' was deleted in v.1.0.0, use '{CONF_COMPRESSOR_DISCHARGE_TEMPERATURE}' instead." + ), + cv.Optional(CONF_COMPRESSOR_DISCHARGE_TEMPERATURE): sensor.sensor_schema( unit_of_measurement=UNIT_CELSIUS, - icon=ICON_COMPRESSOR_TEMPERATURE, + icon=ICON_COMPRESSOR_DISCHARGE_TEMPERATURE, accuracy_decimals=0, device_class=DEVICE_CLASS_TEMPERATURE, state_class=STATE_CLASS_MEASUREMENT, @@ -265,6 +286,34 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, } ), + + cv.Optional(CONF_OUTBOUND_TEMPERATURE_DEPRICATED): cv.invalid( + f"Parameter '{CONF_OUTBOUND_TEMPERATURE_DEPRICATED}' was deleted in v.1.0.0, use '{CONF_COMPRESSOR_SUCTION_TEMPERATURE}' instead." + ), + cv.Optional(CONF_COMPRESSOR_SUCTION_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_COMPRESSOR_SUCTION_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_DEFROST_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_DEFROST_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_VLOUVER_STATE): sensor.sensor_schema( icon=ICON_VLOUVER_STATE, accuracy_decimals=0, @@ -274,14 +323,14 @@ CONFIG_SCHEMA = cv.All( } ), cv.Optional(CONF_DISPLAY_STATE): binary_sensor.binary_sensor_schema( - icon=ICON_DISPLAY, + icon=ICON_DISPLAY_STATE, ).extend( { cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, } ), cv.Optional(CONF_DEFROST_STATE): binary_sensor.binary_sensor_schema( - icon=ICON_DEFROST, + icon=ICON_DEFROST_STATE, ).extend( { cv.Optional(CONF_INTERNAL, default="true"): cv.boolean, @@ -335,77 +384,88 @@ async def to_code(config): await climate.register_climate(var, config) parent = await cg.get_variable(config[CONF_UART_ID]) - cg.add(var.initAC(parent)) + cg.add(var.set_uart(parent)) - if CONF_INDOOR_TEMPERATURE in config: - conf = config[CONF_INDOOR_TEMPERATURE] + if CONF_INDOOR_AMBIENT_TEMPERATURE in config: + conf = config[CONF_INDOOR_AMBIENT_TEMPERATURE] sens = await sensor.new_sensor(conf) - cg.add(var.set_indoor_temperature_sensor(sens)) + cg.add(var.set_sensor_temperature_indoor_ambient(sens)) - if CONF_OUTDOOR_TEMPERATURE in config: - conf = config[CONF_OUTDOOR_TEMPERATURE] + if CONF_INDOOR_COIL_TEMPERATURE in config: + conf = config[CONF_INDOOR_COIL_TEMPERATURE] sens = await sensor.new_sensor(conf) - cg.add(var.set_outdoor_temperature_sensor(sens)) + cg.add(var.set_sensor_temperature_indoor_coil(sens)) - if CONF_OUTBOUND_TEMPERATURE in config: - conf = config[CONF_OUTBOUND_TEMPERATURE] + if CONF_OUTDOOR_AMBIENT_TEMPERATURE in config: + conf = config[CONF_OUTDOOR_AMBIENT_TEMPERATURE] sens = await sensor.new_sensor(conf) - cg.add(var.set_outbound_temperature_sensor(sens)) + cg.add(var.set_sensor_temperature_outdoor_ambient(sens)) - if CONF_INBOUND_TEMPERATURE in config: - conf = config[CONF_INBOUND_TEMPERATURE] + if CONF_OUTDOOR_CONDENSER_TEMPERATURE in config: + conf = config[CONF_OUTDOOR_CONDENSER_TEMPERATURE] sens = await sensor.new_sensor(conf) - cg.add(var.set_inbound_temperature_sensor(sens)) + cg.add(var.set_sensor_temperature_outdoor_condenser_middle(sens)) - if CONF_COMPRESSOR_TEMPERATURE in config: - conf = config[CONF_COMPRESSOR_TEMPERATURE] + if CONF_DEFROST_TEMPERATURE in config: + conf = config[CONF_DEFROST_TEMPERATURE] sens = await sensor.new_sensor(conf) - cg.add(var.set_compressor_temperature_sensor(sens)) + cg.add(var.set_sensor_temperature_outdoor_defrost(sens)) + + if CONF_COMPRESSOR_DISCHARGE_TEMPERATURE in config: + conf = config[CONF_COMPRESSOR_DISCHARGE_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_sensor_temperature_outdoor_discharge(sens)) + + if CONF_COMPRESSOR_SUCTION_TEMPERATURE in config: + conf = config[CONF_COMPRESSOR_SUCTION_TEMPERATURE] + sens = await sensor.new_sensor(conf) + cg.add(var.set_sensor_temperature_outdoor_suction(sens)) if CONF_VLOUVER_STATE in config: conf = config[CONF_VLOUVER_STATE] sens = await sensor.new_sensor(conf) - cg.add(var.set_vlouver_state_sensor(sens)) + cg.add(var.set_sensor_vlouver_state(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)) + cg.add(var.set_sensor_display(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_INVERTER_POWER in config: - conf = config[CONF_INVERTER_POWER] - sens = await sensor.new_sensor(conf) - cg.add(var.set_inverter_power_sensor(sens)) + cg.add(var.set_sensor_defrost_state(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_sensor_preset_reporter(sens)) + + if CONF_INVERTER_POWER in config: + conf = config[CONF_INVERTER_POWER] + sens = await sensor.new_sensor(conf) + cg.add(var.set_sensor_inverter_power(sens)) + if CONF_INVERTER_POWER_LIMIT_VALUE in config: conf = config[CONF_INVERTER_POWER_LIMIT_VALUE] sens = await sensor.new_sensor(conf) - cg.add(var.set_inverter_power_limit_value_sensor(sens)) + cg.add(var.set_sensor_inverter_power_limit_value(sens)) if CONF_INVERTER_POWER_LIMIT_STATE in config: conf = config[CONF_INVERTER_POWER_LIMIT_STATE] sens = await binary_sensor.new_binary_sensor(conf) - cg.add(var.set_inverter_power_limit_state_sensor(sens)) + cg.add(var.set_sensor_inverter_power_limit_state(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])) + # cg.add(var.set_show_action(config[CONF_SHOW_ACTION])) + cg.add(var.set_display_inversion(config[CONF_DISPLAY_INVERTED])) cg.add(var.set_packet_timeout(config[CONF_TIMEOUT])) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) if CONF_SUPPORTED_MODES in config: cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES])) if CONF_SUPPORTED_SWING_MODES in config: - cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES])) + cg.add(var.set_supported_swing_modes( + config[CONF_SUPPORTED_SWING_MODES])) if CONF_SUPPORTED_PRESETS in config: cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) if CONF_CUSTOM_PRESETS in config: @@ -414,27 +474,27 @@ async def to_code(config): cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES])) - DISPLAY_ACTION_SCHEMA = maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(AirCon), } ) + @automation.register_action( "aux_ac.display_off", AirConDisplayOffAction, DISPLAY_ACTION_SCHEMA ) async def display_off_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) + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, parent) + @automation.register_action( "aux_ac.display_on", AirConDisplayOnAction, DISPLAY_ACTION_SCHEMA ) async def display_on_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) - + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, parent) VLOUVER_ACTION_SCHEMA = maybe_simple_id( @@ -443,55 +503,61 @@ VLOUVER_ACTION_SCHEMA = maybe_simple_id( } ) + @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) + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, parent) + @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) + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, parent) + @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) + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, parent) + @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) + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, parent) + @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) + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, parent) + @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) + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, parent) + @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) - + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, parent) VLOUVER_SET_ACTION_SCHEMA = cv.Schema( @@ -501,81 +567,47 @@ VLOUVER_SET_ACTION_SCHEMA = cv.Schema( } ) + @automation.register_action( "aux_ac.vlouver_set", AirConVLouverSetAction, VLOUVER_SET_ACTION_SCHEMA ) async def vlouver_set_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) template_ = await cg.templatable(config[CONF_POSITION], args, int) cg.add(var.set_value(template_)) return var - POWER_LIMITATION_OFF_ACTION_SCHEMA = maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(AirCon), } ) + @automation.register_action( "aux_ac.power_limit_off", AirConPowerLimitationOffAction, POWER_LIMITATION_OFF_ACTION_SCHEMA ) async def power_limit_off_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) + parent = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, parent) - -POWER_LIMITATION_ON_ACTION_SCHEMA = cv.Schema( +POWER_LIMITATION_ON_ACTION_SCHEMA = maybe_simple_id( { cv.Required(CONF_ID): cv.use_id(AirCon), cv.Optional(CONF_LIMIT, default=AC_POWER_LIMIT_MIN): validate_power_limit_range, } ) + @automation.register_action( "aux_ac.power_limit_on", AirConPowerLimitationOnAction, POWER_LIMITATION_ON_ACTION_SCHEMA ) async def power_limit_on_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) + parent = await cg.get_variable(config[CONF_ID]) + var = cg.new_Pvariable(action_id, template_arg, parent) template_ = await cg.templatable(config[CONF_LIMIT], args, int) cg.add(var.set_value(template_)) return var - - - -# ********************************************************************************************************* -# ВАЖНО! Только для инженеров! -# Вызывайте метод aux_ac.send_packet только если понимаете, что делаете! Он не проверяет данные, а передаёт -# кондиционеру всё как есть. Какой эффект получится от передачи кондиционеру рандомных байт, никто не знает. -# Вы действуете на свой страх и риск. -# ********************************************************************************************************* -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, SEND_TEST_PACKET_ACTION_SCHEMA -) -async def send_packet_to_code(config, action_id, template_arg, args): - paren = await cg.get_variable(config[CONF_ID]) - var = cg.new_Pvariable(action_id, template_arg, paren) - - data = config[CONF_DATA] - if isinstance(data, bytes): - data = list(data) - - if cg.is_template(data): - templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8)) - cg.add(var.set_data_template(templ)) - else: - cg.add(var.set_data_static(data)) - - return var diff --git a/components/aux_ac/command_builder.cpp b/components/aux_ac/command_builder.cpp new file mode 100644 index 0000000..f659dbb --- /dev/null +++ b/components/aux_ac/command_builder.cpp @@ -0,0 +1,454 @@ +#include "command_builder.h" +#include "aircon.h" +#include "frame.h" +#include "esphome/core/optional.h" + +namespace esphome +{ + namespace aux_airconditioner + { + + CommandBuilder::CommandBuilder(AirCon &aircon) + { + _aircon = &aircon; + _command_frame = new Frame; + } + + CommandBuilder::~CommandBuilder() + { + delete _command_frame; + } + + CommandBuilder &CommandBuilder::init_new_command(command_type_t command_type) + { + _command_frame->clear(); + + switch (command_type) + { + case COMMAND_TYPE_SET_STATE: + _command_frame->append_data(_aircon->get_last_frame_11().data(), _aircon->get_last_frame_11().size()); + _command_frame->set_frame_dir(FrameDirection::FRAME_DIR_TO_AC).set_frame_type(FrameType::FRAME_TYPE_COMMAND); + _command_frame->set_value(8, COMMAND).set_value(9, FLAG); + break; + + case COMMAND_TYPE_REQUEST_11: + _command_frame->append_data({_command_frame->get_start_byte(), 0x00, FrameType::FRAME_TYPE_COMMAND, FrameDirection::FRAME_DIR_TO_AC, 0x00, 0x00, COMMAND_REQUEST_BODY_LENGTH, 0x00}); + _command_frame->append_data({0x11, FLAG}); + break; + + case COMMAND_TYPE_REQUEST_21: + _command_frame->append_data({_command_frame->get_start_byte(), 0x00, FrameType::FRAME_TYPE_COMMAND, FrameDirection::FRAME_DIR_TO_AC, 0x00, 0x00, COMMAND_REQUEST_BODY_LENGTH, 0x00}); + _command_frame->append_data({0x21, FLAG}); + break; + + case COMMAND_TYPE_NONE: + default: + ESP_LOGW(TAG, "Command type 0x%02X is unsupported", command_type); + break; + } + + _command_frame->update_crc(true); + if (_command_frame->get_frame_state() == FRAME_STATE_OK) + _command_frame->set_frame_time(_aircon->ms()); + return *this; + } + + CommandBuilder &CommandBuilder::init_new_command(ClimateCall &cmd) + { + this->init_new_command(command_type_t::COMMAND_TYPE_SET_STATE); + + if (cmd.get_mode().has_value()) + this->set_climate_mode(*cmd.get_mode()); + + if (cmd.get_fan_mode().has_value()) + this->set_climate_fan_mode(*cmd.get_fan_mode()); + else if (cmd.get_custom_fan_mode().has_value()) + this->set_climate_custom_fan_mode(*cmd.get_custom_fan_mode()); + + if (cmd.get_preset().has_value()) + this->set_climate_preset(*cmd.get_preset()); + else if (cmd.get_custom_preset().has_value()) + this->set_climate_custom_preset(*cmd.get_custom_preset()); + + if (cmd.get_swing_mode().has_value()) + this->set_climate_swing_mode(*cmd.get_swing_mode()); + + if (cmd.get_target_temperature().has_value()) + if (this->_aircon->mode != ClimateMode::CLIMATE_MODE_FAN_ONLY) + this->set_target_temperature(*cmd.get_target_temperature()); + + return *this; + } + + CommandBuilder &CommandBuilder::fill_frame_with_command(Frame &frame) + { + _command_frame->update_crc(true); + if (_command_frame->get_frame_state() != FRAME_STATE_OK) + return *this; + + frame.clear(); + frame.append_data(_command_frame->data(), 8 + _command_frame->get_body_length() + 2, true); + frame.set_frame_time(_command_frame->get_frame_time()); + return *this; + } + + Frame CommandBuilder::get_builder_result() + { + return *_command_frame; + } + + CommandBuilder &CommandBuilder::set_climate_mode(ClimateMode value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + this->set_power(value != ClimateMode::CLIMATE_MODE_OFF); + + if (value == ClimateMode::CLIMATE_MODE_OFF) + return *this; + + this->set_mode(climate_mode_to_ac_mode(value)); + if (value == ClimateMode::CLIMATE_MODE_FAN_ONLY) + { + this->set_sleep_mode(false); + } + else if (value == ClimateMode::CLIMATE_MODE_DRY) + { + this->set_sleep_mode(false); + this->set_fan_turbo(false); + } + + return *this; + } + + CommandBuilder &CommandBuilder::set_climate_fan_mode(ClimateFanMode value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + switch (value) + { + case ClimateFanMode::CLIMATE_FAN_AUTO: + this->set_fan_speed(ac_fanspeed::AC_FANSPEED_AUTO); + this->set_fan_turbo(false); + this->set_fan_mute(false); + break; + + case ClimateFanMode::CLIMATE_FAN_LOW: + this->set_fan_speed(ac_fanspeed::AC_FANSPEED_LOW); + this->set_fan_turbo(false); + this->set_fan_mute(false); + break; + + case ClimateFanMode::CLIMATE_FAN_MEDIUM: + this->set_fan_speed(ac_fanspeed::AC_FANSPEED_MEDIUM); + this->set_fan_turbo(false); + this->set_fan_mute(false); + break; + + case ClimateFanMode::CLIMATE_FAN_HIGH: + this->set_fan_speed(ac_fanspeed::AC_FANSPEED_HIGH); + this->set_fan_turbo(false); + this->set_fan_mute(false); + break; + + // Other possible values should be ignored + case ClimateFanMode::CLIMATE_FAN_ON: + case ClimateFanMode::CLIMATE_FAN_OFF: + case ClimateFanMode::CLIMATE_FAN_MIDDLE: + case ClimateFanMode::CLIMATE_FAN_FOCUS: + case ClimateFanMode::CLIMATE_FAN_DIFFUSE: + default: + break; + } + + return *this; + } + + CommandBuilder &CommandBuilder::set_climate_custom_fan_mode(std::string value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + if (value == Capabilities::CUSTOM_FAN_MODE_TURBO) + { + this->set_fan_turbo(true); + this->set_fan_mute(false); + } + else if (value == Capabilities::CUSTOM_FAN_MODE_MUTE) + { + this->set_fan_turbo(false); + this->set_fan_mute(true); + } + else + { + this->set_fan_turbo(false); + this->set_fan_mute(false); + } + + return *this; + } + + CommandBuilder &CommandBuilder::set_climate_preset(ClimatePreset value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + switch (value) + { + case ClimatePreset::CLIMATE_PRESET_SLEEP: + // SLEEP function works in COOL and HEAT modes. Some air conditioners allow it in AUTO and DRY mode also. + // We ignore this. Trying to enable it in any mode. + this->set_sleep_mode(true); + this->set_health_mode(false); + break; + + case ClimatePreset::CLIMATE_PRESET_NONE: + this->set_health_mode(false); + this->set_sleep_mode(false); + this->set_antifungus_mode(false); + this->set_iClean_mode(false); + break; + + // all other presets are ignored + case ClimatePreset::CLIMATE_PRESET_HOME: + case ClimatePreset::CLIMATE_PRESET_AWAY: + case ClimatePreset::CLIMATE_PRESET_BOOST: + case ClimatePreset::CLIMATE_PRESET_COMFORT: + case ClimatePreset::CLIMATE_PRESET_ECO: + case ClimatePreset::CLIMATE_PRESET_ACTIVITY: + default: + break; + } + + return *this; + } + + CommandBuilder &CommandBuilder::set_climate_custom_preset(std::string value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + if (value == Capabilities::CUSTOM_PRESET_CLEAN) + { + this->set_iClean_mode(true); + this->set_antifungus_mode(false); + } + else if (value == Capabilities::CUSTOM_PRESET_HEALTH) + { + this->set_health_mode(true); + this->set_fan_turbo(false); + this->set_fan_mute(false); + this->set_sleep_mode(false); + } + else if (value == Capabilities::CUSTOM_PRESET_ANTIFUNGUS) + { + this->set_antifungus_mode(true); + this->set_iClean_mode(false); + } + + return *this; + } + + CommandBuilder &CommandBuilder::set_climate_swing_mode(ClimateSwingMode value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + switch (value) + { + case ClimateSwingMode::CLIMATE_SWING_OFF: + this->set_vertical_louver(ac_louver_V::AC_LOUVERV_OFF); + this->set_horizontal_louver(ac_louver_H::AC_LOUVERH_OFF); + break; + + case ClimateSwingMode::CLIMATE_SWING_BOTH: + this->set_vertical_louver(ac_louver_V::AC_LOUVERV_SWING_UPDOWN); + this->set_horizontal_louver(ac_louver_H::AC_LOUVERH_SWING_LEFTRIGHT); + break; + + case ClimateSwingMode::CLIMATE_SWING_VERTICAL: + this->set_vertical_louver(ac_louver_V::AC_LOUVERV_SWING_UPDOWN); + this->set_horizontal_louver(ac_louver_H::AC_LOUVERH_OFF); + break; + + case ClimateSwingMode::CLIMATE_SWING_HORIZONTAL: + this->set_vertical_louver(ac_louver_V::AC_LOUVERV_OFF); + this->set_horizontal_louver(ac_louver_H::AC_LOUVERH_SWING_LEFTRIGHT); + break; + + default: + break; + } + + return *this; + } + + CommandBuilder &CommandBuilder::set_target_temperature(float value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + value = Capabilities::normilize_target_temperature(value); + _command_frame->set_value(10, (uint8_t)(value - 8), 0b1111'1000, 3); + _command_frame->set_bit(12, 7, (value - (uint8_t)(value) >= 0.5)); + return *this; + } + + CommandBuilder &CommandBuilder::set_vertical_louver(ac_louver_V value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_value(10, (uint8_t)value, 0b0000'0111); + return *this; + } + + CommandBuilder &CommandBuilder::set_horizontal_louver(ac_louver_H value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_value(11, (uint8_t)value, 0b1110'0000); + return *this; + } + + CommandBuilder &CommandBuilder::set_fan_speed(ac_fanspeed value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_value(13, (uint8_t)value, 0b1110'0000); + return *this; + } + + CommandBuilder &CommandBuilder::set_fan_turbo(bool value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_bit(14, 6, value); + if (value) + _command_frame->set_bit(14, 7, false); // MUTE off + return *this; + } + + CommandBuilder &CommandBuilder::set_fan_mute(bool value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_bit(14, 7, value); + if (value) + _command_frame->set_bit(14, 6, false); // TURBO off + return *this; + } + + CommandBuilder &CommandBuilder::set_mode(ac_mode value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_value(15, (uint8_t)value, 0b1110'0000); + + return *this; + } + + CommandBuilder &CommandBuilder::set_fahrenheit_temperature(bool value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_bit(15, 1, value); + return *this; + } + + CommandBuilder &CommandBuilder::set_sleep_mode(bool value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_bit(15, 2, value); + return *this; + } + + CommandBuilder &CommandBuilder::set_power(bool value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_bit(18, 5, value); // power + if (value) // iClean should be off in power on mode + _command_frame->set_bit(18, 2, false); // + else // Health function should be off in power down mode + _command_frame->set_bit(18, 1, false); // + + return *this; + } + + CommandBuilder &CommandBuilder::set_iClean_mode(bool value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_bit(18, 2, value); + if (value) // iClean works in power off mode only + _command_frame->set_bit(18, 5, false); + return *this; + } + + CommandBuilder &CommandBuilder::set_health_mode(bool value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_bit(18, 1, value); + if (value) // Health function works in power on mode only + _command_frame->set_bit(18, 5, true); + return *this; + } + + CommandBuilder &CommandBuilder::set_antifungus_mode(bool value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_bit(20, 3, value); + return *this; + } + + CommandBuilder &CommandBuilder::set_display_state(bool value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + _command_frame->set_bit(20, 4, value ^ _aircon->get_display_inversion()); + return *this; + } + + CommandBuilder &CommandBuilder::set_inverter_power_limitation_state(bool enabled) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + if (!_aircon->ac_type_inverter) + return *this; + + _command_frame->set_bit(21, 7, enabled); + return *this; + } + + CommandBuilder &CommandBuilder::set_inverter_power_limitation_value(uint8_t value) + { + if (_command_frame->get_body_length() != COMMAND_SET_BODY_LENGTH) + return *this; + + if (!_aircon->ac_type_inverter) + return *this; + + _command_frame->set_value(21, Capabilities::normilize_inverter_power_limit(value), 0b0111'1111); + return *this; + } + + } // namespace aux_airconditioner +} // namespace esphome diff --git a/components/aux_ac/command_builder.h b/components/aux_ac/command_builder.h new file mode 100644 index 0000000..eff49b9 --- /dev/null +++ b/components/aux_ac/command_builder.h @@ -0,0 +1,71 @@ +#pragma once + +#include "aircon_common.h" +#include "helpers.h" +#include "esphome.h" +#include "esphome/components/climate/climate.h" + +namespace esphome +{ + namespace aux_airconditioner + { + + using esphome::climate::ClimateCall; + using esphome::climate::ClimateFanMode; + using esphome::climate::ClimateMode; + using esphome::climate::ClimatePreset; + using esphome::climate::ClimateSwingMode; + + class AirCon; + class Frame; + + class CommandBuilder + { + private: + static const uint8_t COMMAND = 0x01; + static const uint8_t FLAG = 0x01; + static const uint8_t COMMAND_SET_BODY_LENGTH = 0x0F; + static const uint8_t COMMAND_REQUEST_BODY_LENGTH = 0x02; + + AirCon *_aircon{nullptr}; + Frame *_command_frame{nullptr}; + + public: + CommandBuilder() = delete; + CommandBuilder(AirCon &aircon); + ~CommandBuilder(); + + CommandBuilder &init_new_command(command_type_t command_type = COMMAND_TYPE_SET_STATE); + CommandBuilder &init_new_command(ClimateCall &cmd); + CommandBuilder &fill_frame_with_command(Frame &frame); + Frame get_builder_result(); + + // ESPHome climate setters (high level) + CommandBuilder &set_climate_mode(ClimateMode value); + CommandBuilder &set_climate_fan_mode(ClimateFanMode value); + CommandBuilder &set_climate_custom_fan_mode(std::string value); + CommandBuilder &set_climate_preset(ClimatePreset value); + CommandBuilder &set_climate_custom_preset(std::string value); + CommandBuilder &set_climate_swing_mode(ClimateSwingMode value); + + // basic setters (low level) + CommandBuilder &set_target_temperature(float value); + CommandBuilder &set_vertical_louver(ac_louver_V value); + CommandBuilder &set_horizontal_louver(ac_louver_H value); + CommandBuilder &set_fan_speed(ac_fanspeed value); + CommandBuilder &set_fan_turbo(bool value); + CommandBuilder &set_fan_mute(bool value); + CommandBuilder &set_mode(ac_mode value); + CommandBuilder &set_fahrenheit_temperature(bool value); + CommandBuilder &set_sleep_mode(bool value); + CommandBuilder &set_power(bool value); + CommandBuilder &set_iClean_mode(bool value); + CommandBuilder &set_health_mode(bool value); + CommandBuilder &set_antifungus_mode(bool value); + CommandBuilder &set_display_state(bool value); + CommandBuilder &set_inverter_power_limitation_state(bool enabled); + CommandBuilder &set_inverter_power_limitation_value(uint8_t value); + }; + + } // namespace aux_airconditioner +} // namespace esphome diff --git a/components/aux_ac/frame.cpp b/components/aux_ac/frame.cpp new file mode 100644 index 0000000..0efe9b0 --- /dev/null +++ b/components/aux_ac/frame.cpp @@ -0,0 +1,417 @@ +#include "frame.h" +#include +#include + +namespace esphome +{ + namespace aux_airconditioner + { + + bool Frame::_is_header_loaded() const + { + return this->size() >= Frame::FRAME_HEADER_SIZE; + } + + Frame::crc16_t Frame::_calc_crc(uint8_t data_size) const + { + Frame::crc16_t crc16; + + uint8_t data_length = data_size; + uint8_t corrected_data_length = data_length + (data_length % 2); // data length should be even for crc16 + + uint8_t crc_buffer[corrected_data_length]; + memset(crc_buffer, 0, corrected_data_length); + memcpy(crc_buffer, this->data(), data_length); + + data_length = corrected_data_length; + + uint32_t crc_tmp = 0; + uint16_t *p_u16 = (uint16_t *)crc_buffer; + while (data_length > 0) + { + crc_tmp += *p_u16; + p_u16++; + data_length -= 2; + } + crc_tmp = (crc_tmp >> 16) + (crc_tmp & 0xFFFF); + crc_tmp = ~crc_tmp; + + crc16.crc16 = crc_tmp & 0xFFFF; + return crc16; + } + + FrameState Frame::_set_frame_state(FrameState state) + { + _state = state; + return this->_state; + } + + std::string Frame::_dump_data(const uint8_t *data, uint8_t data_length) + { + if (data == nullptr || data_length == 0) + return ""; + + uint8_t counter = 0; + std::stringstream ss; + ss << std::hex << std::uppercase; + while (counter < data_length) + { + ss << std::setfill('0') << std::setw(2) << (int)*data; + counter++; + data++; + if (counter < data_length) + ss << " "; + } + return ss.str(); + } + + FrameType Frame::get_frame_type() const + { + return (this->_is_header_loaded()) ? (FrameType)this->get_value(Frame::OFFSET_FRAME_TYPE) : (FrameType)0; + } + + Frame &Frame::set_frame_type(FrameType frame_type) + { + if (this->_is_header_loaded()) + this->set_value(Frame::OFFSET_FRAME_TYPE, frame_type); + + return *this; + } + + uint8_t Frame::get_body_length() const + { + return (this->_is_header_loaded()) ? this->get_value(Frame::OFFSET_BODY_LENGTH) : 0; + } + + Frame &Frame::set_body_length(uint8_t body_length) + { + if (this->_is_header_loaded()) + this->set_value(Frame::OFFSET_BODY_LENGTH, body_length); + + return *this; + } + + FrameDirection Frame::get_frame_dir() const + { + return (this->_is_header_loaded()) ? (FrameDirection)this->get_value(Frame::OFFSET_FRAME_DIRECTION) : (FrameDirection)0; + } + + Frame &Frame::set_frame_dir(FrameDirection frame_direction) + { + if (this->_is_header_loaded()) + this->set_value(Frame::OFFSET_FRAME_DIRECTION, frame_direction); + + return *this; + } + + Frame &Frame::set_frame_time(uint32_t time) + { + this->_frame_time = time; + return *this; + } + + Frame &Frame::clear() + { + this->_data.clear(); + this->_frame_time = 0; + this->update_frame_state(); + return *this; + } + + bool Frame::send(UARTComponent &uart) + { + uart.write_array(this->data(), this->size()); + ESP_LOGD(TAG, "%s", this->to_string(true).c_str()); + + return true; + } + + FrameState Frame::load(UARTComponent &uart) + { + if (!this->has_frame_state(FRAME_STATE_PARTIALLY_LOADED)) + this->clear(); + + if (uart.available() == 0) + return this->get_frame_state(); + + uint8_t data_byte = 0; + if (this->has_frame_state(FRAME_STATE_BLANK)) + { + while (uart.available() && + this->has_frame_state(FRAME_STATE_BLANK)) + { + if (!uart.read_byte(&data_byte)) + { + ESP_LOGW(TAG, "uart read error"); + break; + } + + if (data_byte == this->get_start_byte()) + { + this->append_data(data_byte); + this->update_frame_state(); + } + } + } + + while (uart.available() && + this->has_frame_state(FRAME_STATE_PARTIALLY_LOADED)) + { + this->update_frame_state(); + if (this->has_frame_state(FRAME_STATE_OK)) + break; + + if (this->has_frame_state(FRAME_STATE_ERROR)) + { + ESP_LOGW(TAG, "Broken frame received: %s", this->to_string(true).c_str()); + break; + } + + if (!uart.read_byte(&data_byte)) + { + ESP_LOGW(TAG, "UART read error"); + break; + } + + this->append_data(data_byte); + } + + return this->update_frame_state(); + } + + Frame &Frame::append_data(uint8_t data, bool update_state) + { + this->_data.insert(this->_data.end(), data); + if (update_state) + this->update_frame_state(); + return *this; + } + + Frame &Frame::append_data(const uint8_t data, const uint8_t count, bool update_state) + { + this->_data.insert(this->_data.end(), count, data); + if (update_state) + this->update_frame_state(); + return *this; + } + + Frame &Frame::append_data(std::vector data, bool update_state) + { + this->_data.insert(this->_data.end(), data.begin(), data.end()); + if (update_state) + this->update_frame_state(); + return *this; + } + + Frame &Frame::append_data(const uint8_t *data, uint8_t data_length, bool update_state) + { + if (data != nullptr && data_length != 0) + std::copy(data, data + data_length, std::back_inserter(this->_data)); + + if (update_state) + this->update_frame_state(); + return *this; + } + + Frame &Frame::trim_data(uint8_t first_element_index) + { + if (first_element_index < this->size()) + { + this->_data.erase(this->_data.begin() + first_element_index, this->_data.end()); + } + return *this; + } + + Frame &Frame::update_crc(bool update_state) + { + if (!this->_is_header_loaded()) + return *this; + + uint8_t expected_frame_size = Frame::FRAME_HEADER_SIZE + this->get_body_length() + sizeof(crc16_t); + if (this->size() < expected_frame_size - 2 || + this->size() > expected_frame_size) + return *this; + + if (this->size() > expected_frame_size) + { + this->_data.erase(this->_data.begin() + expected_frame_size, this->_data.end()); + } + else + { + this->_data.insert(this->_data.end(), expected_frame_size - this->size(), 0x00); + } + + crc16_t crc = this->_calc_crc(this->size() - sizeof(crc16_t)); + this->_data.erase(this->_data.end() - 2, this->_data.end()); + this->_data.insert(this->_data.end(), {crc.crc[0], crc.crc[1]}); + + if (update_state) + this->update_frame_state(); + return *this; + } + + bool Frame::is_valid_crc() const + { + if (this->size() < 2) + return false; + + crc16_t crc; + memcpy(&crc, &(this->_data.rbegin()[1]), 2); + return this->_calc_crc(this->size() - 2).crc16 == crc.crc16; + } + + bool Frame::get_bit(uint8_t data_index, uint8_t bit_index) const + { + if (bit_index > 7) + return false; + + return get_value(data_index, (1 << bit_index)) >> bit_index == 1; + } + + Frame &Frame::set_bit(uint8_t data_index, uint8_t bit_index, bool value) + { + if (bit_index > 7) + return *this; + + this->set_value(data_index, (value << bit_index), (1 << bit_index)); + return *this; + } + + uint8_t Frame::get_value(uint8_t index, uint8_t mask, uint8_t shift) const + { + if (index >= this->size()) + return 0; + + return (this->_data[index] & mask) >> shift; + } + + Frame &Frame::set_value(uint8_t index, uint8_t value, uint8_t mask, uint8_t shift) + { + if (index >= this->size()) + return *this; + + this->_data[index] &= ~mask; + this->_data[index] |= (value << shift) & mask; + return *this; + } + + bool Frame::get_crc(uint16_t &crc16) const + { + if (this->size() < 2) + return false; + + memcpy(&crc16, &(this->_data.rbegin()[1]), 2); + return true; + } + + bool Frame::get_crc(uint8_t &crc16_1, uint8_t &crc16_2) const + { + if (this->size() < 2) + return false; + + crc16_1 = this->_data.rbegin()[1]; + crc16_2 = this->_data.rbegin()[0]; + return true; + } + + FrameState Frame::update_frame_state() + { + this->_state = FRAME_STATE_ERROR; + if (this->size() == 0) + return this->_set_frame_state(FRAME_STATE_BLANK); + + if (this->_data[0] != this->get_start_byte()) + return this->_set_frame_state(FRAME_STATE_ERROR); + + if (this->size() < Frame::FRAME_HEADER_SIZE) + return this->_set_frame_state(FRAME_STATE_PARTIALLY_LOADED); + + if (this->size() >= Frame::FRAME_HEADER_SIZE) + { + if (this->size() < Frame::FRAME_HEADER_SIZE + this->get_body_length() + sizeof(crc16_t)) + return this->_set_frame_state(FRAME_STATE_PARTIALLY_LOADED); + + if (this->size() > Frame::FRAME_HEADER_SIZE + this->get_body_length() + sizeof(crc16_t)) + return this->_set_frame_state(FRAME_STATE_ERROR); + + if (this->size() == Frame::FRAME_HEADER_SIZE + this->get_body_length() + sizeof(crc16_t)) + { + return this->_set_frame_state(this->is_valid_crc() ? FRAME_STATE_OK : FRAME_STATE_ERROR); + } + } + return this->_state; + } + + std::string Frame::to_string(bool show_time) const + { + std::stringstream ss; + if (show_time) + ss << std::setfill('0') << std::setw(10) << _frame_time << ": "; + + if (this->has_frame_state(FRAME_STATE_OK)) + { + ss << this->direction_to_string() + << "[" << _dump_data(this->data(), Frame::FRAME_HEADER_SIZE) << "] " + << _dump_data(this->data() + Frame::FRAME_HEADER_SIZE, this->get_body_length()) << ((this->get_body_length() != 0) ? " " : "") + << "[" << _dump_data(this->data() + Frame::FRAME_HEADER_SIZE + this->get_body_length(), sizeof(crc16_t)) << "]"; + } + else + { + ss << "[--] " << _dump_data(this->data(), this->size()); + } + + return ss.str(); + } + + std::string Frame::state_to_string() const + { + switch (this->get_frame_state()) + { + case FRAME_STATE_BLANK: + return "blank"; + + case FRAME_STATE_ERROR: + return "error"; + + case FRAME_STATE_PARTIALLY_LOADED: + return "partially loaded"; + + case FRAME_STATE_OK: + return "ok"; + + default: + return "unknown"; + } + } + + std::string Frame::type_to_string() const + { + switch (this->get_frame_type()) + { + case FRAME_TYPE_COMMAND: + return "command"; + + case FRAME_TYPE_INIT: + return "init"; + + case FRAME_TYPE_PING: + return "ping"; + + case FRAME_TYPE_RESPONSE: + return "response"; + + case FRAME_TYPE_STRANGE: + return "strange"; + + default: + return "unknown"; + } + } + + std::string Frame::direction_to_string() const + { + return (this->get_frame_dir() == FRAME_DIR_TO_AC) ? "[=>] " : "[<=] "; + } + + } // namespace aux_ac +} // namespace esphome diff --git a/components/aux_ac/frame.h b/components/aux_ac/frame.h new file mode 100644 index 0000000..2777676 --- /dev/null +++ b/components/aux_ac/frame.h @@ -0,0 +1,110 @@ +#pragma once + +#include +#include +#include +#include +#include "esphome.h" +#include "esphome/components/climate/climate.h" +#include "esphome/components/uart/uart_component.h" + +#include "frame_constants.h" + +namespace esphome +{ + namespace aux_airconditioner + { + + using esphome::uart::UARTComponent; + + // ************************************************************************************************** + class Frame + { + protected: + // Frame header params + static const uint8_t FRAME_HEADER_SIZE = 8; + static const uint8_t OFFSET_START_BYTE = 0; + static const uint8_t OFFSET_FRAME_TYPE = 2; + static const uint8_t OFFSET_FRAME_DIRECTION = 3; + static const uint8_t OFFSET_BODY_LENGTH = 6; + + union crc16_t + { + uint16_t crc16; + uint8_t crc[2]; + } __attribute__((packed)); + static_assert(sizeof(crc16_t) == 2); + + uint32_t _frame_time = 0; + std::vector _data = {}; + FrameState _state = FRAME_STATE_BLANK; + static const uint8_t START_BYTE = 0xBB; + + bool _is_header_loaded() const; + crc16_t _calc_crc(uint8_t data_size) const; + FrameState _set_frame_state(FrameState state); + static std::string _dump_data(const uint8_t *data, uint8_t data_length); + + public: + Frame() : _frame_time(0){}; + Frame(uint32_t time) : _frame_time(time){}; + Frame(uint32_t time, FrameType frame_type, FrameDirection frame_direction) + : _frame_time(time), + _data({START_BYTE, 0x00, frame_type, frame_direction, 0x00, 0x00, 0x00, 0x00}) { this->update_frame_state(); } + Frame(uint32_t time, std::vector data) + : _frame_time(time), + _data(data) { this->update_frame_state(); } + ~Frame() = default; + + static uint8_t get_start_byte() { return Frame::START_BYTE; }; + + bool has_type(FrameType frame_type) const { return get_frame_type() == frame_type; }; + FrameType get_frame_type() const; + Frame &set_frame_type(FrameType frame_type); + + uint8_t get_body_length() const; + Frame &set_body_length(uint8_t body_length); + + FrameDirection get_frame_dir() const; + Frame &set_frame_dir(FrameDirection frame_direction); + + uint32_t get_frame_time() { return this->_frame_time; }; + Frame &set_frame_time(uint32_t time); + + Frame &clear(); + + bool send(UARTComponent &uart); + FrameState load(UARTComponent &uart); + + Frame &append_data(uint8_t data, bool update_state = false); + Frame &append_data(const uint8_t data, const uint8_t count, bool update_state = false); + Frame &append_data(std::vector data, bool update_state = false); + Frame &append_data(const uint8_t *data, uint8_t data_length, bool update_state = false); + Frame &trim_data(uint8_t first_element_index); + Frame &update_crc(bool update_state = false); + bool is_valid_crc() const; + bool is_valid_frame() const { return this->has_frame_state(FRAME_STATE_OK); }; + + bool get_bit(uint8_t data_index, uint8_t bit_index) const; + Frame &set_bit(uint8_t data_index, uint8_t bit_index, bool value); + uint8_t get_value(uint8_t index, uint8_t mask = 255, uint8_t shift = 0) const; + Frame &set_value(uint8_t index, uint8_t value, uint8_t mask = 255, uint8_t shift = 0); + + bool get_crc(uint16_t &crc16) const; + bool get_crc(uint8_t &crc16_1, uint8_t &crc16_2) const; + + FrameState get_frame_state() const { return this->_state; }; + bool has_frame_state(FrameState frame_state) const { return this->get_frame_state() == frame_state; }; + FrameState update_frame_state(); + + const uint8_t *data() const { return this->_data.data(); }; + uint8_t size() const { return this->_data.size(); }; + + std::string to_string(bool show_time = false) const; + std::string state_to_string() const; + std::string type_to_string() const; + std::string direction_to_string() const; + }; + + } // namespace aux_ac +} // namespace esphome diff --git a/components/aux_ac/frame_constants.h b/components/aux_ac/frame_constants.h new file mode 100644 index 0000000..e208fc1 --- /dev/null +++ b/components/aux_ac/frame_constants.h @@ -0,0 +1,34 @@ +#pragma once + +#include + +namespace esphome +{ + namespace aux_airconditioner + { + + enum FrameType : uint8_t + { + FRAME_TYPE_PING = 0x01, + FRAME_TYPE_COMMAND = 0x06, + FRAME_TYPE_RESPONSE = 0x07, + FRAME_TYPE_INIT = 0x09, + FRAME_TYPE_STRANGE = 0x0b, + }; + + enum FrameDirection : uint8_t + { + FRAME_DIR_TO_DONGLE = 0x00, + FRAME_DIR_TO_AC = 0x80, + }; + + enum FrameState : uint8_t + { + FRAME_STATE_BLANK = 0x00, + FRAME_STATE_PARTIALLY_LOADED = 0x01, + FRAME_STATE_OK = 0x0F, + FRAME_STATE_ERROR = 0xFF, + }; + + } // namespace aux_airconditioner +} // namespace esphome diff --git a/components/aux_ac/frame_processor.cpp b/components/aux_ac/frame_processor.cpp new file mode 100644 index 0000000..8bb0f9b --- /dev/null +++ b/components/aux_ac/frame_processor.cpp @@ -0,0 +1,371 @@ +#include "frame_processor.h" + +#include "frame.h" +#include "helpers.h" +#include + +namespace esphome +{ + namespace aux_airconditioner + { + + using esphome::helpers::update_property; + + /*********************************************************************************************\ + \*********************************************************************************************/ + void FrameProcessorInterface::process(const Frame &frame, AirCon &aircon) const + { + if (!this->applicable(frame)) + return; + + if (!aircon.is_hardware_connected()) + return; + + aircon.reset_ping_timeout(); + this->_specific_process(frame, aircon); + } + + /*********************************************************************************************\ + \*********************************************************************************************/ + bool FrameProcessorPing::applicable(const Frame &frame) const + { + return frame.has_type(FrameType::FRAME_TYPE_PING); + } + + FrameType FrameProcessorPing::get_applicable_frame_type() const + { + return FrameType::FRAME_TYPE_PING; + } + + void FrameProcessorPing::_specific_process(const Frame &frame, AirCon &aircon) const + { + aircon.schedule_ping_response(); + } + + /*********************************************************************************************\ + \*********************************************************************************************/ + bool FrameProcessorResponse01::applicable(const Frame &frame) const + { + return frame.has_type(FrameType::FRAME_TYPE_RESPONSE) && + frame.get_body_length() == 0x04 && + frame.get_value(9) == 0x01; + } + + FrameType FrameProcessorResponse01::get_applicable_frame_type() const + { + return FrameType::FRAME_TYPE_RESPONSE; + } + + void FrameProcessorResponse01::_specific_process(const Frame &frame, AirCon &aircon) const + { + } + + /*********************************************************************************************\ + \*********************************************************************************************/ + ClimateMode FrameProcessorResponse11::_power_and_mode_to_climate_mode(bool power_on, ac_mode mode) const + { + ClimateMode result = ClimateMode::CLIMATE_MODE_OFF; + if (power_on) + { + switch (mode) + { + case AC_MODE_AUTO: + result = ClimateMode::CLIMATE_MODE_HEAT_COOL; + break; + + case AC_MODE_COOL: + result = ClimateMode::CLIMATE_MODE_COOL; + break; + + case AC_MODE_DRY: + result = ClimateMode::CLIMATE_MODE_DRY; + break; + + case AC_MODE_HEAT: + result = ClimateMode::CLIMATE_MODE_HEAT; + break; + + case AC_MODE_FAN: + result = ClimateMode::CLIMATE_MODE_FAN_ONLY; + break; + + default: + ESP_LOGW(TAG, "Warning: unknown air conditioner mode: 0x%02X", mode); + break; + } + } + + return result; + } + + bool FrameProcessorResponse11::applicable(const Frame &frame) const + { + return frame.has_type(FrameType::FRAME_TYPE_RESPONSE) && + frame.get_body_length() == 0x0F && + frame.get_value(9) == 0x11; + } + + FrameType FrameProcessorResponse11::get_applicable_frame_type() const + { + return FrameType::FRAME_TYPE_RESPONSE; + } + + void FrameProcessorResponse11::_specific_process(const Frame &frame, AirCon &aircon) const + { + aircon.set_last_frame(frame); + + bool state_changed = false; + + // target temperature: + // byte 10: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b10 + // byte 12: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b12 + update_property(aircon.target_temperature, (float)(8.0 + (float)frame.get_value(10, 0b1111'1000, 3) + (frame.get_bit(12, 7) ? 0.5 : 0.0)), state_changed); + + // vertical louver state: + // byte 10: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b10 + // horizontal louver state: + // byte 11: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b11 + update_property(aircon.louver_vertical, (ac_louver_V)frame.get_value(10, 0b0000'0111), state_changed); + update_property(aircon.louver_horizontal, (ac_louver_H)frame.get_value(11, 0b1110'0000), state_changed); + if (aircon.louver_vertical == AC_LOUVERV_SWING_UPDOWN && aircon.louver_horizontal != AC_LOUVERH_SWING_LEFTRIGHT) + update_property(aircon.swing_mode, ClimateSwingMode::CLIMATE_SWING_VERTICAL, state_changed); + else if (aircon.louver_vertical != AC_LOUVERV_SWING_UPDOWN && aircon.louver_horizontal == AC_LOUVERH_SWING_LEFTRIGHT) + update_property(aircon.swing_mode, ClimateSwingMode::CLIMATE_SWING_HORIZONTAL, state_changed); + else if (aircon.louver_vertical == AC_LOUVERV_SWING_UPDOWN && aircon.louver_horizontal == AC_LOUVERH_SWING_LEFTRIGHT) + update_property(aircon.swing_mode, ClimateSwingMode::CLIMATE_SWING_BOTH, state_changed); + else if (aircon.louver_vertical != AC_LOUVERV_SWING_UPDOWN && aircon.louver_horizontal != AC_LOUVERH_SWING_LEFTRIGHT) + update_property(aircon.swing_mode, ClimateSwingMode::CLIMATE_SWING_OFF, state_changed); + + // last IR-command was this time ago (minutes) + // byte 12: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b12 + update_property(aircon.last_IR_passed, frame.get_value(12, 0b0011'1111), state_changed); + + // fan speed: + // byte 13: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b13 + update_property(aircon.fan_mode, ac_fanspeed_to_climate_fan_mode((ac_fanspeed)frame.get_value(13, 0b1110'0000)), state_changed); + + // timer activation & delay: + // byte 13: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b13 + // byte 14: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b14 + // byte 18: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b18 + // timer.delay_minutes_uint16 = frame.get_value(13, 0b0001'1111) * 60 + frame.get_value(14, 0b0001'1111); + // timer.enabled_bool = frame.get_bit(18, 6); + // update_property(aircon.???, ???, state_changed); + + // fan TURBO mode: + // byte 14: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b14 + if (frame.get_bit(14, 6)) + { + update_property(aircon.custom_fan_mode, Capabilities::CUSTOM_FAN_MODE_TURBO, state_changed); + } + else if (aircon.custom_fan_mode == Capabilities::CUSTOM_FAN_MODE_TURBO) + { + update_property(aircon.custom_fan_mode, (std::string) "", state_changed); + } + + // fan MUTE mode: + // byte 14: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b14 + if (frame.get_bit(14, 7)) + { + update_property(aircon.custom_fan_mode, Capabilities::CUSTOM_FAN_MODE_MUTE, state_changed); + } + else if (aircon.custom_fan_mode == Capabilities::CUSTOM_FAN_MODE_MUTE) + { + update_property(aircon.custom_fan_mode, (std::string) "", state_changed); + } + + // power & mode: + // byte 15: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b15 + // byte 18: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b18 + update_property(aircon.mode, _power_and_mode_to_climate_mode(frame.get_bit(18, 5), (ac_mode)frame.get_value(15, 0b1110'0000)), state_changed); + + // temperature: Celsius or Fahrenheit + // byte 15: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b15 + if (update_property(aircon.temperature_in_fahrenheit, frame.get_bit(15, 1), state_changed)) + aircon.update_all_sensors_unit_of_measurement(); + + // SLEEP preset: + // byte 15: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b15 + if (frame.get_bit(15, 2)) + { + update_property(aircon.preset, ClimatePreset::CLIMATE_PRESET_SLEEP, state_changed); + } + else if (aircon.preset == ClimatePreset::CLIMATE_PRESET_SLEEP) + { + update_property(aircon.preset, ClimatePreset::CLIMATE_PRESET_NONE, state_changed); + } + + // iFeel function: disabled due to uselessness (and doesn't work with wi-fi) + // byte 15: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b15 + // update_property(aircon.iFeel, frame.get_bit(15, 3), state_changed); + + // iClean function: + // byte 18: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b18 + if (frame.get_bit(18, 2) && !frame.get_bit(18, 5)) // iClean + Power_Off + { + update_property(aircon.custom_preset, Capabilities::CUSTOM_PRESET_CLEAN, state_changed); + } + else if (aircon.custom_preset == Capabilities::CUSTOM_PRESET_CLEAN) + { + update_property(aircon.custom_preset, (std::string) "", state_changed); + } + + // Health function: + // byte 18: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b18 + if (frame.get_bit(18, 1) && frame.get_bit(18, 5)) // Health + Power_On + { + update_property(aircon.custom_preset, Capabilities::CUSTOM_PRESET_HEALTH, state_changed); + } + else if (aircon.custom_preset == Capabilities::CUSTOM_PRESET_HEALTH) + { + update_property(aircon.custom_preset, (std::string) "", state_changed); + } + + // Antifungus function: + // byte 20: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b20 + if (frame.get_bit(20, 3)) + { + update_property(aircon.custom_preset, Capabilities::CUSTOM_PRESET_ANTIFUNGUS, state_changed); + } + else if (aircon.custom_preset == Capabilities::CUSTOM_PRESET_ANTIFUNGUS) + { + update_property(aircon.custom_preset, (std::string) "", state_changed); + } + + // Display: + // byte 20: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b20 + update_property(aircon.display_enabled, (bool)(frame.get_bit(20, 4) ^ aircon.get_display_inversion()), state_changed); + + // Power limitation for inverters: + // byte 21: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_11_b21 + if (aircon.ac_type_inverter) + { + update_property(aircon.inverter_power_limitation_value, frame.get_value(21, 0b0111'1111), state_changed); + update_property(aircon.inverter_power_limitation_on, frame.get_bit(21, 7), state_changed); + } + else + { + aircon.inverter_power_limitation_value.reset(); + aircon.inverter_power_limitation_on.reset(); + } + + if (state_changed) + { + aircon.publish_all_states(); + } + } + + /*********************************************************************************************\ + \*********************************************************************************************/ + bool FrameProcessorResponse2x::applicable(const Frame &frame) const + { + return frame.has_type(FrameType::FRAME_TYPE_RESPONSE) && + frame.get_body_length() == 0x18 && + frame.get_value(9, 0b11110000) == 0x20; + } + + FrameType FrameProcessorResponse2x::get_applicable_frame_type() const + { + return FrameType::FRAME_TYPE_RESPONSE; + } + + void FrameProcessorResponse2x::_specific_process(const Frame &frame, AirCon &aircon) const + { + aircon.set_last_frame(frame); + + bool state_changed = false; + + // TODO: doublecheck the temperature bytes. Probably here is a mess... + + // air conditioner type: + // byte 10: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b10 + update_property(aircon.ac_type_inverter, frame.get_bit(10, 5), state_changed); + + // byte 11: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b11 + + // iClean + defrost + // byte 12: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b12 + update_property(aircon.defrost_enabled, frame.get_bit(12, 5), state_changed); + + // real FAN speed + // byte 13: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b13 + update_property(aircon.real_fan_speed, (ac_fanspeed_real)frame.get_value(13, 0b0000'0111), state_changed); + + // byte 14: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b14 + + // ambient indoor temperature: + // byte 15: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b15 + // byte 31: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b31 + update_property(aircon.current_temperature, (float)(frame.get_value(31, 0b0000'1111) / 10.0 + frame.get_value(15) - 0x20), state_changed); + + // byte 16: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b16 + + // indoor coil temperature: + // byte 17: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b17 + if (frame.get_value(17) >= 0x20) + update_property(aircon.temperature_indoor_coil, (uint8_t)(frame.get_value(17) - 0x20), state_changed); + else + aircon.temperature_indoor_coil.reset(); + + // byte 18: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b18 + // TODO: maybe this is a mess + if (frame.get_value(18) >= 0x20) + update_property(aircon.temperature_outdoor_ambient, (uint8_t)(frame.get_value(18) - 0x20), state_changed); + else + aircon.temperature_outdoor_ambient.reset(); + + // byte 19: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b19 + + // condenser middle temperature sensor: + // byte 20: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b20 + if (frame.get_value(20) >= 0x20) + update_property(aircon.temperature_condenser_middle, (uint8_t)(frame.get_value(20) - 0x20), state_changed); + else + aircon.temperature_condenser_middle.reset(); + + // temperature sensor #2 "PIPE"?: + // byte 21: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b21 + // This byte is equal to 0x20 for inverters without this sensor. + // This byte is equal to 0x00 for on-off air conditioners. + if (frame.get_value(21) >= 0x20) + update_property(aircon.temperature_outdoor_suction, (uint8_t)(frame.get_value(21) - 0x20), state_changed); + else + aircon.temperature_outdoor_suction.reset(); + + // compressor temperature: + // byte 22: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b22 + if (frame.get_value(22, 0b0111'1111) >= 0x20) + update_property(aircon.temperature_outdoor_discharge, (uint8_t)(frame.get_value(22, 0b0111'1111) - 0x20), state_changed); + else + aircon.temperature_outdoor_discharge.reset(); + + // byte 23: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b23 + // TODO: maybe this is a mess + if (frame.get_value(23) >= 0x20) + update_property(aircon.temperature_outdoor_defrost, (uint8_t)(frame.get_value(23) - 0x20), state_changed); + else + aircon.temperature_outdoor_defrost.reset(); + + // inverter power (0..100 %) + // byte 24: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b24 + if (aircon.ac_type_inverter) + update_property(aircon.inverter_power, frame.get_value(24, 0b0111'1111), state_changed); + else + aircon.inverter_power.reset(); + + // byte 25: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b25 + // byte 26: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b26 + // byte 27: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b27 + // byte 28: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b28 + // byte 29: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b29 + // byte 30: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b30 + + // ambient temperature fractional part (see byte 15) + // byte 31: https://github.com/GrKoR/AUX_HVAC_Protocol#packet_cmd_21_b31 + + if (state_changed) + aircon.publish_all_states(); + } + + } // namespace aux_airconditioner +} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/frame_processor.h b/components/aux_ac/frame_processor.h new file mode 100644 index 0000000..1844942 --- /dev/null +++ b/components/aux_ac/frame_processor.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include "esphome.h" +#include "esphome/components/climate/climate.h" +#include "aircon_common.h" +#include "frame_constants.h" + +namespace esphome +{ + namespace aux_airconditioner + { + + using esphome::climate::ClimateAction; + using esphome::climate::ClimateFanMode; + using esphome::climate::ClimateMode; + using esphome::climate::ClimatePreset; + using esphome::climate::ClimateSwingMode; + + class AirCon; + class Frame; + + //********************************************************************************************* + class FrameProcessorInterface + { + protected: + virtual void _specific_process(const Frame &frame, AirCon &aircon) const = 0; + + public: + virtual ~FrameProcessorInterface() = default; + virtual bool applicable(const Frame &frame) const = 0; + virtual FrameType get_applicable_frame_type() const = 0; + void process(const Frame &frame, AirCon &aircon) const; + }; + + //********************************************************************************************* + class FrameProcessorPing : public FrameProcessorInterface + { + protected: + void _specific_process(const Frame &frame, AirCon &aircon) const override; + + public: + FrameProcessorPing() = default; + bool applicable(const Frame &frame) const override; + FrameType get_applicable_frame_type() const override; + }; + + //********************************************************************************************* + class FrameProcessorResponse01 : public FrameProcessorInterface + { + protected: + void _specific_process(const Frame &frame, AirCon &aircon) const override; + + public: + FrameProcessorResponse01() = default; + bool applicable(const Frame &frame) const override; + FrameType get_applicable_frame_type() const override; + }; + + //********************************************************************************************* + class FrameProcessorResponse11 : public FrameProcessorInterface + { + protected: + ClimateMode _power_and_mode_to_climate_mode(bool power_on, ac_mode mode) const; + void _specific_process(const Frame &frame, AirCon &aircon) const override; + + public: + FrameProcessorResponse11() = default; + bool applicable(const Frame &frame) const override; + FrameType get_applicable_frame_type() const override; + }; + + //********************************************************************************************* + class FrameProcessorResponse2x : public FrameProcessorInterface + { + protected: + void _specific_process(const Frame &frame, AirCon &aircon) const override; + + public: + FrameProcessorResponse2x() = default; + bool applicable(const Frame &frame) const override; + FrameType get_applicable_frame_type() const override; + }; + + } // namespace aux_airconditioner +} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/frame_processor_manager.cpp b/components/aux_ac/frame_processor_manager.cpp new file mode 100644 index 0000000..1a665cc --- /dev/null +++ b/components/aux_ac/frame_processor_manager.cpp @@ -0,0 +1,74 @@ +#include "frame_processor_manager.h" +#include "frame.h" +#include + +namespace esphome +{ + namespace aux_airconditioner + { + + void FrameProcessorManager::_update_map() + { + _processor_map.clear(); + for (FrameProcessorInterface *processor : _processors) + { + auto it = std::find(_processor_map[processor->get_applicable_frame_type()].begin(), _processor_map[processor->get_applicable_frame_type()].end(), processor); + if (it == _processor_map[processor->get_applicable_frame_type()].end()) + _processor_map[processor->get_applicable_frame_type()].push_back(processor); + } + } + + FrameProcessorManager::FrameProcessorManager() + { + _processors.clear(); + _processors.push_back(new FrameProcessorPing); + _processors.push_back(new FrameProcessorResponse01); + _processors.push_back(new FrameProcessorResponse11); + _processors.push_back(new FrameProcessorResponse2x); + + this->_update_map(); + } + + void FrameProcessorManager::add_frame_processor(FrameProcessorInterface *frame_processor) + { + if (frame_processor == nullptr) + return; + + _processors.push_back(frame_processor); + this->_update_map(); + } + + void FrameProcessorManager::delete_all_processors() + { + while (!_processors.empty()) + { + delete _processors.front(); + _processors.pop_front(); + } + _processor_map.clear(); + } + + void FrameProcessorManager::process_frame(Frame &frame) + { + auto processor_it = _processor_map.find(frame.get_frame_type()); + if (processor_it == _processor_map.end()) + { + ESP_LOGW(TAG, "No processor for frame type 0x%02X (%s). Frame: %s", frame.get_frame_type(), frame.type_to_string().c_str(), frame.to_string().c_str()); + return; + } + + // check if list of processors is empty + if (processor_it->second.size() == 0) + return; + + for (FrameProcessorInterface *processor : processor_it->second) + { + if (processor->applicable(frame)) + { + processor->process(frame, *_aircon); + } + } + } + + } // namespace aux_airconditioner +} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/frame_processor_manager.h b/components/aux_ac/frame_processor_manager.h new file mode 100644 index 0000000..c73a0b6 --- /dev/null +++ b/components/aux_ac/frame_processor_manager.h @@ -0,0 +1,34 @@ +#pragma once + +#include "frame_processor.h" +#include +#include + +namespace esphome +{ + namespace aux_airconditioner + { + class AirCon; + class FrameProcessorInterface; + class Frame; + + class FrameProcessorManager + { + protected: + AirCon *_aircon = nullptr; + std::map> _processor_map; + std::list _processors; + void _update_map(); + + public: + FrameProcessorManager(); + ~FrameProcessorManager() { this->delete_all_processors(); } + + void set_aircon(AirCon &aircon) { _aircon = &aircon; } + void add_frame_processor(FrameProcessorInterface *frame_processor); + void delete_all_processors(); + void process_frame(Frame &frame); + }; + + } // namespace aux_airconditioner +} // namespace esphome \ No newline at end of file diff --git a/components/aux_ac/helpers.cpp b/components/aux_ac/helpers.cpp new file mode 100644 index 0000000..e4fa1fa --- /dev/null +++ b/components/aux_ac/helpers.cpp @@ -0,0 +1,12 @@ +#include "helpers.h" + +namespace esphome +{ + namespace helpers + { + + uint32_t TimerManager::_millis = 0; + + } // namespace helpers + +} // namespace GrKoR \ No newline at end of file diff --git a/components/aux_ac/helpers.h b/components/aux_ac/helpers.h new file mode 100644 index 0000000..df6306f --- /dev/null +++ b/components/aux_ac/helpers.h @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include "esphome/core/optional.h" + +namespace esphome +{ + namespace helpers + { + + /*************************************************************************************************\ + \*************************************************************************************************/ + class TimerInterface + { + public: + virtual bool is_expired() const = 0; + virtual bool is_enabled() const = 0; + virtual void start(uint32_t period_ms) = 0; + virtual void stop() = 0; + virtual void reset() = 0; + virtual void set_callback(std::function callback) = 0; + virtual void trigger_callback() = 0; + }; + + /*************************************************************************************************\ + \*************************************************************************************************/ + using millis_function_t = uint32_t (*)(); + + class TimerManager + { + public: + static void set_millis(uint32_t current_time) { TimerManager::_millis = current_time; } + static uint32_t get_millis() { return TimerManager::_millis; } + + void set_millis_func(millis_function_t millis) { _millis_func = millis; } + + void register_timer(TimerInterface &timer) { _timers.push_back(&timer); } + + void task() + { + if (_millis_func != nullptr) + _millis = _millis_func(); + + for (auto timer : _timers) + if (timer->is_enabled() && timer->is_expired()) + timer->trigger_callback(); + } + + private: + millis_function_t _millis_func{nullptr}; + static uint32_t _millis; + std::list _timers; + }; + + /*************************************************************************************************\ + \*************************************************************************************************/ + static void dummy_stopper(TimerInterface *timer) { timer->stop(); } + + class Timer : public TimerInterface + { + public: + Timer() : _callback(dummy_stopper), _period_ms(0) {} + + virtual bool is_expired() const override { return TimerManager::get_millis() - this->_last_trigger_time >= this->_period_ms; } + virtual bool is_enabled() const override { return this->_period_ms > 0; } + + virtual void start(uint32_t period_ms) override + { + this->_period_ms = period_ms; + this->reset(); + } + virtual void stop() override { this->_period_ms = 0; } + virtual void reset() override { this->_last_trigger_time = TimerManager::get_millis(); } + + virtual void set_callback(std::function callback) override { this->_callback = callback; } + virtual void trigger_callback() override + { + this->_callback((TimerInterface *)this); + this->reset(); + } + + private: + std::function _callback = nullptr; + uint32_t _period_ms; + uint32_t _last_trigger_time; + }; + + /*********************************************************************************************\ + \*********************************************************************************************/ + template + bool update_property(T &property, const T &value, bool &flag) + { + if (property != value) + { + property = value; + flag = true; + return true; + } + return false; + } + + template + bool update_property(optional &property, const T &value, bool &flag) + { + if (property != value) + { + property = value; + flag = true; + return true; + } + return false; + } + + } // namespace helpers +} // namespace esphome diff --git a/examples/advanced/ac_common.yaml b/examples/advanced/ac_common.yaml index 23a270a..7bf115d 100644 --- a/examples/advanced/ac_common.yaml +++ b/examples/advanced/ac_common.yaml @@ -63,39 +63,46 @@ climate: id: aux_id uart_id: ac_uart_bus period: 7s - show_action: true display_inverted: true optimistic: 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 + indoor_ambient_temperature: + name: ${upper_devicename} Indoor Ambient Temperature + id: ${devicename}_indoor_ambient_temp internal: false - outbound_temperature: - name: ${upper_devicename} Coolant Outbound Temperature - id: ${devicename}_outbound_temp + indoor_coil_temperature: + name: ${upper_devicename} Indoor Coil Temperature + id: ${devicename}_indoor_coil_temp internal: false - inbound_temperature: - name: ${upper_devicename} Coolant Inbound Temperature - id: ${devicename}_inbound_temp + outdoor_ambient_temperature: + name: ${upper_devicename} Outdoor Ambient Temperature + id: ${devicename}_outdoor_ambient_temp internal: false - compressor_temperature: - name: ${upper_devicename} Compressor Temperature - id: ${devicename}_strange_temp + outdoor_condenser_temperature: + name: ${upper_devicename} Outdoor Condenser Temperature + id: ${devicename}_outdoor_condenser_temp + internal: false + compressor_suction_temperature: + name: ${upper_devicename} Compressor Suction Temperature + id: ${devicename}_compressor_suction_temp + internal: false + compressor_discharge_temperature: + name: ${upper_devicename} Compressor Discharge Temperature + id: ${devicename}_compressor_discharge_temp + internal: false + defrost_temperature: + name: ${upper_devicename} Defrost Temperature + id: ${devicename}_defrost_temp internal: false defrost_state: name: ${upper_devicename} Defrost State id: ${devicename}_defrost_state internal: false inverter_power: - name: ${upper_devicename} Invertor Power + name: ${upper_devicename} Inverter Power id: ${devicename}_inverter_power internal: false preset_reporter: @@ -109,7 +116,9 @@ climate: visual: min_temperature: 16 max_temperature: 32 - temperature_step: 0.5 + temperature_step: + target_temperature: 0.5 + current_temperature: 0.1 supported_modes: - HEAT_COOL - COOL @@ -138,6 +147,8 @@ sensor: update_interval: 30s unit_of_measurement: "dBa" accuracy_decimals: 0 + - platform: uptime + name: ${upper_devicename} Uptime Sensor switch: @@ -154,43 +165,72 @@ switch: turn_off_action: - aux_ac.display_off: aux_id + + - platform: template + name: ${upper_devicename} Power Limitation + lambda: |- + if (id(${devicename}_inverter_power).state) { + return true; + } else { + return false; + } + turn_on_action: + #- aux_ac.power_limit_on: aux_id + - aux_ac.power_limit_on: + id: aux_id + limit: 40 + turn_off_action: + - aux_ac.power_limit_off: aux_id + + button: + - platform: template + name: ${upper_devicename} 26 deg Cool Low Fan + on_press: + - climate.control: + id: aux_id + mode: COOL + target_temperature: 26°C + fan_mode: LOW + #custom_fan_mode: MUTE + swing_mode: "OFF" + - 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" @@ -226,4 +266,5 @@ number: step: 1 set_action: then: - - lambda: !lambda "id(aux_id).powerLimitationOnSequence( x );" + - lambda: !lambda "id(aux_id).action_power_limitation_on( x );" + diff --git a/tests/ac_send_packet_for_engineer.py b/tests/ac_send_packet_for_engineer.py deleted file mode 100644 index ce26215..0000000 --- a/tests/ac_send_packet_for_engineer.py +++ /dev/null @@ -1,191 +0,0 @@ -import time -import aioesphomeapi -import asyncio -import re -import sys -import argparse -from aioesphomeapi.api_pb2 import (LOG_LEVEL_NONE, - LOG_LEVEL_ERROR, - LOG_LEVEL_WARN, - LOG_LEVEL_INFO, - LOG_LEVEL_DEBUG, - LOG_LEVEL_VERBOSE, - LOG_LEVEL_VERY_VERBOSE) - -def createParser (): - parser = argparse.ArgumentParser( - description='''This script is used for collecting logs from ac_aux ESPHome component. - For more info, see https://github.com/GrKoR/ac_python_logger''', - add_help = False) - parent_group = parser.add_argument_group (title='Params') - 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') - return parser - -async def main(): - """Connect to an ESPHome device and wait for state changes.""" - api = aioesphomeapi.APIClient(namespace.ip[0], 6053, namespace.pwd[0]) - - try: - await api.connect(login=True) - except aioesphomeapi.InvalidAuthAPIError as e: - return print(e) - - print(api.api_version) - - 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], - } - ) - - 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], - } - ) - - 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], - } - ) - - 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], - } - ) - - 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( - name="send_data", - key=311254518, - args=[ - aioesphomeapi.UserServiceArg(name="data_buf", type=aioesphomeapi.UserServiceArgType.INT_ARRAY), - ], - ) - - time.sleep(7) - await ac_get11_00() - time.sleep(7) - await ac_get11_01() - - #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) - - -parser = createParser() -namespace = parser.parse_args() -print("IP: ", namespace.ip[0]) - - -loop = asyncio.get_event_loop() -try: - #asyncio.ensure_future(main()) - #loop.run_forever() - loop.run_until_complete(main()) -except aioesphomeapi.InvalidAuthAPIError as e: - print(e) -except KeyboardInterrupt: - pass -finally: - loop.close() - pass \ No newline at end of file diff --git a/tests/test-ext-esp32.yaml b/tests/test-ext-esp32.yaml index d4e5177..8da0a02 100644 --- a/tests/test-ext-esp32.yaml +++ b/tests/test-ext-esp32.yaml @@ -1,5 +1,5 @@ external_components: - - source: github://GrKoR/esphome_aux_ac_component@dev + - source: github://GrKoR/esphome_aux_ac_component components: [ aux_ac ] refresh: 0s @@ -58,31 +58,38 @@ climate: id: aux_id uart_id: ac_uart_bus 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 + indoor_ambient_temperature: + name: $upper_devicename Indoor Ambient Temperature + id: ${devicename}_indoor_ambient_temp internal: false - outbound_temperature: - name: $upper_devicename Coolant Outbound Temperature - id: ${devicename}_outbound_temp + outdoor_ambient_temperature: + name: $upper_devicename Outdoor Ambient Temperature + id: ${devicename}_outdoor_ambient_temp internal: false - inbound_temperature: - name: $upper_devicename Coolant Inbound Temperature - id: ${devicename}_inbound_temp + outdoor_condenser_temperature: + name: $upper_devicename Outdoor Condenser Temperature + id: ${devicename}_outdoor_condenser_temp internal: false - compressor_temperature: - name: $upper_devicename Compressor Temperature - id: ${devicename}_strange_temp + compressor_suction_temperature: + name: $upper_devicename Compressor Suction Temperature + id: ${devicename}_compressor_suction_temp + internal: false + indoor_coil_temperature: + name: $upper_devicename Indoor Coil Temperature + id: ${devicename}_indoor_coil_temp + internal: false + compressor_discharge_temperature: + name: $upper_devicename Compressor Discharge Temperature + id: ${devicename}_compressor_discharge_temp + internal: false + defrost_temperature: + name: $upper_devicename Defrost Temperature + id: ${devicename}_defrost_temp internal: false defrost_state: name: $upper_devicename Defrost State diff --git a/tests/test-ext-for-engineer.yaml b/tests/test-ext-for-engineer.yaml deleted file mode 100644 index 20f4ed3..0000000 --- a/tests/test-ext-for-engineer.yaml +++ /dev/null @@ -1,156 +0,0 @@ -external_components: - - source: github://GrKoR/esphome_aux_ac_component@dev - components: [ aux_ac ] - refresh: 0s - -substitutions: - devicename: test_aux_ac_ext_engeneer - 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: Test AUX Fallback Hotspot - password: !secret wifi_ap_pass - -logger: - level: DEBUG - baud_rate: 0 - -api: - password: !secret api_pass - reboot_timeout: 0s - services: - # этот сервис можно вызвать из Home Assistant или Python. Он отправляет полученные байты в кондиционер - - service: send_data - variables: - data_buf: int[] - then: - # ВАЖНО! Только для инженеров! - # Вызывайте метод aux_ac.send_packet только если понимаете, что делаете! Он не проверяет данные, а передаёт - # кондиционеру всё как есть. Какой эффект получится от передачи кондиционеру рандомных байт, никто не знает. - # Вы действуете на свой страх и риск. - - aux_ac.send_packet: - id: aux_id - data: !lambda |- - std::vector data{}; - for (int n : data_buf) { - data.push_back( (uint8_t) n ); - } - return data; - -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 - optimistic: 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 Coolant Outbound Temperature - id: ${devicename}_outbound_temp - internal: false - inbound_temperature: - name: $upper_devicename Coolant 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 - inverter_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 - - COOL - - HEAT - - DRY - - FAN_ONLY - custom_fan_modes: - - MUTE - - TURBO - supported_presets: - - SLEEP - custom_presets: - - CLEAN - - HEALTH - - ANTIFUNGUS - supported_swing_modes: - - VERTICAL - - HORIZONTAL - - BOTH - - -switch: - - platform: template - name: AC Display - lambda: |- - if (id(${devicename}_display_state).state) { - return true; - } else { - return false; - } - turn_on_action: - - aux_ac.display_on: aux_id - turn_off_action: - - aux_ac.display_off: aux_id \ No newline at end of file diff --git a/tests/test-ext-power-limit.yaml b/tests/test-ext-power-limit.yaml index b873586..4ffc75c 100644 --- a/tests/test-ext-power-limit.yaml +++ b/tests/test-ext-power-limit.yaml @@ -1,5 +1,5 @@ external_components: - - source: github://GrKoR/esphome_aux_ac_component@dev + - source: github://GrKoR/esphome_aux_ac_component components: [ aux_ac ] refresh: 0s @@ -59,32 +59,39 @@ climate: id: aux_id uart_id: ac_uart_bus period: 7s - show_action: true display_inverted: true - timeout: 150 - indoor_temperature: - name: $upper_devicename Indoor Temperature - id: ${devicename}_indoor_temp - internal: false + timeout: 300 display_state: name: $upper_devicename Display State id: ${devicename}_display_state internal: false - outdoor_temperature: - name: $upper_devicename Outdoor Temperature - id: ${devicename}_outdoor_temp + indoor_ambient_temperature: + name: $upper_devicename Indoor Ambient Temperature + id: ${devicename}_indoor_ambient_temp internal: false - outbound_temperature: - name: $upper_devicename Coolant Outbound Temperature - id: ${devicename}_outbound_temp + outdoor_ambient_temperature: + name: $upper_devicename Outdoor Ambient Temperature + id: ${devicename}_outdoor_ambient_temp internal: false - inbound_temperature: - name: $upper_devicename Coolant Inbound Temperature - id: ${devicename}_inbound_temp + outdoor_condenser_temperature: + name: $upper_devicename Outdoor Condenser Temperature + id: ${devicename}_outdoor_condenser_temp internal: false - compressor_temperature: - name: $upper_devicename Compressor Temperature - id: ${devicename}_strange_temp + compressor_suction_temperature: + name: $upper_devicename Compressor Suction Temperature + id: ${devicename}_compressor_suction_temp + internal: false + indoor_coil_temperature: + name: $upper_devicename Indoor Coil Temperature + id: ${devicename}_indoor_coil_temp + internal: false + compressor_discharge_temperature: + name: $upper_devicename Compressor Discharge Temperature + id: ${devicename}_compressor_discharge_temp + internal: false + defrost_temperature: + name: $upper_devicename Defrost Temperature + id: ${devicename}_defrost_temp internal: false defrost_state: name: $upper_devicename Defrost State diff --git a/tests/test-local-airflow-dir.yaml b/tests/test-local-airflow-dir.yaml index 04bfaaa..e6ecf66 100644 --- a/tests/test-local-airflow-dir.yaml +++ b/tests/test-local-airflow-dir.yaml @@ -61,7 +61,6 @@ climate: id: aux_id uart_id: ac_uart_bus period: 7s - show_action: true display_inverted: true @@ -121,5 +120,4 @@ number: set_action: then: - lambda: !lambda |- - if (x == 6) x = 7; // делаем так, чтобы выключение отрабатывать корректно - id(aux_id).setVLouverSequence( static_cast(x) ); + id(aux_id).action_set_vlouver_position( static_cast(x)); diff --git a/tests/test-local-power-limit.yaml b/tests/test-local-power-limit.yaml index 349607c..c0813d2 100644 --- a/tests/test-local-power-limit.yaml +++ b/tests/test-local-power-limit.yaml @@ -59,32 +59,39 @@ climate: id: aux_id uart_id: ac_uart_bus period: 7s - show_action: true display_inverted: true - timeout: 150 - indoor_temperature: - name: $upper_devicename Indoor Temperature - id: ${devicename}_indoor_temp - internal: false + timeout: 300 display_state: name: $upper_devicename Display State id: ${devicename}_display_state internal: false - outdoor_temperature: - name: $upper_devicename Outdoor Temperature - id: ${devicename}_outdoor_temp + indoor_ambient_temperature: + name: $upper_devicename Indoor Ambient Temperature + id: ${devicename}_indoor_ambient_temp internal: false - outbound_temperature: - name: $upper_devicename Coolant Outbound Temperature - id: ${devicename}_outbound_temp + outdoor_ambient_temperature: + name: $upper_devicename Outdoor Ambient Temperature + id: ${devicename}_outdoor_ambient_temp internal: false - inbound_temperature: - name: $upper_devicename Coolant Inbound Temperature - id: ${devicename}_inbound_temp + outdoor_condenser_temperature: + name: $upper_devicename Outdoor Condenser Temperature + id: ${devicename}_outdoor_condenser_temp internal: false - compressor_temperature: - name: $upper_devicename Compressor Temperature - id: ${devicename}_strange_temp + compressor_suction_temperature: + name: $upper_devicename Compressor Suction Temperature + id: ${devicename}_compressor_suction_temp + internal: false + indoor_coil_temperature: + name: $upper_devicename Indoor Coil Temperature + id: ${devicename}_indoor_coil_temp + internal: false + compressor_discharge_temperature: + name: $upper_devicename Compressor Discharge Temperature + id: ${devicename}_compressor_discharge_temp + internal: false + defrost_temperature: + name: $upper_devicename Defrost Temperature + id: ${devicename}_defrost_temp internal: false defrost_state: name: $upper_devicename Defrost State @@ -158,5 +165,5 @@ number: set_action: then: - lambda: !lambda |- - id(aux_id).powerLimitationOnSequence( x ); + id(aux_id).action_power_limitation_on( x ); diff --git a/tests/test-local.yaml b/tests/test-local.yaml index e934728..d0f97b9 100644 --- a/tests/test-local.yaml +++ b/tests/test-local.yaml @@ -59,33 +59,40 @@ climate: id: aux_id uart_id: ac_uart_bus period: 7s - show_action: true display_inverted: true optimistic: true - indoor_temperature: - name: $upper_devicename Indoor Temperature - id: ${devicename}_indoor_temp + indoor_ambient_temperature: + name: $upper_devicename Indoor Ambient Temperature + id: ${devicename}_indoor_ambient_temp + internal: false + outdoor_ambient_temperature: + name: $upper_devicename Outdoor Ambient Temperature + id: ${devicename}_outdoor_ambient_temp + internal: false + outdoor_condenser_temperature: + name: $upper_devicename Outdoor Condenser Temperature + id: ${devicename}_outdoor_condenser_temp + internal: false + compressor_suction_temperature: + name: $upper_devicename Compressor Suction Temperature + id: ${devicename}_compressor_suction_temp + internal: false + indoor_coil_temperature: + name: $upper_devicename Indoor Coil Temperature + id: ${devicename}_indoor_coil_temp + internal: false + compressor_discharge_temperature: + name: $upper_devicename Compressor Discharge Temperature + id: ${devicename}_compressor_discharge_temp + internal: false + defrost_temperature: + name: $upper_devicename Defrost Temperature + id: ${devicename}_defrost_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 Coolant Outbound Temperature - id: ${devicename}_outbound_temp - internal: false - inbound_temperature: - name: $upper_devicename Coolant 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