7 Commits

Author SHA1 Message Date
GrKoR
efa0991dd0 fix: automations play() method fixed 2025-11-27 17:57:13 -08:00
GrKoR
2e0c421c9f fix: removed module dependency" 2025-11-26 23:28:20 -08:00
GrKoR
53414d8ab4 fix: python code generator fix for custom presets & custom fan modes 2025-11-26 21:14:46 -08:00
GrKoR
5a165d3a3d upd: version++ 2025-11-26 18:58:36 -08:00
GK
02887baa04 Merge pull request #187 from nonitex/master
Preserve swing mode and vertical vane position when toggling the othe…
2025-11-26 18:53:38 -08:00
GrKoR
5f9d2c0c0f fix: esphome 2025.11.0 breacing changes 2025-11-26 17:38:20 -08:00
nonitex
7a4bacfb9a Preserve swing mode and vertical vane position when toggling the other axis
Added helper functions to normalize louver states for consistent swing detection across different AUX models. Updated swing mode logic to utilize these helpers for improved clarity and functionality.
2025-10-20 03:52:29 +02:00
15 changed files with 5215 additions and 376 deletions

1
.gitignore vendored
View File

@@ -3,7 +3,6 @@
# You can modify this file to suit your needs.
**/.vscode/
**/.esphome/
**/esphome/
**/.pioenvs/
**/.piolibdeps/
**/lib/

View File

