From 236f02f57357a913f57b74d29a58d98d356e3c0f Mon Sep 17 00:00:00 2001 From: Lorenzo Prosseda Date: Wed, 19 Feb 2025 16:21:44 +0100 Subject: [PATCH] Add vertical_direction select comp. for platform airton A select component called vertical_direction will be available as a config for the airton climate component; additionally, now changing the display or sleep switches will immediately send the IR command. --- esphome/components/airton/airton.cpp | 82 +++++++++++++------ esphome/components/airton/airton.h | 30 ++++++- esphome/components/airton/climate.py | 16 ++++ esphome/components/airton/quirks.h | 76 +++++++++++++++++ esphome/components/airton/select/__init__.py | 38 +++++++++ .../components/airton/select/direction.cpp | 15 ++++ esphome/components/airton/select/direction.h | 18 ++++ esphome/components/airton/switch/display.cpp | 2 +- esphome/components/airton/switch/sleep.cpp | 2 +- 9 files changed, 248 insertions(+), 31 deletions(-) create mode 100644 esphome/components/airton/quirks.h create mode 100644 esphome/components/airton/select/__init__.py create mode 100644 esphome/components/airton/select/direction.cpp create mode 100644 esphome/components/airton/select/direction.h diff --git a/esphome/components/airton/airton.cpp b/esphome/components/airton/airton.cpp index 7ebda88c3e..f60fd69441 100644 --- a/esphome/components/airton/airton.cpp +++ b/esphome/components/airton/airton.cpp @@ -6,30 +6,62 @@ namespace airton { static const char *const TAG = "airton.climate"; -void AirtonClimate::set_sleep_mode_state(bool state) { +void AirtonClimate::set_sleep_mode_state(bool state, bool send_ir = false) { if (state != this->settings_.sleep_state) { this->settings_.sleep_state = state; #ifdef USE_SWITCH this->sleep_mode_switch_->publish_state(state); #endif this->airton_rtc_.save(&this->settings_); + if (send_ir) + this->transmit_state(); } } bool AirtonClimate::get_sleep_mode_state() const { return this->settings_.sleep_state; } -void AirtonClimate::set_display_state(bool state) { +void AirtonClimate::set_display_state(bool state, bool send_ir = false) { if (state != this->settings_.display_state) { this->settings_.display_state = state; #ifdef USE_SWITCH this->display_switch_->publish_state(state); #endif this->airton_rtc_.save(&this->settings_); + if (send_ir) + this->transmit_state(); } } bool AirtonClimate::get_display_state() const { return this->settings_.display_state; } +void AirtonClimate::set_vertical_direction_state(VerticalDirection state) { + if (state.toUint8() != this->settings_.vertical_direction_state.toUint8()) { + this->settings_.vertical_direction_state = state; +#ifdef USE_SELECT + this->vertical_direction_select_->publish_state(state.toString()); +#endif + this->airton_rtc_.save(&this->settings_); + } +} +void AirtonClimate::set_vertical_direction_state(const std::string state) { + if (state != this->settings_.vertical_direction_state.toString()) { + VerticalDirection new_state; + new_state.setDirection(state); + this->settings_.vertical_direction_state = new_state.getDirection(); +#ifdef USE_SELECT + this->vertical_direction_select_->publish_state(state); +#endif + this->airton_rtc_.save(&this->settings_); + // This overloaded function is called only from the select component, upon changing selection + // Therefore, we transmit the updated state after saving it + this->transmit_state(); + } +} + +VerticalDirection AirtonClimate::get_vertical_direction_state() const { + return this->settings_.vertical_direction_state; +} + #ifdef USE_SWITCH void AirtonClimate::set_sleep_mode_switch(switch_::Switch *sw) { this->sleep_mode_switch_ = sw; @@ -45,6 +77,15 @@ void AirtonClimate::set_display_switch(switch_::Switch *sw) { } #endif // USE_SWITCH +#ifdef USE_SELECT +void AirtonClimate::set_vertical_direction_select(select::Select *sel) { + this->vertical_direction_select_ = sel; + if (this->vertical_direction_select_ != nullptr) { + this->vertical_direction_select_->publish_state(this->get_vertical_direction_state().toString()); + } +} +#endif // USE_SELECT + uint8_t AirtonClimate::get_previous_mode_() { return previous_mode_; } void AirtonClimate::set_previous_mode_(uint8_t mode) { previous_mode_ = mode; } @@ -68,7 +109,7 @@ void AirtonClimate::transmit_state() { remote_state[3] |= this->temperature_(); remote_state[4] = 0; - remote_state[4] |= this->swing_mode_(); + remote_state[4] |= this->get_vertical_direction_state().toUint8(); remote_state[5] = this->operation_settings_(); @@ -165,18 +206,6 @@ uint8_t AirtonClimate::temperature_() { } } -uint8_t AirtonClimate::swing_mode_() { - uint8_t swing_control = 0b01100000; - switch (this->swing_mode) { - case climate::CLIMATE_SWING_VERTICAL: - swing_control |= 1; - break; - default: - break; - } - return swing_control; -} - // The bits of this packet's byte have the following meanings (from MSB to LSB) // Light, Health, Unknown, HeatOn, Unknown, NotAutoOn, Sleep, Econo uint8_t AirtonClimate::operation_settings_() { @@ -231,19 +260,8 @@ bool AirtonClimate::parse_state_frame_(uint8_t const frame[]) { } else { this->mode = climate::CLIMATE_MODE_OFF; } - uint8_t temperature = frame[3]; - this->target_temperature = - (temperature & 0b00001111) + 16; // Mask the higher half of the byte (unused), add back the offset - uint8_t fan_mode = (frame[2] & 0b01110000) >> 4; // Mask anything but bits 5-7, then shift them to the right - uint8_t swing_mode = frame[4] & 0b00000001; // Mask anything but the LSB - if (swing_mode) { - this->swing_mode = climate::CLIMATE_SWING_VERTICAL; - } else { - this->swing_mode = climate::CLIMATE_SWING_OFF; - } - switch (fan_mode) { case AIRTON_FAN_1: case AIRTON_FAN_2: @@ -261,6 +279,18 @@ bool AirtonClimate::parse_state_frame_(uint8_t const frame[]) { break; } + uint8_t temperature = frame[3]; + this->target_temperature = + (temperature & 0b00001111) + 16; // Mask the higher half of the byte (unused), add back the offset + + uint8_t swing_mode = frame[4] & 0b00001111; // Mask the higher nibble + if (swing_mode == (uint8_t) VerticalDirection::VERTICAL_DIRECTION_OFF) { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } else { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } + this->set_vertical_direction_state(static_cast(swing_mode)); + uint8_t display_light = frame[5] & 0b10000000; // Mask anything but the MSB this->set_display_state(display_light != 0); diff --git a/esphome/components/airton/airton.h b/esphome/components/airton/airton.h index 53c02e4997..1dc4522016 100644 --- a/esphome/components/airton/airton.h +++ b/esphome/components/airton/airton.h @@ -1,11 +1,16 @@ #pragma once #include "esphome/components/climate_ir/climate_ir.h" +#include "quirks.h" #ifdef USE_SWITCH #include "esphome/components/switch/switch.h" #endif +#ifdef USE_SELECT +#include "esphome/components/select/select.h" +#endif + namespace esphome { namespace airton { @@ -45,8 +50,18 @@ const uint8_t AIRTON_STATE_FRAME_SIZE = 7; struct AirtonSettings { bool sleep_state; bool display_state; + VerticalDirection vertical_direction_state; }; +// Local vertical direction constants +static const VerticalDirection VERTICAL_DIRECTION_OFF = VerticalDirection::VERTICAL_DIRECTION_OFF; +static const VerticalDirection VERTICAL_DIRECTION_SWING = VerticalDirection::VERTICAL_DIRECTION_SWING; +static const VerticalDirection VERTICAL_DIRECTION_UP = VerticalDirection::VERTICAL_DIRECTION_UP; +static const VerticalDirection VERTICAL_DIRECTION_MIDDLE_UP = VerticalDirection::VERTICAL_DIRECTION_MIDDLE_UP; +static const VerticalDirection VERTICAL_DIRECTION_MIDDLE = VerticalDirection::VERTICAL_DIRECTION_MIDDLE; +static const VerticalDirection VERTICAL_DIRECTION_MIDDLE_DOWN = VerticalDirection::VERTICAL_DIRECTION_MIDDLE_DOWN; +static const VerticalDirection VERTICAL_DIRECTION_DOWN = VerticalDirection::VERTICAL_DIRECTION_DOWN; + class AirtonClimate : public climate_ir::ClimateIR { #ifdef USE_SWITCH public: @@ -56,6 +71,13 @@ class AirtonClimate : public climate_ir::ClimateIR { protected: switch_::Switch *sleep_mode_switch_{nullptr}; switch_::Switch *display_switch_{nullptr}; +#endif +#ifdef USE_SELECT + public: + void set_vertical_direction_select(select::Select *sel); + + protected: + select::Select *vertical_direction_select_{nullptr}; #endif public: AirtonClimate() @@ -63,10 +85,13 @@ class AirtonClimate : public climate_ir::ClimateIR { {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH}, {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {} - void set_sleep_mode_state(bool state); + void set_sleep_mode_state(bool state, bool send_ir); bool get_sleep_mode_state() const; - void set_display_state(bool state); + void set_display_state(bool state, bool send_ir); bool get_display_state() const; + void set_vertical_direction_state(VerticalDirection state); + void set_vertical_direction_state(std::string state); + VerticalDirection get_vertical_direction_state() const; private: // Save the previous operation mode inside instance @@ -85,7 +110,6 @@ class AirtonClimate : public climate_ir::ClimateIR { uint16_t fan_speed_(); bool turbo_control_(); uint8_t temperature_(); - uint8_t swing_mode_(); uint8_t operation_settings_(); uint8_t sum_bytes_(const uint8_t *start, uint16_t length); diff --git a/esphome/components/airton/climate.py b/esphome/components/airton/climate.py index 927f953f9f..a7afbb9070 100644 --- a/esphome/components/airton/climate.py +++ b/esphome/components/airton/climate.py @@ -11,10 +11,25 @@ AirtonClimate = airton_ns.class_("AirtonClimate", climate_ir.ClimateIR) CONF_AIRTON_ID = "airton_id" CONF_SLEEP_MODE = "sleep_mode" +CONF_VERTICAL_DIRECTION = "vertical_direction" + +VerticalDirections = airton_ns.enum("VerticalDirections") +VERTICAL_DIRECTIONS = { + "off": VerticalDirections.VERTICAL_DIRECTION_OFF, + "swing": VerticalDirections.VERTICAL_DIRECTION_SWING, + "up": VerticalDirections.VERTICAL_DIRECTION_UP, + "middle-up": VerticalDirections.VERTICAL_DIRECTION_MIDDLE_UP, + "middle": VerticalDirections.VERTICAL_DIRECTION_MIDDLE, + "middle-down": VerticalDirections.VERTICAL_DIRECTION_MIDDLE_DOWN, + "down": VerticalDirections.VERTICAL_DIRECTION_DOWN, +} CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(AirtonClimate), + cv.Optional(CONF_VERTICAL_DIRECTION, default="off"): cv.enum( + VERTICAL_DIRECTIONS + ), } ) @@ -57,3 +72,4 @@ async def sleep_action_to_code(config, action_id, template_arg, args): async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.register_climate_ir(var, config) + cg.add(var.set_vertical_direction_state(config[CONF_VERTICAL_DIRECTION])) diff --git a/esphome/components/airton/quirks.h b/esphome/components/airton/quirks.h new file mode 100644 index 0000000000..8cad155441 --- /dev/null +++ b/esphome/components/airton/quirks.h @@ -0,0 +1,76 @@ +#ifndef AIRTON_QUIRKS_H +#define AIRTON_QUIRKS_H + +#include +#include +#include "esphome/core/log.h" + +namespace esphome { +namespace airton { + +// Vertical direction for air outlet flap +class VerticalDirection { + public: + enum Direction { + VERTICAL_DIRECTION_OFF = 0x00, + VERTICAL_DIRECTION_SWING = 0x01, + VERTICAL_DIRECTION_UP = 0x02, + VERTICAL_DIRECTION_MIDDLE_UP = 0x03, + VERTICAL_DIRECTION_MIDDLE = 0x04, + VERTICAL_DIRECTION_MIDDLE_DOWN = 0x05, + VERTICAL_DIRECTION_DOWN = 0x06, + }; + + VerticalDirection(Direction direction = Direction::VERTICAL_DIRECTION_OFF) : _direction(direction) {} + + std::string toString() const { + switch (_direction) { + case Direction::VERTICAL_DIRECTION_SWING: + return "swing"; + case Direction::VERTICAL_DIRECTION_UP: + return "up"; + case Direction::VERTICAL_DIRECTION_MIDDLE_UP: + return "middle-up"; + case Direction::VERTICAL_DIRECTION_MIDDLE: + return "middle"; + case Direction::VERTICAL_DIRECTION_MIDDLE_DOWN: + return "middle-down"; + case Direction::VERTICAL_DIRECTION_DOWN: + return "down"; + case Direction::VERTICAL_DIRECTION_OFF: + default: + return "off"; + } + } + + uint8_t toUint8() const { return static_cast(_direction); } + + void setDirection(Direction direction) { _direction = direction; } + void setDirection(std::string direction) { + if (direction == "swing") { + _direction = Direction::VERTICAL_DIRECTION_SWING; + } else if (direction == "up") { + _direction = Direction::VERTICAL_DIRECTION_UP; + } else if (direction == "middle-up") { + _direction = Direction::VERTICAL_DIRECTION_MIDDLE_UP; + } else if (direction == "middle") { + _direction = Direction::VERTICAL_DIRECTION_MIDDLE; + } else if (direction == "middle-down") { + _direction = Direction::VERTICAL_DIRECTION_MIDDLE_DOWN; + } else if (direction == "down") { + _direction = Direction::VERTICAL_DIRECTION_DOWN; + } else { + _direction = Direction::VERTICAL_DIRECTION_OFF; + } + } + + Direction getDirection() const { return _direction; } + + private: + Direction _direction; +}; + +} // namespace airton +} // namespace esphome + +#endif // AIRTON_QUIRKS_H diff --git a/esphome/components/airton/select/__init__.py b/esphome/components/airton/select/__init__.py new file mode 100644 index 0000000000..bc1c0b15e9 --- /dev/null +++ b/esphome/components/airton/select/__init__.py @@ -0,0 +1,38 @@ +import esphome.codegen as cg +from esphome.components import select +import esphome.config_validation as cv +from esphome.const import ENTITY_CATEGORY_CONFIG + +from ..climate import ( + CONF_AIRTON_ID, + CONF_VERTICAL_DIRECTION, + VERTICAL_DIRECTIONS, + AirtonClimate, + airton_ns, +) + +CODEOWNERS = ["@procsiab"] + +VerticalDirectionSelect = airton_ns.class_("VerticalDirectionSelect", select.Select) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_AIRTON_ID): cv.use_id(AirtonClimate), + cv.Optional(CONF_VERTICAL_DIRECTION): select.select_schema( + VerticalDirectionSelect, + entity_category=ENTITY_CATEGORY_CONFIG, + ), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_AIRTON_ID]) + + for select_type in [CONF_VERTICAL_DIRECTION]: + if conf := config.get(select_type): + sel_var = await select.new_select( + conf, options=list(VERTICAL_DIRECTIONS.keys()) + ) + await cg.register_parented(sel_var, parent) + cg.add(getattr(parent, f"set_{select_type}_select")(sel_var)) diff --git a/esphome/components/airton/select/direction.cpp b/esphome/components/airton/select/direction.cpp new file mode 100644 index 0000000000..425c705d75 --- /dev/null +++ b/esphome/components/airton/select/direction.cpp @@ -0,0 +1,15 @@ +#include "direction.h" + +namespace esphome { +namespace airton { + +void VerticalDirectionSelect::control(const std::string &value) { + if (this->parent_->get_vertical_direction_state().toString() != value) { + ESP_LOGD("vertical_direction", "Select received value: %s", value.c_str()); + this->parent_->set_vertical_direction_state(value); + } + this->publish_state(value); +} + +} // namespace airton +} // namespace esphome diff --git a/esphome/components/airton/select/direction.h b/esphome/components/airton/select/direction.h new file mode 100644 index 0000000000..fef19ccfa5 --- /dev/null +++ b/esphome/components/airton/select/direction.h @@ -0,0 +1,18 @@ +#pragma once + +#include "esphome/components/select/select.h" +#include "../airton.h" + +namespace esphome { +namespace airton { + +class VerticalDirectionSelect : public select::Select, public Parented { + public: + VerticalDirectionSelect() = default; + + protected: + void control(const std::string &value) override; +}; + +} // namespace airton +} // namespace esphome diff --git a/esphome/components/airton/switch/display.cpp b/esphome/components/airton/switch/display.cpp index 1322d40c95..65570cefb4 100644 --- a/esphome/components/airton/switch/display.cpp +++ b/esphome/components/airton/switch/display.cpp @@ -5,7 +5,7 @@ namespace airton { void DisplaySwitch::write_state(bool state) { if (this->parent_->get_display_state() != state) { - this->parent_->set_display_state(state); + this->parent_->set_display_state(state, true); } this->publish_state(state); } diff --git a/esphome/components/airton/switch/sleep.cpp b/esphome/components/airton/switch/sleep.cpp index 5d6a697894..7eb479031d 100644 --- a/esphome/components/airton/switch/sleep.cpp +++ b/esphome/components/airton/switch/sleep.cpp @@ -5,7 +5,7 @@ namespace airton { void SleepSwitch::write_state(bool state) { if (this->parent_->get_sleep_mode_state() != state) { - this->parent_->set_sleep_mode_state(state); + this->parent_->set_sleep_mode_state(state, true); } this->publish_state(state); }