@@ -0,0 +1,241 @@
#pragma once
#include "aux_ac.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
namespace esphome
{
namespace aux_ac
{
// **************************************** DISPLAY ACTIONS ****************************************
template <typename... Ts>
class AirConDisplayOffAction : public Action<Ts...>
{
public:
explicit AirConDisplayOffAction(AirCon *ac) : ac_(ac) {}
void play(const Ts &...x) override
{
this->ac_->displayOffSequence();
}
protected:
AirCon *ac_;
};
template <typename... Ts>
class AirConDisplayOnAction : public Action<Ts...>
{
public:
explicit AirConDisplayOnAction(AirCon *ac) : ac_(ac) {}
void play(const Ts &...x) override
{
this->ac_->displayOnSequence();
}
protected:
AirCon *ac_;
};
// **************************************** VERTICAL LOUVER ACTIONS ****************************************
template <typename... Ts>
class AirConVLouverSwingAction : public Action<Ts...>
{
public:
explicit AirConVLouverSwingAction(AirCon *ac) : ac_(ac) {}
void play(const Ts &...x) override
{
this->ac_->setVLouverSwingSequence();
}
protected:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverStopAction : public Action<Ts...>
{
public:
explicit AirConVLouverStopAction(AirCon *ac) : ac_(ac) {}
void play(const Ts &...x) override
{
this->ac_->setVLouverStopSequence();
}
protected:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverTopAction : public Action<Ts...>
{
public:
explicit AirConVLouverTopAction(AirCon *ac) : ac_(ac) {}
void play(const Ts &...x) override
{
this->ac_->setVLouverTopSequence();
}
protected:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverMiddleAboveAction : public Action<Ts...>
{
public:
explicit AirConVLouverMiddleAboveAction(AirCon *ac) : ac_(ac) {}
void play(const Ts &...x) override
{
this->ac_->setVLouverMiddleAboveSequence();
}
protected:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverMiddleAction : public Action<Ts...>
{
public:
explicit AirConVLouverMiddleAction(AirCon *ac) : ac_(ac) {}
void play(const Ts &...x) override
{
this->ac_->setVLouverMiddleSequence();
}
protected:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverMiddleBelowAction : public Action<Ts...>
{
public:
explicit AirConVLouverMiddleBelowAction(AirCon *ac) : ac_(ac) {}
void play(const Ts &...x) override
{
this->ac_->setVLouverMiddleBelowSequence();
}
protected:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverBottomAction : public Action<Ts...>
{
public:
explicit AirConVLouverBottomAction(AirCon *ac) : ac_(ac) {}
void play(const Ts &...x) override
{
this->ac_->setVLouverBottomSequence();
}
protected:
AirCon *ac_;
};
template <typename... Ts>
class AirConVLouverSetAction : public Action<Ts...>
{
public:
TEMPLATABLE_VALUE(uint8_t, value);
AirConVLouverSetAction(AirCon *ac) : ac_(ac) {};
void play(const Ts &...x) override
{
vlpos_ = this->value_.value(x...);
this->ac_->setVLouverFrontendSequence((ac_vlouver_frontend)vlpos_);
};
protected:
AirCon *ac_;
uint8_t vlpos_;
};
// **************************************** SEND TEST PACKET ACTION ****************************************
template <typename... Ts>
class AirConSendTestPacketAction : public Action<Ts...>
{
public:
explicit AirConSendTestPacketAction(AirCon *ac) : ac_(ac) {}
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func)
{
this->data_func_ = func;
this->static_ = false;
}
void set_data_static(const std::vector<uint8_t> &data)
{
this->data_static_ = data;
this->static_ = true;
}
void play(const Ts &...x) override
{
if (this->static_)
{
this->ac_->sendTestPacket(this->data_static_);
}
else
{
auto val = this->data_func_(x...);
this->ac_->sendTestPacket(val);
}
}
protected:
AirCon *ac_;
bool static_{false};
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
std::vector<uint8_t> data_static_{};
};
// **************************************** POWER LIMITATION ACTIONS ****************************************
template <typename... Ts>
class AirConPowerLimitationOffAction : public Action<Ts...>
{
public:
explicit AirConPowerLimitationOffAction(AirCon *ac) : ac_(ac) {}
void play(const Ts &...x) override
{
this->ac_->powerLimitationOffSequence();
}
protected:
AirCon *ac_;
};
template <typename... Ts>
class AirConPowerLimitationOnAction : public Action<Ts...>
{
public:
TEMPLATABLE_VALUE(uint8_t, value);
AirConPowerLimitationOnAction(AirCon *ac) : ac_(ac) {};
void play(const Ts &...x) override
{
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

4124
components/aux_ac/aux_ac.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,148 +0,0 @@
#pragma once
#include <stdint.h>
#include <string.h> // for memcpy and memset, move to .cpp later if needed
namespace esphome
{
namespace aux_ac
{
class AuxFrame
{
private:
// CRC of the AUX frame
// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_crc
union frame_crc_t
{
uint16_t crc16;
uint8_t crc[2];
};
// frame header
// https://github.com/GrKoR/AUX_HVAC_Protocol#packet_header
struct frame_header_t
{
uint8_t startByte;
uint8_t _unknown1;
uint8_t frameType;
uint8_t wifi;
uint8_t pingAnswer;
uint8_t _unknown2;
uint8_t bodyLength;
uint8_t _unknown3;
};
uint8_t *_rawData = nullptr;
frame_header_t *_header = nullptr;
uint8_t *_body = nullptr;
frame_crc_t *_crc = nullptr;
bool _isValid = false;
uint8_t const AC_PACKET_START_BYTE = 0xBB;
uint8_t const AC_HEADER_SIZE = 8;
uint8_t const AC_BODY_LENGTH_OFFSET = 6;
uint8_t const AC_BUFFER_SIZE = 35; // TODO: integrate it with aux_uart.h
uint16_t _CRC16(uint8_t *data, uint8_t len)
{
uint32_t crc = 0;
uint8_t _crcBuffer[AC_BUFFER_SIZE];
memset(_crcBuffer, 0, AC_BUFFER_SIZE);
memcpy(_crcBuffer, data, len);
if ((len % 2) == 1)
len++;
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;
}
bool _checkCRC()
{
frame_crc_t crc;
crc.crc16 = _CRC16(this->_rawData, AC_HEADER_SIZE + this->_rawData[AC_BODY_LENGTH_OFFSET]);
return ((this->_crc->crc[0] == crc.crc[1]) && (this->_crc->crc[1] == crc.crc[0]));
}
void _checkFrame()
{
this->_isValid = false;
if (this->_rawData == nullptr)
return;
if (this->_header->startByte != AC_PACKET_START_BYTE)
return;
if (!this->_checkCRC())
return;
this->_isValid = true;
}
public:
AuxFrame() = default;
~AuxFrame() {};
void set_data(uint8_t *data)
{
clearData();
if (data == nullptr)
return;
this->_rawData = data;
this->_header = (frame_header_t *)this->_rawData;
this->_crc = (frame_crc_t *)(this->_rawData + AC_HEADER_SIZE + this->_header->bodyLength);
if (this->_header->bodyLength > 0)
this->_body = this->_rawData + AC_HEADER_SIZE;
else
this->_body = nullptr;
this->_checkFrame();
}
void clearData()
{
this->_rawData = nullptr;
this->_header = nullptr;
this->_crc = nullptr;
this->_body = nullptr;
this->_isValid = false;
}
bool isValid() const
{
return this->_isValid;
};
uint8_t frameSize() const
{
if (!this->isValid())
return 0;
return AC_HEADER_SIZE + this->_header->bodyLength + 2;
}
uint8_t bodyLength() const
{
if (!this->isValid())
return 0;
return this->_header->bodyLength;
}
};
} // namespace aux_ac
} // namespace esphome

View File

@@ -1,100 +0,0 @@
#include "aux_logger.h"
namespace esphome
{
namespace aux_ac
{
static const char *const TAG = "AirCon"; // TODO: verify if this tag is appropriate
/** вывод отладочной информации в лог
*
* dbgLevel - уровень сообщения, определен в ESPHome. За счет его использования можно из ESPHome управлять полнотой сведений в логе.
* msg - сообщение, выводимое в лог
* line - строка, на которой произошел вызов (удобно при отладке)
*/
void debugMsg(const /*String &*/ char *msg, uint8_t dbgLevel, unsigned int line, ...)
{
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, vl);
va_end(vl);
};
/** выводим данные пакета в лог для отладки
*
* dbgLevel - уровень сообщения, определен в ESPHome. За счет его использования можно из ESPHome управлять полнотой сведений в логе.
* packet - указатель на пакет для вывода;
* если указатель на crc равен nullptr или первый байт в буфере не AC_PACKET_START_BYTE, то считаем, что передан битый пакет
* или не пакет вовсе. Для такого выводим только массив байт.
* Для нормального пакета данные выводятся с форматированием.
* line - строка, на которой произошел вызов (удобно при отладке)
**/
void debugPrintPacket(packet_t *packet, uint8_t dbgLevel, unsigned int line)
{
// определяем, полноценный ли пакет нам передан
bool notAPacket = false;
/*
// указатель заголовка всегда установден на начало буфера
notAPacket = notAPacket || (packet->crc == nullptr);
notAPacket = notAPacket || (packet->data[0] != AC_PACKET_START_BYTE);
String st = "";
char textBuf[11];
// заполняем время получения пакета
memset(textBuf, 0, 11);
sprintf(textBuf, "%010" PRIu32, 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);
*/
}
} // namespace aux_ac
} // namespace esphome

View File

@@ -1,15 +0,0 @@
#pragma once
#include <stdint.h>
#include "esphome/core/log.h"
namespace esphome
{
namespace aux_ac
{
using packet_t = uint8_t; // TODO: Replace with actual packet_t definition
void debugMsg(const char *msg, uint8_t dbgLevel, unsigned int line = 0, ...);
void debugPrintPacket(packet_t *packet, uint8_t dbgLevel = ESPHOME_LOG_LEVEL_DEBUG, unsigned int line = __LINE__);
} // namespace aux_ac
} // namespace esphome

View File

@@ -1,68 +0,0 @@
#include "aux_uart.h"
namespace esphome
{
namespace aux_ac
{
void AuxUart::send_frame(const std::vector<uint8_t> &frame)
{
this->write_array(frame);
}
bool AuxUart::read_frame(std::vector<uint8_t> &frame)
{
// Check if enough data is available
if (this->available() < 3)
return false;
// Peek at the first 3 bytes to determine the frame length
uint8_t header[3];
if (!this->peek_array(header, 3))
return false;
// Validate start byte
if (header[0] != 0xAA)
{
// Invalid start byte, discard one byte and return false
uint8_t discard;
this->read_byte(&discard);
return false;
}
// Determine frame length from the second byte
size_t frame_length = header[1];
if (frame_length < 3 || frame_length > AC_BUFFER_SIZE)
{
// Invalid length, discard one byte and return false
uint8_t discard;
this->read_byte(&discard);
return false;
}
// Check if the full frame is available
if (this->available() < frame_length)
return false;
// Read the full frame into the internal buffer
if (!this->read_array(this->_data, frame_length))
return false;
// Validate checksum
uint8_t checksum = 0;
for (size_t i = 0; i < frame_length - 1; i++)
{
checksum += this->_data[i];
}
if (checksum != this->_data[frame_length - 1])
{
// Invalid checksum, discard the frame and return false
return false;
}
// Copy the valid frame to the output vector
frame.assign(this->_data, this->_data + frame_length);
return true;
}
} // namespace aux_ac
} // namespace esphome

View File

@@ -1,30 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#define AC_BUFFER_SIZE 35
using namespace esphome::uart;
namespace esphome
{
namespace aux_ac
{
class AuxUart : public UARTDevice
{
public:
AuxUart() = delete;
explicit AuxUart(UARTComponent *parent) : UARTDevice(parent) {}
~AuxUart() = default;
void send_frame(const std::vector<uint8_t> &frame);
bool read_frame(std::vector<uint8_t> &frame);
protected:
// Internal buffer for incoming data
uint8_t _data[AC_BUFFER_SIZE];
};
} // namespace aux_ac
} // namespace esphome

View File

@@ -2,45 +2,362 @@ import logging
from esphome.core import CORE, Define
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components import uart, climate
from esphome.components import climate, uart, sensor, binary_sensor, text_sensor
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.const import (
CONF_CUSTOM_FAN_MODES,
CONF_CUSTOM_PRESETS,
CONF_DATA,
CONF_ID,
CONF_INTERNAL,
CONF_OPTIMISTIC,
CONF_PERIOD,
CONF_POSITION,
CONF_SUPPORTED_MODES,
CONF_SUPPORTED_SWING_MODES,
CONF_SUPPORTED_PRESETS,
CONF_TIMEOUT,
CONF_UART_ID,
UNIT_CELSIUS,
UNIT_PERCENT,
ICON_POWER,
ICON_THERMOMETER,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_POWER_FACTOR,
STATE_CLASS_MEASUREMENT,
__version__
)
from esphome.components.climate import (
ClimateMode,
ClimatePreset,
ClimateSwingMode,
)
AUX_AC_FIRMWARE_VERSION = '0.2.17'
AUX_AC_FIRMWARE_VERSION = '0.3.1'
AC_PACKET_TIMEOUT_MIN = 150
AC_PACKET_TIMEOUT_MAX = 600
AC_POWER_LIMIT_MIN = 30
AC_POWER_LIMIT_MAX = 100
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@GrKoR"]
DEPENDENCIES = ["uart"]
DEPENDENCIES = ["climate", "uart"]
AUTO_LOAD = ["sensor", "binary_sensor", "text_sensor"]
CONF_SHOW_ACTION = "show_action"
CONF_INDOOR_TEMPERATURE = "indoor_temperature"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
ICON_OUTDOOR_TEMPERATURE = "mdi:home-thermometer-outline"
CONF_INBOUND_TEMPERATURE = "inbound_temperature"
ICON_INBOUND_TEMPERATURE = "mdi:thermometer-plus"
CONF_OUTBOUND_TEMPERATURE = "outbound_temperature"
ICON_OUTBOUND_TEMPERATURE = "mdi:thermometer-minus"
CONF_COMPRESSOR_TEMPERATURE = "compressor_temperature"
ICON_COMPRESSOR_TEMPERATURE = "mdi:thermometer-lines"
CONF_DISPLAY_STATE = "display_state"
CONF_INVERTER_POWER = "inverter_power"
CONF_INVERTER_POWER_DEPRICATED = "invertor_power"
CONF_DEFROST_STATE = "defrost_state"
ICON_DEFROST = "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"
CONF_VLOUVER_STATE = "vlouver_state"
ICON_VLOUVER_STATE = "mdi:compare-vertical"
CONF_LIMIT = "limit"
CONF_INVERTER_POWER_LIMIT_VALUE = "inverter_power_limit_value"
ICON_INVERTER_POWER_LIMIT_VALUE = "mdi:meter-electric-outline"
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")
AirCon = aux_ac_ns.class_("AirCon", climate.Climate, cg.Component)
Capabilities = aux_ac_ns.namespace("Constants")
# 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
)
# 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)
AirConVLouverMiddleAboveAction = aux_ac_ns.class_(
"AirConVLouverMiddleAboveAction", automation.Action
)
AirConVLouverMiddleAction = aux_ac_ns.class_(
"AirConVLouverMiddleAction", automation.Action
)
AirConVLouverMiddleBelowAction = aux_ac_ns.class_(
"AirConVLouverMiddleBelowAction", automation.Action
)
AirConVLouverBottomAction = aux_ac_ns.class_(
"AirConVLouverBottomAction", automation.Action
)
AirConVLouverSetAction = aux_ac_ns.class_(
"AirConVLouverSetAction", automation.Action
)
# power limitation actions
AirConPowerLimitationOffAction = aux_ac_ns.class_(
"AirConPowerLimitationOffAction", automation.Action
)
AirConPowerLimitationOnAction = aux_ac_ns.class_(
"AirConPowerLimitationOnAction", automation.Action
)
def use_new_api():
esphome_current_version = tuple(map(int, __version__.split('.')))
esphome_bc_version = tuple(map(int, "2025.11.0".split('.')))
return esphome_current_version >= esphome_bc_version
def validate_packet_timeout(value):
minV = AC_PACKET_TIMEOUT_MIN
maxV = AC_PACKET_TIMEOUT_MAX
if value in range(minV, maxV+1):
return cv.Schema(cv.uint32_t)(value)
raise cv.Invalid(f"Timeout should be in range: {minV}..{maxV}.")
def validate_power_limit_range(value):
minV = AC_POWER_LIMIT_MIN
maxV = AC_POWER_LIMIT_MAX
if value in range(minV, maxV+1):
return cv.Schema(cv.uint32_t)(value)
raise cv.Invalid(f"Power limit should be in range: {minV}..{maxV}")
ALLOWED_CLIMATE_MODES = {
"HEAT_COOL": ClimateMode.CLIMATE_MODE_HEAT_COOL,
"COOL": ClimateMode.CLIMATE_MODE_COOL,
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
"DRY": ClimateMode.CLIMATE_MODE_DRY,
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
}
validate_modes = cv.enum(ALLOWED_CLIMATE_MODES, upper=True)
ALLOWED_CLIMATE_PRESETS = {
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
}
validate_presets = cv.enum(ALLOWED_CLIMATE_PRESETS, upper=True)
ALLOWED_CLIMATE_SWING_MODES = {
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
}
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
CUSTOM_FAN_MODES = {
"MUTE": Capabilities.MUTE,
"TURBO": Capabilities.TURBO,
}
validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True)
CUSTOM_PRESETS = {
"CLEAN": Capabilities.CLEAN,
"HEALTH": Capabilities.HEALTH,
"ANTIFUNGUS": Capabilities.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")
AuxUart = aux_ac_ns.class_("AuxUart", uart.UARTDevice)
def output_info(config):
_LOGGER.info("AUX_AC firmware version: %s", AUX_AC_FIRMWARE_VERSION)
return config
CONFIG_SCHEMA = cv.All(
uart.UART_DEVICE_SCHEMA
.extend(
climate.climate_schema(climate.Climate).extend(
{
cv.GenerateID(): cv.declare_id(AuxUart),
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_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."
),
cv.Optional(CONF_INVERTER_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_POWER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_INDOOR_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_OUTDOOR_TEMPERATURE,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_INBOUND_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_INBOUND_TEMPERATURE,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_OUTBOUND_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_OUTBOUND_TEMPERATURE,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_COMPRESSOR_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
icon=ICON_COMPRESSOR_TEMPERATURE,
accuracy_decimals=0,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_VLOUVER_STATE): sensor.sensor_schema(
icon=ICON_VLOUVER_STATE,
accuracy_decimals=0,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_DISPLAY_STATE): binary_sensor.binary_sensor_schema(
icon=ICON_DISPLAY,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_DEFROST_STATE): binary_sensor.binary_sensor_schema(
icon=ICON_DEFROST,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_PRESET_REPORTER): text_sensor.text_sensor_schema(
icon=ICON_PRESET_REPORTER,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_INVERTER_POWER_LIMIT_VALUE): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_INVERTER_POWER_LIMIT_VALUE,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_INVERTER_POWER_LIMIT_STATE): binary_sensor.binary_sensor_schema(
icon=ICON_INVERTER_POWER_LIMIT_STATE,
).extend(
{
cv.Optional(CONF_INTERNAL, default="true"): cv.boolean,
}
),
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(validate_modes),
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
validate_swing_modes
),
cv.Optional(CONF_SUPPORTED_PRESETS): cv.ensure_list(validate_presets),
cv.Optional(CONF_CUSTOM_PRESETS): cv.ensure_list(validate_custom_presets),
cv.Optional(CONF_CUSTOM_FAN_MODES): cv.ensure_list(
validate_custom_fan_modes
),
}
),
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
output_info,
)
async def to_code(config):
CORE.add_define(
Define("AUX_AC_FIRMWARE_VERSION", '"'+AUX_AC_FIRMWARE_VERSION+'"')
)
CORE.add_define(
Define("AUX_AC_PACKET_TIMEOUT_MIN", AC_PACKET_TIMEOUT_MIN)
)
CORE.add_define(
Define("AUX_AC_PACKET_TIMEOUT_MAX", AC_PACKET_TIMEOUT_MAX)
)
CORE.add_define(
Define("AUX_AC_MIN_INVERTER_POWER_LIMIT", AC_POWER_LIMIT_MIN)
)
CORE.add_define(
Define("AUX_AC_MAX_INVERTER_POWER_LIMIT", AC_POWER_LIMIT_MAX)
)
var = cg.new_Pvariable(config[CONF_ID])
# await cg.register_component(var, config)
await cg.register_component(var, config)
await climate.register_climate(var, config)
parent = await cg.get_variable(config[CONF_UART_ID])
cg.add(var.initAC(parent))
@@ -116,7 +433,190 @@ async def to_code(config):
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:
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
if CONF_CUSTOM_FAN_MODES in config:
cg.add(var.set_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
if use_new_api():
if CONF_CUSTOM_PRESETS in config:
presets = config[CONF_CUSTOM_PRESETS]
c_str_presets = [cg.RawExpression(f"aux_ac::Constants::{p}.c_str()") for p in presets]
cg.add(var.set_custom_presets(c_str_presets))
if CONF_CUSTOM_FAN_MODES in config:
fan_modes = config[CONF_CUSTOM_FAN_MODES]
c_str_fan_modes = [cg.RawExpression(f"aux_ac::Constants::{p}.c_str()") for p in fan_modes]
cg.add(var.set_custom_fan_modes(c_str_fan_modes))
else:
if CONF_CUSTOM_PRESETS in config:
cg.add(var.set_custom_presets(config[CONF_CUSTOM_PRESETS]))
if CONF_CUSTOM_FAN_MODES in 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)
@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)
VLOUVER_ACTION_SCHEMA = maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(AirCon),
}
)
@automation.register_action(
"aux_ac.vlouver_stop", AirConVLouverStopAction, VLOUVER_ACTION_SCHEMA
)
async def vlouver_stop_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"aux_ac.vlouver_swing", AirConVLouverSwingAction, VLOUVER_ACTION_SCHEMA
)
async def vlouver_swing_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"aux_ac.vlouver_top", AirConVLouverTopAction, VLOUVER_ACTION_SCHEMA
)
async def vlouver_top_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"aux_ac.vlouver_middle_above", AirConVLouverMiddleAboveAction, VLOUVER_ACTION_SCHEMA
)
async def vlouver_middle_above_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"aux_ac.vlouver_middle", AirConVLouverMiddleAction, VLOUVER_ACTION_SCHEMA
)
async def vlouver_middle_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"aux_ac.vlouver_middle_below", AirConVLouverMiddleBelowAction, VLOUVER_ACTION_SCHEMA
)
async def vlouver_middle_below_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
@automation.register_action(
"aux_ac.vlouver_bottom", AirConVLouverBottomAction, VLOUVER_ACTION_SCHEMA
)
async def vlouver_bottom_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)
VLOUVER_SET_ACTION_SCHEMA = cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(AirCon),
cv.Required(CONF_POSITION): cv.templatable(cv.int_range(0, 6)),
}
)
@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)
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)
POWER_LIMITATION_ON_ACTION_SCHEMA = cv.Schema(
{
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)
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

10
examples/advanced/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphome/
**/.pioenvs/
**/.piolibdeps/
**/lib/
**/src/
**/platformio.ini
/secrets.yaml

View File

@@ -0,0 +1,233 @@
# DON'T COMPILE THIS FILE
# This file contains common settings for all air conditioners of your house
external_components:
- source: github://GrKoR/esphome_aux_ac_component
components: [ aux_ac ]
refresh: 0s
esphome:
name: $devicename
esp8266:
board: esp12e
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_pass
manual_ip:
static_ip: ${wifi_ip}
gateway: !secret wifi_gateway
subnet: !secret wifi_subnet
ap:
ssid: ${upper_devicename} Hotspot
password: !secret wifi_ap_pass
use_address: ${wifi_ota_ip}
captive_portal:
debug:
logger:
level: DEBUG
baud_rate: 0
# set hardware_uart to UART1 and comment out baud_rate above in case of boot crashes
# it is suitable if you need hardware loggin
# hardware_uart: UART1
api:
password: !secret api_pass
ota:
- platform: esphome
password: !secret ota_pass
web_server:
port: 80
auth:
username: !secret web_server_user
password: !secret web_server_password
# UART0 configuration for AUX air conditioner communication
uart:
id: ac_uart_bus
# ATTENTION! Use GPIO4 (D2) and GPIO5 (D1) as the TX and RX for NodeMCU-like boards!
tx_pin: GPIO1
rx_pin: GPIO3
baud_rate: 4800
data_bits: 8
parity: EVEN
stop_bits: 1
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}_inverter_power
internal: false
preset_reporter:
name: ${upper_devicename} Preset Reporter
id: ${devicename}_preset_reporter
internal: false
vlouver_state:
name: ${upper_devicename} VLouvers State
id: ${devicename}_vlouver_state
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
sensor:
# just wifi signal strength for debug purpose only
- platform: wifi_signal
name: ${upper_devicename} WiFi Signal
update_interval: 30s
unit_of_measurement: "dBa"
accuracy_decimals: 0
- platform: uptime
name: ${upper_devicename} Uptime Sensor
switch:
- platform: template
name: ${upper_devicename} 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
button:
- platform: template
name: ${upper_devicename} VLouver Stop
icon: "mdi:circle-small"
on_press:
- aux_ac.vlouver_stop: aux_id
- platform: template
name: ${upper_devicename} VLouver Swing
icon: "mdi:pan-vertical"
on_press:
- aux_ac.vlouver_swing: aux_id
- platform: template
name: ${upper_devicename} VLouver Top
icon: "mdi:pan-up"
on_press:
- aux_ac.vlouver_top: aux_id
- platform: template
name: ${upper_devicename} VLouver Middle Above
icon: "mdi:pan-top-left"
on_press:
- aux_ac.vlouver_middle_above: aux_id
- platform: template
name: ${upper_devicename} VLouver Middle
icon: "mdi:pan-left"
on_press:
- aux_ac.vlouver_middle: aux_id
- platform: template
name: ${upper_devicename} VLouver Middle Below
icon: "mdi:pan-bottom-left"
on_press:
- aux_ac.vlouver_middle_below: aux_id
- platform: template
name: ${upper_devicename} VLouver Bottom
icon: "mdi:pan-down"
on_press:
- aux_ac.vlouver_bottom: aux_id
number:
- platform: template
name: ${upper_devicename} Vertical Louvers
id: ${devicename}_vlouver
icon: "mdi:circle-small"
mode: "slider"
min_value: 0
max_value: 6
step: 1
update_interval: 2s
lambda: |-
return id(${devicename}_vlouver_state).state;
set_action:
then:
- aux_ac.vlouver_set:
id: aux_id
position: !lambda "return x;"
- platform: template
name: ${upper_devicename} Power Limit
id: ${devicename}_power_limit
icon: "mdi:battery-unknown"
mode: "slider"
min_value: 30
max_value: 100
step: 1
set_action:
then:
- lambda: !lambda "id(aux_id).powerLimitationOnSequence( x );"

View File

@@ -0,0 +1,17 @@
# compile this file with ESPHome
# This file contains unique settings for specific air conditioner
#===================================================================================
# KITCHEN AIR CONDITIONER
#===================================================================================
substitutions:
devicename: kitchen_ac
upper_devicename: Kitchen AC
# use different wifi_ip and wifi_ota_ip in case of esp ip-address change
# if ip haven't changed wifi_ip and wifi_ota_ip should be the same
wifi_ip: !secret wifi_ip_kitchen
wifi_ota_ip: !secret wifi_ota_ip_kitchen
<<: !include ac_common.yaml

View File

@@ -0,0 +1,17 @@
# compile this file with ESPHome
# This file contains unique settings for specific air conditioner
#===================================================================================
# LIVINGROOM AIR CONDITIONER
#===================================================================================
substitutions:
devicename: livingroom_ac
upper_devicename: Livingroom AC
# use different wifi_ip and wifi_ota_ip in case of esp ip-address change
# if ip haven't changed wifi_ip and wifi_ota_ip should be the same
wifi_ip: !secret wifi_ip_livingroom
wifi_ota_ip: !secret wifi_ota_ip_livingroom
<<: !include ac_common.yaml

10
examples/simple/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphome/
**/.pioenvs/
**/.piolibdeps/
**/lib/
**/src/
**/platformio.ini
/secrets.yaml

View File

@@ -0,0 +1,49 @@
external_components:
- source: github://GrKoR/esphome_aux_ac_component
components: [ aux_ac ]
refresh: 0s
esphome:
name: aux_air_conditioner
esp8266:
board: esp12e
# don't forget to set your's wifi settings!
wifi:
ssid: "WIFI SSID"
password: "seCRETpassWORD"
manual_ip:
static_ip: 192.168.0.2
gateway: 192.168.0.1
subnet: 255.255.255.0
ap:
ssid: AUX Hotspot
password: "seCREThotSPOTpassWORD"
captive_portal:
debug:
logger:
level: DEBUG
baud_rate: 0
api:
ota:
- platform: esphome
# UART0 configuration for AUX air conditioner communication
uart:
id: ac_uart_bus
# ATTENTION! Use GPIO4 (D2) and GPIO5 (D1) as the TX and RX for NodeMCU-like boards!
tx_pin: GPIO1
rx_pin: GPIO3
baud_rate: 4800
data_bits: 8
parity: EVEN
stop_bits: 1
climate:
- platform: aux_ac
name: "AC Name"