Merge branch 'esphome:dev' into nvds-status-info

This commit is contained in:
NP v/d Spek 2023-11-21 13:15:31 +01:00 committed by GitHub
commit cc4cc4774f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1485 additions and 915 deletions

View file

@ -48,6 +48,8 @@ RUN \
libfreetype-dev=2.12.1+dfsg-5 \ libfreetype-dev=2.12.1+dfsg-5 \
libssl-dev=3.0.11-1~deb12u2 \ libssl-dev=3.0.11-1~deb12u2 \
libffi-dev=3.4.4-1 \ libffi-dev=3.4.4-1 \
libopenjp2-7=2.5.0-2 \
libtiff6=4.5.0-6 \
cargo=0.66.0+ds1-1 \ cargo=0.66.0+ds1-1 \
pkg-config=1.8.1-1 \ pkg-config=1.8.1-1 \
gcc-arm-linux-gnueabihf=4:12.2.0-3; \ gcc-arm-linux-gnueabihf=4:12.2.0-3; \
@ -68,7 +70,7 @@ ENV \
# See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian
RUN \ RUN \
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \ ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \
fi fi
RUN \ RUN \

View file

@ -8,7 +8,6 @@ from typing import Any
from aioesphomeapi import APIClient from aioesphomeapi import APIClient
from aioesphomeapi.api_pb2 import SubscribeLogsResponse from aioesphomeapi.api_pb2 import SubscribeLogsResponse
from aioesphomeapi.log_runner import async_run from aioesphomeapi.log_runner import async_run
from zeroconf.asyncio import AsyncZeroconf
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__ from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
from esphome.core import CORE from esphome.core import CORE
@ -28,14 +27,12 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
if CONF_ENCRYPTION in conf: if CONF_ENCRYPTION in conf:
noise_psk = conf[CONF_ENCRYPTION][CONF_KEY] noise_psk = conf[CONF_ENCRYPTION][CONF_KEY]
_LOGGER.info("Starting log output from %s using esphome API", address) _LOGGER.info("Starting log output from %s using esphome API", address)
aiozc = AsyncZeroconf()
cli = APIClient( cli = APIClient(
address, address,
port, port,
password, password,
client_info=f"ESPHome Logs {__version__}", client_info=f"ESPHome Logs {__version__}",
noise_psk=noise_psk, noise_psk=noise_psk,
zeroconf_instance=aiozc.zeroconf,
) )
dashboard = CORE.dashboard dashboard = CORE.dashboard
@ -48,12 +45,10 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
text = text.replace("\033", "\\033") text = text.replace("\033", "\\033")
print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}") print(f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]{text}")
stop = await async_run(cli, on_log, aio_zeroconf_instance=aiozc, name=name) stop = await async_run(cli, on_log, name=name)
try: try:
while True: await asyncio.Event().wait()
await asyncio.sleep(60)
finally: finally:
await aiozc.async_close()
await stop() await stop()

View file

@ -3,23 +3,26 @@ from typing import Union, Optional
from pathlib import Path from pathlib import Path
import logging import logging
import os import os
import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p from esphome.helpers import copy_file_if_changed, write_file_if_changed, mkdir_p
from esphome.const import ( from esphome.const import (
CONF_ADVANCED,
CONF_BOARD, CONF_BOARD,
CONF_COMPONENTS, CONF_COMPONENTS,
CONF_ESPHOME,
CONF_FRAMEWORK, CONF_FRAMEWORK,
CONF_IGNORE_EFUSE_MAC_CRC,
CONF_NAME, CONF_NAME,
CONF_PATH,
CONF_PLATFORMIO_OPTIONS,
CONF_REF,
CONF_REFRESH,
CONF_SOURCE, CONF_SOURCE,
CONF_TYPE, CONF_TYPE,
CONF_URL,
CONF_VARIANT, CONF_VARIANT,
CONF_VERSION, CONF_VERSION,
CONF_ADVANCED,
CONF_REFRESH,
CONF_PATH,
CONF_URL,
CONF_REF,
CONF_IGNORE_EFUSE_MAC_CRC,
KEY_CORE, KEY_CORE,
KEY_FRAMEWORK_VERSION, KEY_FRAMEWORK_VERSION,
KEY_NAME, KEY_NAME,
@ -327,6 +330,32 @@ def _detect_variant(value):
return value return value
def final_validate(config):
if CONF_PLATFORMIO_OPTIONS not in fv.full_config.get()[CONF_ESPHOME]:
return config
pio_flash_size_key = "board_upload.flash_size"
pio_partitions_key = "board_build.partitions"
if (
CONF_PARTITIONS in config
and pio_partitions_key
in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
):
raise cv.Invalid(
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
)
if (
pio_flash_size_key
in fv.full_config.get()[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS]
):
raise cv.Invalid(
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
)
return config
CONF_PLATFORM_VERSION = "platform_version" CONF_PLATFORM_VERSION = "platform_version"
ARDUINO_FRAMEWORK_SCHEMA = cv.All( ARDUINO_FRAMEWORK_SCHEMA = cv.All(
@ -387,6 +416,7 @@ FRAMEWORK_SCHEMA = cv.typed_schema(
FLASH_SIZES = [ FLASH_SIZES = [
"2MB",
"4MB", "4MB",
"8MB", "8MB",
"16MB", "16MB",
@ -394,6 +424,7 @@ FLASH_SIZES = [
] ]
CONF_FLASH_SIZE = "flash_size" CONF_FLASH_SIZE = "flash_size"
CONF_PARTITIONS = "partitions"
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@ -401,6 +432,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of( cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
*FLASH_SIZES, upper=True *FLASH_SIZES, upper=True
), ),
cv.Optional(CONF_PARTITIONS): cv.file_,
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA,
} }
@ -410,6 +442,9 @@ CONFIG_SCHEMA = cv.All(
) )
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
async def to_code(config): async def to_code(config):
cg.add_platformio_option("board", config[CONF_BOARD]) cg.add_platformio_option("board", config[CONF_BOARD])
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
@ -462,7 +497,10 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False) add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
cg.add_platformio_option("board_build.partitions", "partitions.csv") if CONF_PARTITIONS in config:
cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])
else:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
@ -507,7 +545,10 @@ async def to_code(config):
[f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"],
) )
cg.add_platformio_option("board_build.partitions", "partitions.csv") if CONF_PARTITIONS in config:
cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS])
else:
cg.add_platformio_option("board_build.partitions", "partitions.csv")
cg.add_define( cg.add_define(
"USE_ARDUINO_VERSION_CODE", "USE_ARDUINO_VERSION_CODE",
@ -518,6 +559,7 @@ async def to_code(config):
APP_PARTITION_SIZES = { APP_PARTITION_SIZES = {
"2MB": 0x0C0000, # 768 KB
"4MB": 0x1C0000, # 1792 KB "4MB": 0x1C0000, # 1792 KB
"8MB": 0x3C0000, # 3840 KB "8MB": 0x3C0000, # 3840 KB
"16MB": 0x7C0000, # 7936 KB "16MB": 0x7C0000, # 7936 KB

View file

@ -38,16 +38,20 @@ PROTOCOL_MIN_TEMPERATURE = 16.0
PROTOCOL_MAX_TEMPERATURE = 30.0 PROTOCOL_MAX_TEMPERATURE = 30.0
PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0 PROTOCOL_TARGET_TEMPERATURE_STEP = 1.0
PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5 PROTOCOL_CURRENT_TEMPERATURE_STEP = 0.5
PROTOCOL_CONTROL_PACKET_SIZE = 10
CODEOWNERS = ["@paveldn"] CODEOWNERS = ["@paveldn"]
AUTO_LOAD = ["sensor"] AUTO_LOAD = ["sensor"]
DEPENDENCIES = ["climate", "uart"] DEPENDENCIES = ["climate", "uart"]
CONF_WIFI_SIGNAL = "wifi_signal" CONF_ALTERNATIVE_SWING_CONTROL = "alternative_swing_control"
CONF_ANSWER_TIMEOUT = "answer_timeout" CONF_ANSWER_TIMEOUT = "answer_timeout"
CONF_CONTROL_METHOD = "control_method"
CONF_CONTROL_PACKET_SIZE = "control_packet_size"
CONF_DISPLAY = "display" CONF_DISPLAY = "display"
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature" CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_VERTICAL_AIRFLOW = "vertical_airflow" CONF_VERTICAL_AIRFLOW = "vertical_airflow"
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow" CONF_WIFI_SIGNAL = "wifi_signal"
PROTOCOL_HON = "HON" PROTOCOL_HON = "HON"
PROTOCOL_SMARTAIR2 = "SMARTAIR2" PROTOCOL_SMARTAIR2 = "SMARTAIR2"
@ -107,6 +111,13 @@ SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
"SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP, "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
} }
HonControlMethod = haier_ns.enum("HonControlMethod", True)
SUPPORTED_HON_CONTROL_METHODS = {
"MONITOR_ONLY": HonControlMethod.MONITOR_ONLY,
"SET_GROUP_PARAMETERS": HonControlMethod.SET_GROUP_PARAMETERS,
"SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER,
}
def validate_visual(config): def validate_visual(config):
if CONF_VISUAL in config: if CONF_VISUAL in config:
@ -184,6 +195,9 @@ CONFIG_SCHEMA = cv.All(
PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend(
{ {
cv.GenerateID(): cv.declare_id(Smartair2Climate), cv.GenerateID(): cv.declare_id(Smartair2Climate),
cv.Optional(
CONF_ALTERNATIVE_SWING_CONTROL, default=False
): cv.boolean,
cv.Optional( cv.Optional(
CONF_SUPPORTED_PRESETS, CONF_SUPPORTED_PRESETS,
default=list( default=list(
@ -197,7 +211,15 @@ CONFIG_SCHEMA = cv.All(
PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend(
{ {
cv.GenerateID(): cv.declare_id(HonClimate), cv.GenerateID(): cv.declare_id(HonClimate),
cv.Optional(
CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS"
): cv.ensure_list(
cv.enum(SUPPORTED_HON_CONTROL_METHODS, upper=True)
),
cv.Optional(CONF_BEEPER, default=True): cv.boolean, cv.Optional(CONF_BEEPER, default=True): cv.boolean,
cv.Optional(
CONF_CONTROL_PACKET_SIZE, default=PROTOCOL_CONTROL_PACKET_SIZE
): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
cv.Optional( cv.Optional(
CONF_SUPPORTED_PRESETS, CONF_SUPPORTED_PRESETS,
default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()), default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()),
@ -408,6 +430,8 @@ async def to_code(config):
await climate.register_climate(var, config) await climate.register_climate(var, config)
cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL]))
if CONF_CONTROL_METHOD in config:
cg.add(var.set_control_method(config[CONF_CONTROL_METHOD]))
if CONF_BEEPER in config: if CONF_BEEPER in config:
cg.add(var.set_beeper_state(config[CONF_BEEPER])) cg.add(var.set_beeper_state(config[CONF_BEEPER]))
if CONF_DISPLAY in config: if CONF_DISPLAY in config:
@ -423,5 +447,15 @@ async def to_code(config):
cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS])) cg.add(var.set_supported_presets(config[CONF_SUPPORTED_PRESETS]))
if CONF_ANSWER_TIMEOUT in config: if CONF_ANSWER_TIMEOUT in config:
cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT])) cg.add(var.set_answer_timeout(config[CONF_ANSWER_TIMEOUT]))
if CONF_ALTERNATIVE_SWING_CONTROL in config:
cg.add(
var.set_alternative_swing_control(config[CONF_ALTERNATIVE_SWING_CONTROL])
)
if CONF_CONTROL_PACKET_SIZE in config:
cg.add(
var.set_extra_control_packet_bytes_size(
config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE
)
)
# https://github.com/paveldn/HaierProtocol # https://github.com/paveldn/HaierProtocol
cg.add_library("pavlodn/HaierProtocol", "0.9.20") cg.add_library("pavlodn/HaierProtocol", "0.9.24")

View file

@ -19,56 +19,45 @@ constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000;
constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000; constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000; constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400; constexpr size_t CONTROL_MESSAGES_INTERVAL_MS = 400;
constexpr size_t CONTROL_TIMEOUT_MS = 7000;
constexpr size_t NO_COMMAND = 0xFF; // Indicate that there is no command supplied
#if (HAIER_LOG_LEVEL > 4)
// To reduce size of binary this function only available when log level is Verbose
const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) { const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
static const char *phase_names[] = { static const char *phase_names[] = {
"SENDING_INIT_1", "SENDING_INIT_1",
"WAITING_INIT_1_ANSWER",
"SENDING_INIT_2", "SENDING_INIT_2",
"WAITING_INIT_2_ANSWER",
"SENDING_FIRST_STATUS_REQUEST", "SENDING_FIRST_STATUS_REQUEST",
"WAITING_FIRST_STATUS_ANSWER",
"SENDING_ALARM_STATUS_REQUEST", "SENDING_ALARM_STATUS_REQUEST",
"WAITING_ALARM_STATUS_ANSWER",
"IDLE", "IDLE",
"UNKNOWN",
"SENDING_STATUS_REQUEST", "SENDING_STATUS_REQUEST",
"WAITING_STATUS_ANSWER",
"SENDING_UPDATE_SIGNAL_REQUEST", "SENDING_UPDATE_SIGNAL_REQUEST",
"WAITING_UPDATE_SIGNAL_ANSWER",
"SENDING_SIGNAL_LEVEL", "SENDING_SIGNAL_LEVEL",
"WAITING_SIGNAL_LEVEL_ANSWER",
"SENDING_CONTROL", "SENDING_CONTROL",
"WAITING_CONTROL_ANSWER", "SENDING_ACTION_COMMAND",
"SENDING_POWER_ON_COMMAND",
"WAITING_POWER_ON_ANSWER",
"SENDING_POWER_OFF_COMMAND",
"WAITING_POWER_OFF_ANSWER",
"UNKNOWN" // Should be the last! "UNKNOWN" // Should be the last!
}; };
static_assert(
(sizeof(phase_names) / sizeof(char *)) == (((int) ProtocolPhases::NUM_PROTOCOL_PHASES) + 1),
"Wrong phase_names array size. Please, make sure that this array is aligned with the enum ProtocolPhases");
int phase_index = (int) phase; int phase_index = (int) phase;
if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0)) if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES; phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
return phase_names[phase_index]; return phase_names[phase_index];
} }
#endif
bool check_timeout(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
size_t timeout) {
return std::chrono::duration_cast<std::chrono::milliseconds>(now - tpoint).count() > timeout;
}
HaierClimateBase::HaierClimateBase() HaierClimateBase::HaierClimateBase()
: haier_protocol_(*this), : haier_protocol_(*this),
protocol_phase_(ProtocolPhases::SENDING_INIT_1), protocol_phase_(ProtocolPhases::SENDING_INIT_1),
action_request_(ActionRequest::NO_ACTION),
display_status_(true), display_status_(true),
health_mode_(false), health_mode_(false),
force_send_control_(false), force_send_control_(false),
forced_publish_(false),
forced_request_status_(false), forced_request_status_(false),
first_control_attempt_(false),
reset_protocol_request_(false), reset_protocol_request_(false),
send_wifi_signal_(true) { send_wifi_signal_(true),
use_crc_(false) {
this->traits_ = climate::ClimateTraits(); this->traits_ = climate::ClimateTraits();
this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, this->traits_.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY,
@ -84,42 +73,43 @@ HaierClimateBase::~HaierClimateBase() {}
void HaierClimateBase::set_phase(ProtocolPhases phase) { void HaierClimateBase::set_phase(ProtocolPhases phase) {
if (this->protocol_phase_ != phase) { if (this->protocol_phase_ != phase) {
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase)); ESP_LOGV(TAG, "Phase transition: %s => %s", phase_to_string_(this->protocol_phase_), phase_to_string_(phase));
#else
ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase);
#endif
this->protocol_phase_ = phase; this->protocol_phase_ = phase;
} }
} }
bool HaierClimateBase::check_timeout_(std::chrono::steady_clock::time_point now, void HaierClimateBase::reset_phase_() {
std::chrono::steady_clock::time_point tpoint, size_t timeout) { this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
return std::chrono::duration_cast<std::chrono::milliseconds>(now - tpoint).count() > timeout; : ProtocolPhases::SENDING_INIT_1);
}
void HaierClimateBase::reset_to_idle_() {
this->force_send_control_ = false;
if (this->current_hvac_settings_.valid)
this->current_hvac_settings_.reset();
this->forced_request_status_ = true;
this->set_phase(ProtocolPhases::IDLE);
this->action_request_.reset();
} }
bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { bool HaierClimateBase::is_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS); return check_timeout(now, this->last_request_timestamp_, DEFAULT_MESSAGES_INTERVAL_MS);
} }
bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) { bool HaierClimateBase::is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS); return check_timeout(now, this->last_status_request_, STATUS_REQUEST_INTERVAL_MS);
}
bool HaierClimateBase::is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->control_request_timestamp_, CONTROL_TIMEOUT_MS);
} }
bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) { bool HaierClimateBase::is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS); return check_timeout(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS);
} }
bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) { bool HaierClimateBase::is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now) {
return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL); return check_timeout(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL);
} }
#ifdef USE_WIFI #ifdef USE_WIFI
haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t message_type) { haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_() {
static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00}; static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00};
if (wifi::global_wifi_component->is_connected()) { if (wifi::global_wifi_component->is_connected()) {
wifi_status_data[1] = 0; wifi_status_data[1] = 0;
@ -131,7 +121,8 @@ haier_protocol::HaierMessage HaierClimateBase::get_wifi_signal_message_(uint8_t
wifi_status_data[1] = 1; wifi_status_data[1] = 1;
wifi_status_data[3] = 0; wifi_status_data[3] = 0;
} }
return haier_protocol::HaierMessage(message_type, wifi_status_data, sizeof(wifi_status_data)); return haier_protocol::HaierMessage(haier_protocol::FrameType::REPORT_NETWORK_STATUS, wifi_status_data,
sizeof(wifi_status_data));
} }
#endif #endif
@ -140,7 +131,7 @@ bool HaierClimateBase::get_display_state() const { return this->display_status_;
void HaierClimateBase::set_display_state(bool state) { void HaierClimateBase::set_display_state(bool state) {
if (this->display_status_ != state) { if (this->display_status_ != state) {
this->display_status_ = state; this->display_status_ = state;
this->set_force_send_control_(true); this->force_send_control_ = true;
} }
} }
@ -149,15 +140,24 @@ bool HaierClimateBase::get_health_mode() const { return this->health_mode_; }
void HaierClimateBase::set_health_mode(bool state) { void HaierClimateBase::set_health_mode(bool state) {
if (this->health_mode_ != state) { if (this->health_mode_ != state) {
this->health_mode_ = state; this->health_mode_ = state;
this->set_force_send_control_(true); this->force_send_control_ = true;
} }
} }
void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; } void HaierClimateBase::send_power_on_command() {
this->action_request_ =
PendingAction({ActionRequest::TURN_POWER_ON, esphome::optional<haier_protocol::HaierMessage>()});
}
void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; } void HaierClimateBase::send_power_off_command() {
this->action_request_ =
PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional<haier_protocol::HaierMessage>()});
}
void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; } void HaierClimateBase::toggle_power() {
this->action_request_ =
PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()});
}
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) { void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
this->traits_.set_supported_swing_modes(modes); this->traits_.set_supported_swing_modes(modes);
@ -165,9 +165,7 @@ void HaierClimateBase::set_supported_swing_modes(const std::set<climate::Climate
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
} }
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
this->answer_timeout_ = std::chrono::milliseconds(timeout);
}
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) { void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
this->traits_.set_supported_modes(modes); this->traits_.set_supported_modes(modes);
@ -183,29 +181,42 @@ void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePres
void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; } void HaierClimateBase::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type, void HaierClimateBase::send_custom_command(const haier_protocol::HaierMessage &message) {
uint8_t expected_request_message_type, this->action_request_ = PendingAction({ActionRequest::SEND_CUSTOM_COMMAND, message});
uint8_t answer_message_type, }
uint8_t expected_answer_message_type,
ProtocolPhases expected_phase) { haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(
haier_protocol::FrameType request_message_type, haier_protocol::FrameType expected_request_message_type,
haier_protocol::FrameType answer_message_type, haier_protocol::FrameType expected_answer_message_type,
ProtocolPhases expected_phase) {
haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK; haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type)) if ((expected_request_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) &&
(request_message_type != expected_request_message_type))
result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type)) if ((expected_answer_message_type != haier_protocol::FrameType::UNKNOWN_FRAME_TYPE) &&
(answer_message_type != expected_answer_message_type))
result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; result = haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)) if (!this->haier_protocol_.is_waiting_for_answer() ||
((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_)))
result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE; result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
if (is_message_invalid(answer_message_type)) if (answer_message_type == haier_protocol::FrameType::INVALID)
result = haier_protocol::HandlerError::INVALID_ANSWER; result = haier_protocol::HandlerError::INVALID_ANSWER;
return result; return result;
} }
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) { haier_protocol::HandlerError HaierClimateBase::report_network_status_answer_handler_(
#if (HAIER_LOG_LEVEL > 4) haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_)); size_t data_size) {
#else haier_protocol::HandlerError result =
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_); this->answer_preprocess_(request_type, haier_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
#endif haier_protocol::FrameType::CONFIRM, ProtocolPhases::SENDING_SIGNAL_LEVEL);
this->set_phase(ProtocolPhases::IDLE);
return result;
}
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(haier_protocol::FrameType request_type) {
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) request_type,
phase_to_string_(this->protocol_phase_));
if (this->protocol_phase_ > ProtocolPhases::IDLE) { if (this->protocol_phase_ > ProtocolPhases::IDLE) {
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
} else { } else {
@ -219,79 +230,95 @@ void HaierClimateBase::setup() {
// Set timestamp here to give AC time to boot // Set timestamp here to give AC time to boot
this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->last_request_timestamp_ = std::chrono::steady_clock::now();
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
this->set_handlers();
this->haier_protocol_.set_default_timeout_handler( this->haier_protocol_.set_default_timeout_handler(
std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1)); std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
this->set_handlers();
} }
void HaierClimateBase::dump_config() { void HaierClimateBase::dump_config() {
LOG_CLIMATE("", "Haier Climate", this); LOG_CLIMATE("", "Haier Climate", this);
ESP_LOGCONFIG(TAG, " Device communication status: %s", ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none");
(this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none");
} }
void HaierClimateBase::loop() { void HaierClimateBase::loop() {
std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now(); std::chrono::steady_clock::time_point now = std::chrono::steady_clock::now();
if ((std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_valid_status_timestamp_).count() > if ((std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_valid_status_timestamp_).count() >
COMMUNICATION_TIMEOUT_MS) || COMMUNICATION_TIMEOUT_MS) ||
(this->reset_protocol_request_)) { (this->reset_protocol_request_ && (!this->haier_protocol_.is_waiting_for_answer()))) {
this->last_valid_status_timestamp_ = now;
if (this->protocol_phase_ >= ProtocolPhases::IDLE) { if (this->protocol_phase_ >= ProtocolPhases::IDLE) {
// No status too long, reseting protocol // No status too long, reseting protocol
// No need to reset protocol if we didn't pass initialization phase
if (this->reset_protocol_request_) { if (this->reset_protocol_request_) {
this->reset_protocol_request_ = false; this->reset_protocol_request_ = false;
ESP_LOGW(TAG, "Protocol reset requested"); ESP_LOGW(TAG, "Protocol reset requested");
} else { } else {
ESP_LOGW(TAG, "Communication timeout, reseting protocol"); ESP_LOGW(TAG, "Communication timeout, reseting protocol");
} }
this->last_valid_status_timestamp_ = now; this->process_protocol_reset();
this->set_force_send_control_(false);
if (this->hvac_settings_.valid)
this->hvac_settings_.reset();
this->set_phase(ProtocolPhases::SENDING_INIT_1);
return; return;
} else {
// No need to reset protocol if we didn't pass initialization phase
this->last_valid_status_timestamp_ = now;
} }
}; };
if ((this->protocol_phase_ == ProtocolPhases::IDLE) || if ((!this->haier_protocol_.is_waiting_for_answer()) &&
(this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) || ((this->protocol_phase_ == ProtocolPhases::IDLE) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) || (this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL)) { (this->protocol_phase_ == ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST) ||
(this->protocol_phase_ == ProtocolPhases::SENDING_SIGNAL_LEVEL))) {
// If control message or action is pending we should send it ASAP unless we are in initialisation // If control message or action is pending we should send it ASAP unless we are in initialisation
// procedure or waiting for an answer // procedure or waiting for an answer
if (this->action_request_ != ActionRequest::NO_ACTION) { if (this->action_request_.has_value() && this->prepare_pending_action()) {
this->process_pending_action(); this->set_phase(ProtocolPhases::SENDING_ACTION_COMMAND);
} else if (this->hvac_settings_.valid || this->force_send_control_) { } else if (this->next_hvac_settings_.valid || this->force_send_control_) {
ESP_LOGV(TAG, "Control packet is pending..."); ESP_LOGV(TAG, "Control packet is pending...");
this->set_phase(ProtocolPhases::SENDING_CONTROL); this->set_phase(ProtocolPhases::SENDING_CONTROL);
if (this->next_hvac_settings_.valid) {
this->current_hvac_settings_ = this->next_hvac_settings_;
this->next_hvac_settings_.reset();
} else {
this->current_hvac_settings_.reset();
}
} }
} }
this->process_phase(now); this->process_phase(now);
this->haier_protocol_.loop(); this->haier_protocol_.loop();
} }
void HaierClimateBase::process_pending_action() { void HaierClimateBase::process_protocol_reset() {
ActionRequest request = this->action_request_; this->force_send_control_ = false;
if (this->action_request_ == ActionRequest::TOGGLE_POWER) { if (this->current_hvac_settings_.valid)
request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF; this->current_hvac_settings_.reset();
} if (this->next_hvac_settings_.valid)
switch (request) { this->next_hvac_settings_.reset();
case ActionRequest::TURN_POWER_ON: this->mode = CLIMATE_MODE_OFF;
this->set_phase(ProtocolPhases::SENDING_POWER_ON_COMMAND); this->current_temperature = NAN;
break; this->target_temperature = NAN;
case ActionRequest::TURN_POWER_OFF: this->fan_mode.reset();
this->set_phase(ProtocolPhases::SENDING_POWER_OFF_COMMAND); this->preset.reset();
break; this->publish_state();
case ActionRequest::TOGGLE_POWER: this->set_phase(ProtocolPhases::SENDING_INIT_1);
case ActionRequest::NO_ACTION: }
// shouldn't get here, do nothing
break; bool HaierClimateBase::prepare_pending_action() {
default: if (this->action_request_.has_value()) {
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_); switch (this->action_request_.value().action) {
break; case ActionRequest::SEND_CUSTOM_COMMAND:
} return true;
this->action_request_ = ActionRequest::NO_ACTION; case ActionRequest::TURN_POWER_ON:
this->action_request_.value().message = this->get_power_message(true);
return true;
case ActionRequest::TURN_POWER_OFF:
this->action_request_.value().message = this->get_power_message(false);
return true;
case ActionRequest::TOGGLE_POWER:
this->action_request_.value().message = this->get_power_message(this->mode == ClimateMode::CLIMATE_MODE_OFF);
return true;
default:
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_.value().action);
this->action_request_.reset();
return false;
}
} else
return false;
} }
ClimateTraits HaierClimateBase::traits() { return traits_; } ClimateTraits HaierClimateBase::traits() { return traits_; }
@ -302,23 +329,22 @@ void HaierClimateBase::control(const ClimateCall &call) {
ESP_LOGW(TAG, "Can't send control packet, first poll answer not received"); ESP_LOGW(TAG, "Can't send control packet, first poll answer not received");
return; // cancel the control, we cant do it without a poll answer. return; // cancel the control, we cant do it without a poll answer.
} }
if (this->hvac_settings_.valid) { if (this->current_hvac_settings_.valid) {
ESP_LOGW(TAG, "Overriding old valid settings before they were applied!"); ESP_LOGW(TAG, "New settings come faster then processed!");
} }
{ {
if (call.get_mode().has_value()) if (call.get_mode().has_value())
this->hvac_settings_.mode = call.get_mode(); this->next_hvac_settings_.mode = call.get_mode();
if (call.get_fan_mode().has_value()) if (call.get_fan_mode().has_value())
this->hvac_settings_.fan_mode = call.get_fan_mode(); this->next_hvac_settings_.fan_mode = call.get_fan_mode();
if (call.get_swing_mode().has_value()) if (call.get_swing_mode().has_value())
this->hvac_settings_.swing_mode = call.get_swing_mode(); this->next_hvac_settings_.swing_mode = call.get_swing_mode();
if (call.get_target_temperature().has_value()) if (call.get_target_temperature().has_value())
this->hvac_settings_.target_temperature = call.get_target_temperature(); this->next_hvac_settings_.target_temperature = call.get_target_temperature();
if (call.get_preset().has_value()) if (call.get_preset().has_value())
this->hvac_settings_.preset = call.get_preset(); this->next_hvac_settings_.preset = call.get_preset();
this->hvac_settings_.valid = true; this->next_hvac_settings_.valid = true;
} }
this->first_control_attempt_ = true;
} }
void HaierClimateBase::HvacSettings::reset() { void HaierClimateBase::HvacSettings::reset() {
@ -330,19 +356,9 @@ void HaierClimateBase::HvacSettings::reset() {
this->preset.reset(); this->preset.reset();
} }
void HaierClimateBase::set_force_send_control_(bool status) { void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats,
this->force_send_control_ = status; std::chrono::milliseconds interval) {
if (status) { this->haier_protocol_.send_message(command, use_crc, num_repeats, interval);
this->first_control_attempt_ = true;
}
}
void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) {
if (this->answer_timeout_.has_value()) {
this->haier_protocol_.send_message(command, use_crc, this->answer_timeout_.value());
} else {
this->haier_protocol_.send_message(command, use_crc);
}
this->last_request_timestamp_ = std::chrono::steady_clock::now(); this->last_request_timestamp_ = std::chrono::steady_clock::now();
} }

View file

@ -11,7 +11,7 @@ namespace esphome {
namespace haier { namespace haier {
enum class ActionRequest : uint8_t { enum class ActionRequest : uint8_t {
NO_ACTION = 0, SEND_CUSTOM_COMMAND = 0,
TURN_POWER_ON = 1, TURN_POWER_ON = 1,
TURN_POWER_OFF = 2, TURN_POWER_OFF = 2,
TOGGLE_POWER = 3, TOGGLE_POWER = 3,
@ -33,7 +33,6 @@ class HaierClimateBase : public esphome::Component,
void control(const esphome::climate::ClimateCall &call) override; void control(const esphome::climate::ClimateCall &call) override;
void dump_config() override; void dump_config() override;
float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; } float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; }
void set_fahrenheit(bool fahrenheit);
void set_display_state(bool state); void set_display_state(bool state);
bool get_display_state() const; bool get_display_state() const;
void set_health_mode(bool state); void set_health_mode(bool state);
@ -45,6 +44,7 @@ class HaierClimateBase : public esphome::Component,
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes); void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes); void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets); void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets);
bool valid_connection() { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); }; size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
size_t read_array(uint8_t *data, size_t len) noexcept override { size_t read_array(uint8_t *data, size_t len) noexcept override {
return esphome::uart::UARTDevice::read_array(data, len) ? len : 0; return esphome::uart::UARTDevice::read_array(data, len) ? len : 0;
@ -55,63 +55,56 @@ class HaierClimateBase : public esphome::Component,
bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; }; bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; };
void set_answer_timeout(uint32_t timeout); void set_answer_timeout(uint32_t timeout);
void set_send_wifi(bool send_wifi); void set_send_wifi(bool send_wifi);
void send_custom_command(const haier_protocol::HaierMessage &message);
protected: protected:
enum class ProtocolPhases { enum class ProtocolPhases {
UNKNOWN = -1, UNKNOWN = -1,
// INITIALIZATION // INITIALIZATION
SENDING_INIT_1 = 0, SENDING_INIT_1 = 0,
WAITING_INIT_1_ANSWER = 1, SENDING_INIT_2,
SENDING_INIT_2 = 2, SENDING_FIRST_STATUS_REQUEST,
WAITING_INIT_2_ANSWER = 3, SENDING_ALARM_STATUS_REQUEST,
SENDING_FIRST_STATUS_REQUEST = 4,
WAITING_FIRST_STATUS_ANSWER = 5,
SENDING_ALARM_STATUS_REQUEST = 6,
WAITING_ALARM_STATUS_ANSWER = 7,
// FUNCTIONAL STATE // FUNCTIONAL STATE
IDLE = 8, IDLE,
SENDING_STATUS_REQUEST = 10, SENDING_STATUS_REQUEST,
WAITING_STATUS_ANSWER = 11, SENDING_UPDATE_SIGNAL_REQUEST,
SENDING_UPDATE_SIGNAL_REQUEST = 12, SENDING_SIGNAL_LEVEL,
WAITING_UPDATE_SIGNAL_ANSWER = 13, SENDING_CONTROL,
SENDING_SIGNAL_LEVEL = 14, SENDING_ACTION_COMMAND,
WAITING_SIGNAL_LEVEL_ANSWER = 15,
SENDING_CONTROL = 16,
WAITING_CONTROL_ANSWER = 17,
SENDING_POWER_ON_COMMAND = 18,
WAITING_POWER_ON_ANSWER = 19,
SENDING_POWER_OFF_COMMAND = 20,
WAITING_POWER_OFF_ANSWER = 21,
NUM_PROTOCOL_PHASES NUM_PROTOCOL_PHASES
}; };
#if (HAIER_LOG_LEVEL > 4)
const char *phase_to_string_(ProtocolPhases phase); const char *phase_to_string_(ProtocolPhases phase);
#endif
virtual void set_handlers() = 0; virtual void set_handlers() = 0;
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0; virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
virtual haier_protocol::HaierMessage get_control_message() = 0; virtual haier_protocol::HaierMessage get_control_message() = 0;
virtual bool is_message_invalid(uint8_t message_type) = 0; virtual haier_protocol::HaierMessage get_power_message(bool state) = 0;
virtual void process_pending_action(); virtual bool prepare_pending_action();
virtual void process_protocol_reset();
esphome::climate::ClimateTraits traits() override; esphome::climate::ClimateTraits traits() override;
// Answers handlers // Answer handlers
haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type, haier_protocol::HandlerError answer_preprocess_(haier_protocol::FrameType request_message_type,
uint8_t answer_message_type, uint8_t expected_answer_message_type, haier_protocol::FrameType expected_request_message_type,
haier_protocol::FrameType answer_message_type,
haier_protocol::FrameType expected_answer_message_type,
ProtocolPhases expected_phase); ProtocolPhases expected_phase);
haier_protocol::HandlerError report_network_status_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size);
// Timeout handler // Timeout handler
haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type); haier_protocol::HandlerError timeout_default_handler_(haier_protocol::FrameType request_type);
// Helper functions // Helper functions
void set_force_send_control_(bool status); void send_message_(const haier_protocol::HaierMessage &command, bool use_crc, uint8_t num_repeats = 0,
void send_message_(const haier_protocol::HaierMessage &command, bool use_crc); std::chrono::milliseconds interval = std::chrono::milliseconds::zero());
virtual void set_phase(ProtocolPhases phase); virtual void set_phase(ProtocolPhases phase);
bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint, void reset_phase_();
size_t timeout); void reset_to_idle_();
bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_status_request_interval_exceeded_(std::chrono::steady_clock::time_point now);
bool is_control_message_timeout_exceeded_(std::chrono::steady_clock::time_point now);
bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_control_message_interval_exceeded_(std::chrono::steady_clock::time_point now);
bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now); bool is_protocol_initialisation_interval_exceeded_(std::chrono::steady_clock::time_point now);
#ifdef USE_WIFI #ifdef USE_WIFI
haier_protocol::HaierMessage get_wifi_signal_message_(uint8_t message_type); haier_protocol::HaierMessage get_wifi_signal_message_();
#endif #endif
struct HvacSettings { struct HvacSettings {
@ -122,29 +115,34 @@ class HaierClimateBase : public esphome::Component,
esphome::optional<esphome::climate::ClimatePreset> preset; esphome::optional<esphome::climate::ClimatePreset> preset;
bool valid; bool valid;
HvacSettings() : valid(false){}; HvacSettings() : valid(false){};
HvacSettings(const HvacSettings &) = default;
HvacSettings &operator=(const HvacSettings &) = default;
void reset(); void reset();
}; };
struct PendingAction {
ActionRequest action;
esphome::optional<haier_protocol::HaierMessage> message;
};
haier_protocol::ProtocolHandler haier_protocol_; haier_protocol::ProtocolHandler haier_protocol_;
ProtocolPhases protocol_phase_; ProtocolPhases protocol_phase_;
ActionRequest action_request_; esphome::optional<PendingAction> action_request_;
uint8_t fan_mode_speed_; uint8_t fan_mode_speed_;
uint8_t other_modes_fan_speed_; uint8_t other_modes_fan_speed_;
bool display_status_; bool display_status_;
bool health_mode_; bool health_mode_;
bool force_send_control_; bool force_send_control_;
bool forced_publish_;
bool forced_request_status_; bool forced_request_status_;
bool first_control_attempt_;
bool reset_protocol_request_; bool reset_protocol_request_;
bool send_wifi_signal_;
bool use_crc_;
esphome::climate::ClimateTraits traits_; esphome::climate::ClimateTraits traits_;
HvacSettings hvac_settings_; HvacSettings current_hvac_settings_;
HvacSettings next_hvac_settings_;
std::unique_ptr<uint8_t[]> last_status_message_;
std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages std::chrono::steady_clock::time_point last_request_timestamp_; // For interval between messages
std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout std::chrono::steady_clock::time_point last_valid_status_timestamp_; // For protocol timeout
std::chrono::steady_clock::time_point last_status_request_; // To request AC status std::chrono::steady_clock::time_point last_status_request_; // To request AC status
std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level
optional<std::chrono::milliseconds> answer_timeout_; // Message answer timeout
bool send_wifi_signal_;
std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level
}; };
} // namespace haier } // namespace haier

View file

@ -14,6 +14,8 @@ namespace haier {
static const char *const TAG = "haier.climate"; static const char *const TAG = "haier.climate";
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64; constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) { hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
switch (direction) { switch (direction) {
@ -48,14 +50,11 @@ hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDir
} }
HonClimate::HonClimate() HonClimate::HonClimate()
: last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]), : cleaning_status_(CleaningState::NO_CLEANING),
cleaning_status_(CleaningState::NO_CLEANING),
got_valid_outdoor_temp_(false), got_valid_outdoor_temp_(false),
hvac_hardware_info_available_(false),
hvac_functions_{false, false, false, false, false},
use_crc_(hvac_functions_[2]),
active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, active_alarms_{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
outdoor_sensor_(nullptr) { outdoor_sensor_(nullptr) {
last_status_message_ = std::unique_ptr<uint8_t[]>(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]);
this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID; this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID;
this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO; this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
} }
@ -72,14 +71,14 @@ AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this-
void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) { void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) {
this->vertical_direction_ = direction; this->vertical_direction_ = direction;
this->set_force_send_control_(true); this->force_send_control_ = true;
} }
AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; } AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; }
void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) { void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) {
this->horizontal_direction_ = direction; this->horizontal_direction_ = direction;
this->set_force_send_control_(true); this->force_send_control_ = true;
} }
std::string HonClimate::get_cleaning_status_text() const { std::string HonClimate::get_cleaning_status_text() const {
@ -98,35 +97,35 @@ CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_st
void HonClimate::start_self_cleaning() { void HonClimate::start_self_cleaning() {
if (this->cleaning_status_ == CleaningState::NO_CLEANING) { if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
ESP_LOGI(TAG, "Sending self cleaning start request"); ESP_LOGI(TAG, "Sending self cleaning start request");
this->action_request_ = ActionRequest::START_SELF_CLEAN; this->action_request_ =
this->set_force_send_control_(true); PendingAction({ActionRequest::START_SELF_CLEAN, esphome::optional<haier_protocol::HaierMessage>()});
} }
} }
void HonClimate::start_steri_cleaning() { void HonClimate::start_steri_cleaning() {
if (this->cleaning_status_ == CleaningState::NO_CLEANING) { if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
ESP_LOGI(TAG, "Sending steri cleaning start request"); ESP_LOGI(TAG, "Sending steri cleaning start request");
this->action_request_ = ActionRequest::START_STERI_CLEAN; this->action_request_ =
this->set_force_send_control_(true); PendingAction({ActionRequest::START_STERI_CLEAN, esphome::optional<haier_protocol::HaierMessage>()});
} }
} }
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) { const uint8_t *data, size_t data_size) {
// Should check this before preprocess // Should check this before preprocess
if (message_type == (uint8_t) hon_protocol::FrameType::INVALID) { if (message_type == haier_protocol::FrameType::INVALID) {
ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 " ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the smartAir2 "
"protocol instead of hOn"); "protocol instead of hOn");
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
return haier_protocol::HandlerError::INVALID_ANSWER; return haier_protocol::HandlerError::INVALID_ANSWER;
} }
haier_protocol::HandlerError result = this->answer_preprocess_( haier_protocol::HandlerError result =
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type, this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_VERSION, message_type,
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_INIT_1_ANSWER); haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::SENDING_INIT_1);
if (result == haier_protocol::HandlerError::HANDLER_OK) { if (result == haier_protocol::HandlerError::HANDLER_OK) {
if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) { if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) {
// Wrong structure // Wrong structure
this->set_phase(ProtocolPhases::SENDING_INIT_1);
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
} }
// All OK // All OK
@ -134,54 +133,57 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint
char tmp[9]; char tmp[9];
tmp[8] = 0; tmp[8] = 0;
strncpy(tmp, answr->protocol_version, 8); strncpy(tmp, answr->protocol_version, 8);
this->hvac_protocol_version_ = std::string(tmp); this->hvac_hardware_info_ = HardwareInfo();
this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp);
strncpy(tmp, answr->software_version, 8); strncpy(tmp, answr->software_version, 8);
this->hvac_software_version_ = std::string(tmp); this->hvac_hardware_info_.value().software_version_ = std::string(tmp);
strncpy(tmp, answr->hardware_version, 8); strncpy(tmp, answr->hardware_version, 8);
this->hvac_hardware_version_ = std::string(tmp); this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp);
strncpy(tmp, answr->device_name, 8); strncpy(tmp, answr->device_name, 8);
this->hvac_device_name_ = std::string(tmp); this->hvac_hardware_info_.value().device_name_ = std::string(tmp);
this->hvac_functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
this->hvac_functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support this->hvac_hardware_info_.value().functions_[1] =
this->hvac_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support (answr->functions[1] & 0x02) != 0; // controller-device mode support
this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
this->hvac_hardware_info_available_ = true; this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
this->use_crc_ = this->hvac_hardware_info_.value().functions_[2];
this->set_phase(ProtocolPhases::SENDING_INIT_2); this->set_phase(ProtocolPhases::SENDING_INIT_2);
return result; return result;
} else { } else {
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->reset_phase_();
: ProtocolPhases::SENDING_INIT_1);
return result; return result;
} }
} }
haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) { const uint8_t *data, size_t data_size) {
haier_protocol::HandlerError result = this->answer_preprocess_( haier_protocol::HandlerError result =
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type, this->answer_preprocess_(request_type, haier_protocol::FrameType::GET_DEVICE_ID, message_type,
(uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_INIT_2_ANSWER); haier_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::SENDING_INIT_2);
if (result == haier_protocol::HandlerError::HANDLER_OK) { if (result == haier_protocol::HandlerError::HANDLER_OK) {
this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
return result; return result;
} else { } else {
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->reset_phase_();
: ProtocolPhases::SENDING_INIT_1);
return result; return result;
} }
} }
haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameType request_type,
const uint8_t *data, size_t data_size) { haier_protocol::FrameType message_type, const uint8_t *data,
size_t data_size) {
haier_protocol::HandlerError result = haier_protocol::HandlerError result =
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type, this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type,
(uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
if (result == haier_protocol::HandlerError::HANDLER_OK) { if (result == haier_protocol::HandlerError::HANDLER_OK) {
result = this->process_status_message_(data, data_size); result = this->process_status_message_(data, data_size);
if (result != haier_protocol::HandlerError::HANDLER_OK) { if (result != haier_protocol::HandlerError::HANDLER_OK) {
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->reset_phase_();
: ProtocolPhases::SENDING_INIT_1); this->action_request_.reset();
this->force_send_control_ = false;
} else { } else {
if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) { if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) {
memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl)); memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl));
@ -189,36 +191,48 @@ haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, u
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
sizeof(hon_protocol::HaierPacketControl)); sizeof(hon_protocol::HaierPacketControl));
} }
if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { switch (this->protocol_phase_) {
ESP_LOGI(TAG, "First HVAC status received"); case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST); ESP_LOGI(TAG, "First HVAC status received");
} else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) || this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) || break;
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) { case ProtocolPhases::SENDING_ACTION_COMMAND:
this->set_phase(ProtocolPhases::IDLE); // Do nothing, phase will be changed in process_phase
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { break;
this->set_phase(ProtocolPhases::IDLE); case ProtocolPhases::SENDING_STATUS_REQUEST:
this->set_force_send_control_(false); this->set_phase(ProtocolPhases::IDLE);
if (this->hvac_settings_.valid) break;
this->hvac_settings_.reset(); case ProtocolPhases::SENDING_CONTROL:
if (!this->control_messages_queue_.empty())
this->control_messages_queue_.pop();
if (this->control_messages_queue_.empty()) {
this->set_phase(ProtocolPhases::IDLE);
this->force_send_control_ = false;
if (this->current_hvac_settings_.valid)
this->current_hvac_settings_.reset();
} else {
this->set_phase(ProtocolPhases::SENDING_CONTROL);
}
break;
default:
break;
} }
} }
return result; return result;
} else { } else {
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->action_request_.reset();
: ProtocolPhases::SENDING_INIT_1); this->force_send_control_ = false;
this->reset_phase_();
return result; return result;
} }
} }
haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type, haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(
uint8_t message_type, haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
const uint8_t *data, size_t data_size) {
size_t data_size) { haier_protocol::HandlerError result = this->answer_preprocess_(
haier_protocol::HandlerError result = request_type, haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION, message_type,
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION, haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE, ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE,
ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
if (result == haier_protocol::HandlerError::HANDLER_OK) { if (result == haier_protocol::HandlerError::HANDLER_OK) {
this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
return result; return result;
@ -228,25 +242,16 @@ haier_protocol::HandlerError HonClimate::get_management_information_answer_handl
} }
} }
haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type, haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
uint8_t message_type, haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) {
haier_protocol::HandlerError result =
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
(uint8_t) hon_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
this->set_phase(ProtocolPhases::IDLE);
return result;
}
haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size) { const uint8_t *data, size_t data_size) {
if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) { if (request_type == haier_protocol::FrameType::GET_ALARM_STATUS) {
if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) { if (message_type != haier_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) {
// Unexpected answer to request // Unexpected answer to request
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
} }
if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) { if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) {
// Don't expect this answer now // Don't expect this answer now
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
@ -263,27 +268,27 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(uint8_
void HonClimate::set_handlers() { void HonClimate::set_handlers() {
// Set handlers // Set handlers
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION), haier_protocol::FrameType::GET_DEVICE_VERSION,
std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID), haier_protocol::FrameType::GET_DEVICE_ID,
std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::CONTROL), haier_protocol::FrameType::CONTROL,
std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,
std::placeholders::_4)); std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION), haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1, std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS), haier_protocol::FrameType::GET_ALARM_STATUS,
std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS), haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
} }
@ -291,14 +296,18 @@ void HonClimate::set_handlers() {
void HonClimate::dump_config() { void HonClimate::dump_config() {
HaierClimateBase::dump_config(); HaierClimateBase::dump_config();
ESP_LOGCONFIG(TAG, " Protocol version: hOn"); ESP_LOGCONFIG(TAG, " Protocol version: hOn");
if (this->hvac_hardware_info_available_) { ESP_LOGCONFIG(TAG, " Control method: %d", (uint8_t) this->control_method_);
ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str()); if (this->hvac_hardware_info_.has_value()) {
ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str()); ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_hardware_info_.value().protocol_version_.c_str());
ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str()); ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_hardware_info_.value().software_version_.c_str());
ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str()); ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_info_.value().hardware_version_.c_str());
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""), ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_hardware_info_.value().device_name_.c_str());
(this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""), ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
(this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : "")); (this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""),
(this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""),
(this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""),
(this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""),
(this->hvac_hardware_info_.value().functions_[4] ? " role" : ""));
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str()); ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
} }
} }
@ -307,7 +316,6 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
switch (this->protocol_phase_) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_INIT_1: case ProtocolPhases::SENDING_INIT_1:
if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) { if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) {
this->hvac_hardware_info_available_ = false;
// Indicate device capabilities: // Indicate device capabilities:
// bit 0 - if 1 module support interactive mode // bit 0 - if 1 module support interactive mode
// bit 1 - if 1 module support controller-device mode // bit 1 - if 1 module support controller-device mode
@ -316,109 +324,95 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
// bit 4..bit 15 - not used // bit 4..bit 15 - not used
uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities)); haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_); this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_);
this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_INIT_2: case ProtocolPhases::SENDING_INIT_2:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage DEVICEID_REQUEST((uint8_t) hon_protocol::FrameType::GET_DEVICE_ID); static const haier_protocol::HaierMessage DEVICEID_REQUEST(haier_protocol::FrameType::GET_DEVICE_ID);
this->send_message_(DEVICEID_REQUEST, this->use_crc_); this->send_message_(DEVICEID_REQUEST, this->use_crc_);
this->set_phase(ProtocolPhases::WAITING_INIT_2_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
case ProtocolPhases::SENDING_STATUS_REQUEST: case ProtocolPhases::SENDING_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage STATUS_REQUEST( static const haier_protocol::HaierMessage STATUS_REQUEST(
(uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA); haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::GET_USER_DATA);
this->send_message_(STATUS_REQUEST, this->use_crc_); this->send_message_(STATUS_REQUEST, this->use_crc_);
this->last_status_request_ = now; this->last_status_request_ = now;
this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
} }
break; break;
#ifdef USE_WIFI #ifdef USE_WIFI
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST( static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST(
(uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION); haier_protocol::FrameType::GET_MANAGEMENT_INFORMATION);
this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_); this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_);
this->last_signal_request_ = now; this->last_signal_request_ = now;
this->set_phase(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_SIGNAL_LEVEL: case ProtocolPhases::SENDING_SIGNAL_LEVEL:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
this->send_message_(this->get_wifi_signal_message_((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS), this->send_message_(this->get_wifi_signal_message_(), this->use_crc_);
this->use_crc_);
this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
} }
break; break;
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
break;
#else #else
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
case ProtocolPhases::SENDING_SIGNAL_LEVEL: case ProtocolPhases::SENDING_SIGNAL_LEVEL:
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
break; break;
#endif #endif
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST( static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS);
(uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS);
this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_); this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
this->set_phase(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_CONTROL: case ProtocolPhases::SENDING_CONTROL:
if (this->first_control_attempt_) { if (this->control_messages_queue_.empty()) {
this->control_request_timestamp_ = now; switch (this->control_method_) {
this->first_control_attempt_ = false; case HonControlMethod::SET_GROUP_PARAMETERS: {
haier_protocol::HaierMessage control_message = this->get_control_message();
this->control_messages_queue_.push(control_message);
} break;
case HonControlMethod::SET_SINGLE_PARAMETER:
this->fill_control_messages_queue_();
break;
case HonControlMethod::MONITOR_ONLY:
ESP_LOGI(TAG, "AC control is disabled, monitor only");
this->reset_to_idle_();
return;
default:
ESP_LOGW(TAG, "Unsupported control method for hOn protocol!");
this->reset_to_idle_();
return;
}
} }
if (this->is_control_message_timeout_exceeded_(now)) { if (this->control_messages_queue_.empty()) {
ESP_LOGW(TAG, "Sending control packet timeout!"); ESP_LOGW(TAG, "Control message queue is empty!");
this->set_force_send_control_(false); this->reset_to_idle_();
if (this->hvac_settings_.valid)
this->hvac_settings_.reset();
this->forced_request_status_ = true;
this->forced_publish_ = true;
this->set_phase(ProtocolPhases::IDLE);
} else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) { } else if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
haier_protocol::HaierMessage control_message = get_control_message(); ESP_LOGI(TAG, "Sending control packet, queue size %d", this->control_messages_queue_.size());
this->send_message_(control_message, this->use_crc_); this->send_message_(this->control_messages_queue_.front(), this->use_crc_, CONTROL_MESSAGE_RETRIES,
ESP_LOGI(TAG, "Control packet sent"); CONTROL_MESSAGE_RETRIES_INTERVAL);
this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_POWER_ON_COMMAND: case ProtocolPhases::SENDING_ACTION_COMMAND:
case ProtocolPhases::SENDING_POWER_OFF_COMMAND: if (this->action_request_.has_value()) {
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->action_request_.value().message.has_value()) {
uint8_t pwr_cmd_buf[2] = {0x00, 0x00}; this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND) this->action_request_.value().message.reset();
pwr_cmd_buf[1] = 0x01; } else {
haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL, // Message already sent, reseting request and return to idle
((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1, this->action_request_.reset();
pwr_cmd_buf, sizeof(pwr_cmd_buf)); this->set_phase(ProtocolPhases::IDLE);
this->send_message_(power_cmd, this->use_crc_); }
this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND } else {
? ProtocolPhases::WAITING_POWER_ON_ANSWER ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!");
: ProtocolPhases::WAITING_POWER_OFF_ANSWER); this->set_phase(ProtocolPhases::IDLE);
} }
break; break;
case ProtocolPhases::WAITING_INIT_1_ANSWER:
case ProtocolPhases::WAITING_INIT_2_ANSWER:
case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER:
case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
case ProtocolPhases::WAITING_STATUS_ANSWER:
case ProtocolPhases::WAITING_CONTROL_ANSWER:
case ProtocolPhases::WAITING_POWER_ON_ANSWER:
case ProtocolPhases::WAITING_POWER_OFF_ANSWER:
break;
case ProtocolPhases::IDLE: { case ProtocolPhases::IDLE: {
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST);
@ -433,26 +427,35 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
} break; } break;
default: default:
// Shouldn't get here // Shouldn't get here
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
#else
ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
#endif
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
break; break;
} }
} }
haier_protocol::HaierMessage HonClimate::get_power_message(bool state) {
if (state) {
static haier_protocol::HaierMessage power_on_message(
haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1,
std::initializer_list<uint8_t>({0x00, 0x01}).begin(), 2);
return power_on_message;
} else {
static haier_protocol::HaierMessage power_off_message(
haier_protocol::FrameType::CONTROL, ((uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER) + 1,
std::initializer_list<uint8_t>({0x00, 0x00}).begin(), 2);
return power_off_message;
}
}
haier_protocol::HaierMessage HonClimate::get_control_message() { haier_protocol::HaierMessage HonClimate::get_control_message() {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)]; uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl)); memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer; hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
bool has_hvac_settings = false; bool has_hvac_settings = false;
if (this->hvac_settings_.valid) { if (this->current_hvac_settings_.valid) {
has_hvac_settings = true; has_hvac_settings = true;
HvacSettings climate_control; HvacSettings &climate_control = this->current_hvac_settings_;
climate_control = this->hvac_settings_;
if (climate_control.mode.has_value()) { if (climate_control.mode.has_value()) {
switch (climate_control.mode.value()) { switch (climate_control.mode.value()) {
case CLIMATE_MODE_OFF: case CLIMATE_MODE_OFF:
@ -535,7 +538,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
} }
if (climate_control.target_temperature.has_value()) { if (climate_control.target_temperature.has_value()) {
float target_temp = climate_control.target_temperature.value(); float target_temp = climate_control.target_temperature.value();
out_data->set_point = ((int) target_temp) - 16; // set the temperature at our offset, subtract 16. out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
} }
if (out_data->ac_power == 0) { if (out_data->ac_power == 0) {
@ -587,50 +590,28 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
control_out_buffer[4] = 0; // This byte should be cleared before setting values control_out_buffer[4] = 0; // This byte should be cleared before setting values
out_data->display_status = this->display_status_ ? 1 : 0; out_data->display_status = this->display_status_ ? 1 : 0;
out_data->health_mode = this->health_mode_ ? 1 : 0; out_data->health_mode = this->health_mode_ ? 1 : 0;
switch (this->action_request_) { return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
case ActionRequest::START_SELF_CLEAN:
this->action_request_ = ActionRequest::NO_ACTION;
out_data->self_cleaning_status = 1;
out_data->steri_clean = 0;
out_data->set_point = 0x06;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->light_status = 0;
break;
case ActionRequest::START_STERI_CLEAN:
this->action_request_ = ActionRequest::NO_ACTION;
out_data->self_cleaning_status = 0;
out_data->steri_clean = 1;
out_data->set_point = 0x06;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->light_status = 0;
break;
default:
// No change
break;
}
return haier_protocol::HaierMessage((uint8_t) hon_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, sizeof(hon_protocol::HaierPacketControl)); control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
} }
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) { haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
if (size < sizeof(hon_protocol::HaierStatus)) if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_)
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE; return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
hon_protocol::HaierStatus packet; struct {
if (size < sizeof(hon_protocol::HaierStatus)) hon_protocol::HaierPacketControl control;
size = sizeof(hon_protocol::HaierStatus); hon_protocol::HaierPacketSensors sensors;
memcpy(&packet, packet_buffer, size); } packet;
memcpy(&packet.control, packet_buffer + 2, sizeof(hon_protocol::HaierPacketControl));
memcpy(&packet.sensors,
packet_buffer + 2 + sizeof(hon_protocol::HaierPacketControl) + this->extra_control_packet_bytes_,
sizeof(hon_protocol::HaierPacketSensors));
if (packet.sensors.error_status != 0) { if (packet.sensors.error_status != 0) {
ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status); ESP_LOGW(TAG, "HVAC error, code=0x%02X", packet.sensors.error_status);
} }
if ((this->outdoor_sensor_ != nullptr) && (got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) { if ((this->outdoor_sensor_ != nullptr) &&
got_valid_outdoor_temp_ = true; (this->got_valid_outdoor_temp_ || (packet.sensors.outdoor_temperature > 0))) {
this->got_valid_outdoor_temp_ = true;
float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET); float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET);
if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp)) if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp))
this->outdoor_sensor_->publish_state(otemp); this->outdoor_sensor_->publish_state(otemp);
@ -703,7 +684,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
// Do something only if display status changed // Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) { if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display // AC just turned on from remote need to turn off display
this->set_force_send_control_(true); this->force_send_control_ = true;
} else { } else {
this->display_status_ = disp_status; this->display_status_ = disp_status;
} }
@ -732,7 +713,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning); ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning);
if (new_cleaning == CleaningState::NO_CLEANING) { if (new_cleaning == CleaningState::NO_CLEANING) {
// Turning AC off after cleaning // Turning AC off after cleaning
this->action_request_ = ActionRequest::TURN_POWER_OFF; this->action_request_ =
PendingAction({ActionRequest::TURN_POWER_OFF, esphome::optional<haier_protocol::HaierMessage>()});
} }
this->cleaning_status_ = new_cleaning; this->cleaning_status_ = new_cleaning;
} }
@ -783,51 +765,257 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
should_publish = should_publish || (old_swing_mode != this->swing_mode); should_publish = should_publish || (old_swing_mode != this->swing_mode);
} }
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
if (this->forced_publish_ || should_publish) { if (should_publish) {
#if (HAIER_LOG_LEVEL > 4)
std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
#endif
this->publish_state(); this->publish_state();
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGV(TAG, "Publish delay: %lld ms",
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() -
_publish_start)
.count());
#endif
this->forced_publish_ = false;
} }
if (should_publish) { if (should_publish) {
ESP_LOGI(TAG, "HVAC values changed"); ESP_LOGI(TAG, "HVAC values changed");
} }
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG;
"HVAC Mode = 0x%X", packet.control.ac_mode); esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode);
"Fan speed Status = 0x%X", packet.control.fan_mode); esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode);
"Horizontal Swing Status = 0x%X", packet.control.horizontal_swing_mode); esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Vertical Swing Status = 0x%X", packet.control.vertical_swing_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Set Point Status = 0x%X", packet.control.set_point);
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
} }
bool HonClimate::is_message_invalid(uint8_t message_type) { void HonClimate::fill_control_messages_queue_() {
return message_type == (uint8_t) hon_protocol::FrameType::INVALID; static uint8_t one_buf[] = {0x00, 0x01};
static uint8_t zero_buf[] = {0x00, 0x00};
if (!this->current_hvac_settings_.valid && !this->force_send_control_)
return;
this->clear_control_messages_queue_();
HvacSettings climate_control;
climate_control = this->current_hvac_settings_;
// Beeper command
{
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::BEEPER_STATUS,
this->beeper_status_ ? zero_buf : one_buf, 2));
}
// Health mode
{
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::HEALTH_MODE,
this->health_mode_ ? one_buf : zero_buf, 2));
}
// Climate mode
bool new_power = this->mode != CLIMATE_MODE_OFF;
uint8_t fan_mode_buf[] = {0x00, 0xFF};
uint8_t quiet_mode_buf[] = {0x00, 0xFF};
if (climate_control.mode.has_value()) {
uint8_t buffer[2] = {0x00, 0x00};
switch (climate_control.mode.value()) {
case CLIMATE_MODE_OFF:
new_power = false;
break;
case CLIMATE_MODE_HEAT_COOL:
new_power = true;
buffer[1] = (uint8_t) hon_protocol::ConditioningMode::AUTO;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_;
break;
case CLIMATE_MODE_HEAT:
new_power = true;
buffer[1] = (uint8_t) hon_protocol::ConditioningMode::HEAT;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_;
break;
case CLIMATE_MODE_DRY:
new_power = true;
buffer[1] = (uint8_t) hon_protocol::ConditioningMode::DRY;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_;
break;
case CLIMATE_MODE_FAN_ONLY:
new_power = true;
buffer[1] = (uint8_t) hon_protocol::ConditioningMode::FAN;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_; // Auto doesn't work in fan only mode
// Disabling eco mode for Fan only
quiet_mode_buf[1] = 0;
break;
case CLIMATE_MODE_COOL:
new_power = true;
buffer[1] = (uint8_t) hon_protocol::ConditioningMode::COOL;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_MODE,
buffer, 2));
fan_mode_buf[1] = this->other_modes_fan_speed_;
break;
default:
ESP_LOGE("Control", "Unsupported climate mode");
break;
}
}
// Climate power
{
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::AC_POWER,
new_power ? one_buf : zero_buf, 2));
}
// CLimate preset
{
uint8_t fast_mode_buf[] = {0x00, 0xFF};
if (!new_power) {
// If AC is off - no presets allowed
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
} else if (climate_control.preset.has_value()) {
switch (climate_control.preset.value()) {
case CLIMATE_PRESET_NONE:
quiet_mode_buf[1] = 0x00;
fast_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_ECO:
// Eco is not supported in Fan only mode
quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
fast_mode_buf[1] = 0x00;
break;
case CLIMATE_PRESET_BOOST:
quiet_mode_buf[1] = 0x00;
// Boost is not supported in Fan only mode
fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
break;
default:
ESP_LOGE("Control", "Unsupported preset");
break;
}
}
if (quiet_mode_buf[1] != 0xFF) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::QUIET_MODE,
quiet_mode_buf, 2));
}
if (fast_mode_buf[1] != 0xFF) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::FAST_MODE,
fast_mode_buf, 2));
}
}
// Target temperature
if (climate_control.target_temperature.has_value()) {
uint8_t buffer[2] = {0x00, 0x00};
buffer[1] = ((uint8_t) climate_control.target_temperature.value()) - 16;
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::SET_POINT,
buffer, 2));
}
// Fan mode
if (climate_control.fan_mode.has_value()) {
switch (climate_control.fan_mode.value()) {
case CLIMATE_FAN_LOW:
fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_LOW;
break;
case CLIMATE_FAN_MEDIUM:
fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_MID;
break;
case CLIMATE_FAN_HIGH:
fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_HIGH;
break;
case CLIMATE_FAN_AUTO:
if (mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode
fan_mode_buf[1] = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
break;
default:
ESP_LOGE("Control", "Unsupported fan mode");
break;
}
if (fan_mode_buf[1] != 0xFF) {
this->control_messages_queue_.push(
haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
(uint8_t) hon_protocol::DataParameters::FAN_MODE,
fan_mode_buf, 2));
}
}
} }
void HonClimate::process_pending_action() { void HonClimate::clear_control_messages_queue_() {
switch (this->action_request_) { while (!this->control_messages_queue_.empty())
case ActionRequest::START_SELF_CLEAN: this->control_messages_queue_.pop();
case ActionRequest::START_STERI_CLEAN: }
// Will reset action with control message sending
this->set_phase(ProtocolPhases::SENDING_CONTROL); bool HonClimate::prepare_pending_action() {
break; switch (this->action_request_.value().action) {
case ActionRequest::START_SELF_CLEAN: {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
out_data->self_cleaning_status = 1;
out_data->steri_clean = 0;
out_data->set_point = 0x06;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->light_status = 0;
this->action_request_.value().message = haier_protocol::HaierMessage(
haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
}
return true;
case ActionRequest::START_STERI_CLEAN: {
uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
out_data->self_cleaning_status = 0;
out_data->steri_clean = 1;
out_data->set_point = 0x06;
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::CENTER;
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::CENTER;
out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
out_data->light_status = 0;
this->action_request_.value().message = haier_protocol::HaierMessage(
haier_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcommandsControl::SET_GROUP_PARAMETERS,
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
}
return true;
default: default:
HaierClimateBase::process_pending_action(); return HaierClimateBase::prepare_pending_action();
break;
} }
} }
void HonClimate::process_protocol_reset() {
HaierClimateBase::process_protocol_reset();
if (this->outdoor_sensor_ != nullptr) {
this->outdoor_sensor_->publish_state(NAN);
}
this->got_valid_outdoor_temp_ = false;
this->hvac_hardware_info_.reset();
}
} // namespace haier } // namespace haier
} // namespace esphome } // namespace esphome

View file

@ -30,6 +30,8 @@ enum class CleaningState : uint8_t {
STERI_CLEAN = 2, STERI_CLEAN = 2,
}; };
enum class HonControlMethod { MONITOR_ONLY = 0, SET_GROUP_PARAMETERS, SET_SINGLE_PARAMETER };
class HonClimate : public HaierClimateBase { class HonClimate : public HaierClimateBase {
public: public:
HonClimate(); HonClimate();
@ -48,44 +50,57 @@ class HonClimate : public HaierClimateBase {
CleaningState get_cleaning_status() const; CleaningState get_cleaning_status() const;
void start_self_cleaning(); void start_self_cleaning();
void start_steri_cleaning(); void start_steri_cleaning();
void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; };
void set_control_method(HonControlMethod method) { this->control_method_ = method; };
protected: protected:
void set_handlers() override; void set_handlers() override;
void process_phase(std::chrono::steady_clock::time_point now) override; void process_phase(std::chrono::steady_clock::time_point now) override;
haier_protocol::HaierMessage get_control_message() override; haier_protocol::HaierMessage get_control_message() override;
bool is_message_invalid(uint8_t message_type) override; haier_protocol::HaierMessage get_power_message(bool state) override;
void process_pending_action() override; bool prepare_pending_action() override;
void process_protocol_reset() override;
// Answers handlers // Answers handlers
haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data, haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type, const uint8_t *data,
size_t data_size); size_t data_size);
haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_management_information_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
const uint8_t *data, size_t data_size); haier_protocol::FrameType message_type,
haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
// Helper functions // Helper functions
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
std::unique_ptr<uint8_t[]> last_status_message_; void fill_control_messages_queue_();
void clear_control_messages_queue_();
struct HardwareInfo {
std::string protocol_version_;
std::string software_version_;
std::string hardware_version_;
std::string device_name_;
bool functions_[5];
};
bool beeper_status_; bool beeper_status_;
CleaningState cleaning_status_; CleaningState cleaning_status_;
bool got_valid_outdoor_temp_; bool got_valid_outdoor_temp_;
AirflowVerticalDirection vertical_direction_; AirflowVerticalDirection vertical_direction_;
AirflowHorizontalDirection horizontal_direction_; AirflowHorizontalDirection horizontal_direction_;
bool hvac_hardware_info_available_; esphome::optional<HardwareInfo> hvac_hardware_info_;
std::string hvac_protocol_version_;
std::string hvac_software_version_;
std::string hvac_hardware_version_;
std::string hvac_device_name_;
bool hvac_functions_[5];
bool &use_crc_;
uint8_t active_alarms_[8]; uint8_t active_alarms_[8];
int extra_control_packet_bytes_;
HonControlMethod control_method_;
esphome::sensor::Sensor *outdoor_sensor_; esphome::sensor::Sensor *outdoor_sensor_;
std::queue<haier_protocol::HaierMessage> control_messages_queue_;
}; };
} // namespace haier } // namespace haier

View file

@ -35,6 +35,20 @@ enum class ConditioningMode : uint8_t {
FAN = 0x06 FAN = 0x06
}; };
enum class DataParameters : uint8_t {
AC_POWER = 0x01,
SET_POINT = 0x02,
AC_MODE = 0x04,
FAN_MODE = 0x05,
USE_FAHRENHEIT = 0x07,
TEN_DEGREE = 0x0A,
HEALTH_MODE = 0x0B,
BEEPER_STATUS = 0x16,
LOCK_REMOTE = 0x17,
QUIET_MODE = 0x19,
FAST_MODE = 0x1A,
};
enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 }; enum class SpecialMode : uint8_t { NONE = 0x00, ELDERLY = 0x01, CHILDREN = 0x02, PREGNANT = 0x03 };
enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 }; enum class FanMode : uint8_t { FAN_HIGH = 0x01, FAN_MID = 0x02, FAN_LOW = 0x03, FAN_AUTO = 0x05 };
@ -124,11 +138,7 @@ struct HaierPacketSensors {
uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step) uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step)
}; };
struct HaierStatus { constexpr size_t HAIER_STATUS_FRAME_SIZE = 2 + sizeof(HaierPacketControl) + sizeof(HaierPacketSensors);
uint16_t subcommand;
HaierPacketControl control;
HaierPacketSensors sensors;
};
struct DeviceVersionAnswer { struct DeviceVersionAnswer {
char protocol_version[8]; char protocol_version[8];
@ -140,76 +150,6 @@ struct DeviceVersionAnswer {
uint8_t functions[2]; uint8_t functions[2];
}; };
// In this section comments:
// - module is the ESP32 control module (communication module in Haier protocol document)
// - device is the conditioner control board (network appliances in Haier protocol document)
enum class FrameType : uint8_t {
CONTROL = 0x01, // Requests or sets one or multiple parameters (module <-> device, required)
STATUS = 0x02, // Contains one or multiple parameters values, usually answer to control frame (module <-> device,
// required)
INVALID = 0x03, // Communication error indication (module <-> device, required)
ALARM_STATUS = 0x04, // Alarm status report (module <-> device, interactive, required)
CONFIRM = 0x05, // Acknowledgment, usually used to confirm reception of frame if there is no special answer (module
// <-> device, required)
REPORT = 0x06, // Report frame (module <-> device, interactive, required)
STOP_FAULT_ALARM = 0x09, // Stop fault alarm frame (module -> device, interactive, required)
SYSTEM_DOWNLINK = 0x11, // System downlink frame (module -> device, optional)
DEVICE_UPLINK = 0x12, // Device uplink frame (module <- device , interactive, optional)
SYSTEM_QUERY = 0x13, // System query frame (module -> device, optional)
SYSTEM_QUERY_RESPONSE = 0x14, // System query response frame (module <- device , optional)
DEVICE_QUERY = 0x15, // Device query frame (module <- device, optional)
DEVICE_QUERY_RESPONSE = 0x16, // Device query response frame (module -> device, optional)
GROUP_COMMAND = 0x60, // Group command frame (module -> device, interactive, optional)
GET_DEVICE_VERSION = 0x61, // Requests device version (module -> device, required)
GET_DEVICE_VERSION_RESPONSE = 0x62, // Device version answer (module <- device, required_
GET_ALL_ADDRESSES = 0x67, // Requests all devices addresses (module -> device, interactive, optional)
GET_ALL_ADDRESSES_RESPONSE =
0x68, // Answer to request of all devices addresses (module <- device , interactive, optional)
HANDSET_CHANGE_NOTIFICATION = 0x69, // Handset change notification frame (module <- device , interactive, optional)
GET_DEVICE_ID = 0x70, // Requests Device ID (module -> device, required)
GET_DEVICE_ID_RESPONSE = 0x71, // Response to device ID request (module <- device , required)
GET_ALARM_STATUS = 0x73, // Alarm status request (module -> device, required)
GET_ALARM_STATUS_RESPONSE = 0x74, // Response to alarm status request (module <- device, required)
GET_DEVICE_CONFIGURATION = 0x7C, // Requests device configuration (module -> device, interactive, required)
GET_DEVICE_CONFIGURATION_RESPONSE =
0x7D, // Response to device configuration request (module <- device, interactive, required)
DOWNLINK_TRANSPARENT_TRANSMISSION = 0x8C, // Downlink transparent transmission (proxy data Haier cloud -> device)
// (module -> device, interactive, optional)
UPLINK_TRANSPARENT_TRANSMISSION = 0x8D, // Uplink transparent transmission (proxy data device -> Haier cloud) (module
// <- device, interactive, optional)
START_DEVICE_UPGRADE = 0xE1, // Initiate device OTA upgrade (module -> device, OTA required)
START_DEVICE_UPGRADE_RESPONSE = 0xE2, // Response to initiate device upgrade command (module <- device, OTA required)
GET_FIRMWARE_CONTENT = 0xE5, // Requests to send firmware (module <- device, OTA required)
GET_FIRMWARE_CONTENT_RESPONSE =
0xE6, // Response to send firmware request (module -> device, OTA required) (multipacket?)
CHANGE_BAUD_RATE = 0xE7, // Requests to change port baud rate (module <- device, OTA required)
CHANGE_BAUD_RATE_RESPONSE = 0xE8, // Response to change port baud rate request (module -> device, OTA required)
GET_SUBBOARD_INFO = 0xE9, // Requests subboard information (module -> device, required)
GET_SUBBOARD_INFO_RESPONSE = 0xEA, // Response to subboard information request (module <- device, required)
GET_HARDWARE_INFO = 0xEB, // Requests information about device and subboard (module -> device, required)
GET_HARDWARE_INFO_RESPONSE = 0xEC, // Response to hardware information request (module <- device, required)
GET_UPGRADE_RESULT = 0xED, // Requests result of the firmware update (module <- device, OTA required)
GET_UPGRADE_RESULT_RESPONSE = 0xEF, // Response to firmware update results request (module -> device, OTA required)
GET_NETWORK_STATUS = 0xF0, // Requests network status (module <- device, interactive, optional)
GET_NETWORK_STATUS_RESPONSE = 0xF1, // Response to network status request (module -> device, interactive, optional)
START_WIFI_CONFIGURATION = 0xF2, // Starts WiFi configuration procedure (module <- device, interactive, required)
START_WIFI_CONFIGURATION_RESPONSE =
0xF3, // Response to start WiFi configuration request (module -> device, interactive, required)
STOP_WIFI_CONFIGURATION = 0xF4, // Stop WiFi configuration procedure (module <- device, interactive, required)
STOP_WIFI_CONFIGURATION_RESPONSE =
0xF5, // Response to stop WiFi configuration request (module -> device, interactive, required)
REPORT_NETWORK_STATUS = 0xF7, // Reports network status (module -> device, required)
CLEAR_CONFIGURATION = 0xF8, // Request to clear module configuration (module <- device, interactive, optional)
BIG_DATA_REPORT_CONFIGURATION =
0xFA, // Configuration for autoreport device full status (module -> device, interactive, optional)
BIG_DATA_REPORT_CONFIGURATION_RESPONSE =
0xFB, // Response to set big data configuration (module <- device, interactive, optional)
GET_MANAGEMENT_INFORMATION = 0xFC, // Request management information from device (module -> device, required)
GET_MANAGEMENT_INFORMATION_RESPONSE =
0xFD, // Response to management information request (module <- device, required)
WAKE_UP = 0xFE, // Request to wake up (module <-> device, optional)
};
enum class SubcommandsControl : uint16_t { enum class SubcommandsControl : uint16_t {
GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...) GET_PARAMETERS = 0x4C01, // Request specific parameters (packet content: parameter ID1 + parameter ID2 + ...)
GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None) GET_USER_DATA = 0x4D01, // Request all user data from device (packet content: None)

View file

@ -12,21 +12,28 @@ namespace haier {
static const char *const TAG = "haier.climate"; static const char *const TAG = "haier.climate";
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000; constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
constexpr uint8_t INIT_REQUESTS_RETRY = 2;
constexpr std::chrono::milliseconds INIT_REQUESTS_RETRY_INTERVAL = std::chrono::milliseconds(2000);
Smartair2Climate::Smartair2Climate() Smartair2Climate::Smartair2Climate() {
: last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]), timeouts_counter_(0) {} last_status_message_ = std::unique_ptr<uint8_t[]>(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]);
}
haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError Smartair2Climate::status_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size) { const uint8_t *data, size_t data_size) {
haier_protocol::HandlerError result = haier_protocol::HandlerError result =
this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type, this->answer_preprocess_(request_type, haier_protocol::FrameType::CONTROL, message_type,
(uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN); haier_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
if (result == haier_protocol::HandlerError::HANDLER_OK) { if (result == haier_protocol::HandlerError::HANDLER_OK) {
result = this->process_status_message_(data, data_size); result = this->process_status_message_(data, data_size);
if (result != haier_protocol::HandlerError::HANDLER_OK) { if (result != haier_protocol::HandlerError::HANDLER_OK) {
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result); ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->reset_phase_();
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); this->action_request_.reset();
this->force_send_control_ = false;
} else { } else {
if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) { if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) {
memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl)); memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl));
@ -34,36 +41,45 @@ haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_t
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
sizeof(smartair2_protocol::HaierPacketControl)); sizeof(smartair2_protocol::HaierPacketControl));
} }
if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) { switch (this->protocol_phase_) {
ESP_LOGI(TAG, "First HVAC status received"); case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
this->set_phase(ProtocolPhases::IDLE); ESP_LOGI(TAG, "First HVAC status received");
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) { this->set_phase(ProtocolPhases::IDLE);
this->set_phase(ProtocolPhases::IDLE); break;
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) { case ProtocolPhases::SENDING_ACTION_COMMAND:
this->set_phase(ProtocolPhases::IDLE); // Do nothing, phase will be changed in process_phase
this->set_force_send_control_(false); break;
if (this->hvac_settings_.valid) case ProtocolPhases::SENDING_STATUS_REQUEST:
this->hvac_settings_.reset(); this->set_phase(ProtocolPhases::IDLE);
break;
case ProtocolPhases::SENDING_CONTROL:
this->set_phase(ProtocolPhases::IDLE);
this->force_send_control_ = false;
if (this->current_hvac_settings_.valid)
this->current_hvac_settings_.reset();
break;
default:
break;
} }
} }
return result; return result;
} else { } else {
this->set_phase((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE this->action_request_.reset();
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); this->force_send_control_ = false;
this->reset_phase_();
return result; return result;
} }
} }
haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(uint8_t request_type, haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler_(
uint8_t message_type, haier_protocol::FrameType request_type, haier_protocol::FrameType message_type, const uint8_t *data,
const uint8_t *data, size_t data_size) {
size_t data_size) { if (request_type != haier_protocol::FrameType::GET_DEVICE_VERSION)
if (request_type != (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION)
return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE; return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
if (ProtocolPhases::WAITING_INIT_1_ANSWER != this->protocol_phase_) if (ProtocolPhases::SENDING_INIT_1 != this->protocol_phase_)
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE; return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
// Invalid packet is expected answer // Invalid packet is expected answer
if ((message_type == (uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) && if ((message_type == haier_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE) && (data_size >= 39) &&
((data[37] & 0x04) != 0)) { ((data[37] & 0x04) != 0)) {
ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol " ESP_LOGW(TAG, "It looks like your ESPHome Haier climate configuration is wrong. You should use the hOn protocol "
"instead of smartAir2"); "instead of smartAir2");
@ -72,58 +88,35 @@ haier_protocol::HandlerError Smartair2Climate::get_device_version_answer_handler
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
} }
haier_protocol::HandlerError Smartair2Climate::report_network_status_answer_handler_(uint8_t request_type, haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cycle_for_init_(
uint8_t message_type, haier_protocol::FrameType message_type) {
const uint8_t *data,
size_t data_size) {
haier_protocol::HandlerError result = this->answer_preprocess_(
request_type, (uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS, message_type,
(uint8_t) smartair2_protocol::FrameType::CONFIRM, ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
this->set_phase(ProtocolPhases::IDLE);
return result;
}
haier_protocol::HandlerError Smartair2Climate::initial_messages_timeout_handler_(uint8_t message_type) {
if (this->protocol_phase_ >= ProtocolPhases::IDLE) if (this->protocol_phase_ >= ProtocolPhases::IDLE)
return HaierClimateBase::timeout_default_handler_(message_type); return HaierClimateBase::timeout_default_handler_(message_type);
this->timeouts_counter_++; ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
ESP_LOGI(TAG, "Answer timeout for command %02X, phase %d, timeout counter %d", message_type, phase_to_string_(this->protocol_phase_));
(int) this->protocol_phase_, this->timeouts_counter_); ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
if (this->timeouts_counter_ >= 3) { if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)
ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1); new_phase = ProtocolPhases::SENDING_INIT_1;
if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) this->set_phase(new_phase);
new_phase = ProtocolPhases::SENDING_INIT_1;
this->set_phase(new_phase);
} else {
// Returning to the previous state to try again
this->set_phase((ProtocolPhases) ((int) this->protocol_phase_ - 1));
}
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
} }
void Smartair2Climate::set_handlers() { void Smartair2Climate::set_handlers() {
// Set handlers // Set handlers
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION), haier_protocol::FrameType::GET_DEVICE_VERSION,
std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1, std::bind(&Smartair2Climate::get_device_version_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (smartair2_protocol::FrameType::CONTROL), haier_protocol::FrameType::CONTROL,
std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2,
std::placeholders::_3, std::placeholders::_4)); std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_answer_handler( this->haier_protocol_.set_answer_handler(
(uint8_t) (smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), haier_protocol::FrameType::REPORT_NETWORK_STATUS,
std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1, std::bind(&Smartair2Climate::report_network_status_answer_handler_, this, std::placeholders::_1,
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
this->haier_protocol_.set_timeout_handler( this->haier_protocol_.set_default_timeout_handler(
(uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_ID), std::bind(&Smartair2Climate::messages_timeout_handler_with_cycle_for_init_, this, std::placeholders::_1));
std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1));
this->haier_protocol_.set_timeout_handler(
(uint8_t) (smartair2_protocol::FrameType::GET_DEVICE_VERSION),
std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1));
this->haier_protocol_.set_timeout_handler(
(uint8_t) (smartair2_protocol::FrameType::CONTROL),
std::bind(&Smartair2Climate::initial_messages_timeout_handler_, this, std::placeholders::_1));
} }
void Smartair2Climate::dump_config() { void Smartair2Climate::dump_config() {
@ -134,9 +127,7 @@ void Smartair2Climate::dump_config() {
void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) { void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) {
switch (this->protocol_phase_) { switch (this->protocol_phase_) {
case ProtocolPhases::SENDING_INIT_1: case ProtocolPhases::SENDING_INIT_1:
if (this->can_send_message() && if (this->can_send_message() && this->is_protocol_initialisation_interval_exceeded_(now)) {
(((this->timeouts_counter_ == 0) && (this->is_protocol_initialisation_interval_exceeded_(now))) ||
((this->timeouts_counter_ > 0) && (this->is_message_interval_exceeded_(now))))) {
// Indicate device capabilities: // Indicate device capabilities:
// bit 0 - if 1 module support interactive mode // bit 0 - if 1 module support interactive mode
// bit 1 - if 1 module support controller-device mode // bit 1 - if 1 module support controller-device mode
@ -145,92 +136,65 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
// bit 4..bit 15 - not used // bit 4..bit 15 - not used
uint8_t module_capabilities[2] = {0b00000000, 0b00000111}; uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST( static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
(uint8_t) smartair2_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, haier_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
sizeof(module_capabilities)); this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL);
this->send_message_(DEVICE_VERSION_REQUEST, false);
this->set_phase(ProtocolPhases::WAITING_INIT_1_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_INIT_2: case ProtocolPhases::SENDING_INIT_2:
case ProtocolPhases::WAITING_INIT_2_ANSWER:
this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
break; break;
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST: case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
case ProtocolPhases::SENDING_STATUS_REQUEST: case ProtocolPhases::SENDING_STATUS_REQUEST:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL, static const haier_protocol::HaierMessage STATUS_REQUEST(haier_protocol::FrameType::CONTROL, 0x4D01);
0x4D01); if (this->protocol_phase_ == ProtocolPhases::SENDING_FIRST_STATUS_REQUEST) {
this->send_message_(STATUS_REQUEST, false); this->send_message_(STATUS_REQUEST, this->use_crc_, INIT_REQUESTS_RETRY, INIT_REQUESTS_RETRY_INTERVAL);
} else {
this->send_message_(STATUS_REQUEST, this->use_crc_);
}
this->last_status_request_ = now; this->last_status_request_ = now;
this->set_phase((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
} }
break; break;
#ifdef USE_WIFI #ifdef USE_WIFI
case ProtocolPhases::SENDING_SIGNAL_LEVEL: case ProtocolPhases::SENDING_SIGNAL_LEVEL:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) { if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
this->send_message_( this->send_message_(this->get_wifi_signal_message_(), this->use_crc_);
this->get_wifi_signal_message_((uint8_t) smartair2_protocol::FrameType::REPORT_NETWORK_STATUS), false);
this->last_signal_request_ = now; this->last_signal_request_ = now;
this->set_phase(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
} }
break; break;
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
break;
#else #else
case ProtocolPhases::SENDING_SIGNAL_LEVEL: case ProtocolPhases::SENDING_SIGNAL_LEVEL:
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
break; break;
#endif #endif
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST: case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL); this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
break; break;
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST: case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
break; break;
case ProtocolPhases::SENDING_CONTROL: case ProtocolPhases::SENDING_CONTROL:
if (this->first_control_attempt_) { if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
this->control_request_timestamp_ = now; ESP_LOGI(TAG, "Sending control packet");
this->first_control_attempt_ = false; this->send_message_(get_control_message(), this->use_crc_, CONTROL_MESSAGE_RETRIES,
CONTROL_MESSAGE_RETRIES_INTERVAL);
} }
if (this->is_control_message_timeout_exceeded_(now)) { break;
ESP_LOGW(TAG, "Sending control packet timeout!"); case ProtocolPhases::SENDING_ACTION_COMMAND:
this->set_force_send_control_(false); if (this->action_request_.has_value()) {
if (this->hvac_settings_.valid) if (this->action_request_.value().message.has_value()) {
this->hvac_settings_.reset(); this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
this->forced_request_status_ = true; this->action_request_.value().message.reset();
this->forced_publish_ = true; } else {
// Message already sent, reseting request and return to idle
this->action_request_.reset();
this->set_phase(ProtocolPhases::IDLE);
}
} else {
ESP_LOGW(TAG, "SENDING_ACTION_COMMAND phase without action request!");
this->set_phase(ProtocolPhases::IDLE); this->set_phase(ProtocolPhases::IDLE);
} else if (this->can_send_message() && this->is_control_message_interval_exceeded_(
now)) // Using CONTROL_MESSAGES_INTERVAL_MS to speedup requests
{
haier_protocol::HaierMessage control_message = get_control_message();
this->send_message_(control_message, false);
ESP_LOGI(TAG, "Control packet sent");
this->set_phase(ProtocolPhases::WAITING_CONTROL_ANSWER);
} }
break; break;
case ProtocolPhases::SENDING_POWER_ON_COMMAND:
case ProtocolPhases::SENDING_POWER_OFF_COMMAND:
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
haier_protocol::HaierMessage power_cmd(
(uint8_t) smartair2_protocol::FrameType::CONTROL,
this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND ? 0x4D02 : 0x4D03);
this->send_message_(power_cmd, false);
this->set_phase(this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND
? ProtocolPhases::WAITING_POWER_ON_ANSWER
: ProtocolPhases::WAITING_POWER_OFF_ANSWER);
}
break;
case ProtocolPhases::WAITING_INIT_1_ANSWER:
case ProtocolPhases::WAITING_FIRST_STATUS_ANSWER:
case ProtocolPhases::WAITING_STATUS_ANSWER:
case ProtocolPhases::WAITING_CONTROL_ANSWER:
case ProtocolPhases::WAITING_POWER_ON_ANSWER:
case ProtocolPhases::WAITING_POWER_OFF_ANSWER:
break;
case ProtocolPhases::IDLE: { case ProtocolPhases::IDLE: {
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) { if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST); this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST);
@ -245,55 +209,55 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
} break; } break;
default: default:
// Shouldn't get here // Shouldn't get here
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication", ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_); phase_to_string_(this->protocol_phase_), (int) this->protocol_phase_);
#else
ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
#endif
this->set_phase(ProtocolPhases::SENDING_INIT_1); this->set_phase(ProtocolPhases::SENDING_INIT_1);
break; break;
} }
} }
haier_protocol::HaierMessage Smartair2Climate::get_power_message(bool state) {
if (state) {
static haier_protocol::HaierMessage power_on_message(haier_protocol::FrameType::CONTROL, 0x4D02);
return power_on_message;
} else {
static haier_protocol::HaierMessage power_off_message(haier_protocol::FrameType::CONTROL, 0x4D03);
return power_off_message;
}
}
haier_protocol::HaierMessage Smartair2Climate::get_control_message() { haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)]; uint8_t control_out_buffer[sizeof(smartair2_protocol::HaierPacketControl)];
memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl)); memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(smartair2_protocol::HaierPacketControl));
smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer; smartair2_protocol::HaierPacketControl *out_data = (smartair2_protocol::HaierPacketControl *) control_out_buffer;
out_data->cntrl = 0; out_data->cntrl = 0;
if (this->hvac_settings_.valid) { if (this->current_hvac_settings_.valid) {
HvacSettings climate_control; HvacSettings &climate_control = this->current_hvac_settings_;
climate_control = this->hvac_settings_;
if (climate_control.mode.has_value()) { if (climate_control.mode.has_value()) {
switch (climate_control.mode.value()) { switch (climate_control.mode.value()) {
case CLIMATE_MODE_OFF: case CLIMATE_MODE_OFF:
out_data->ac_power = 0; out_data->ac_power = 0;
break; break;
case CLIMATE_MODE_HEAT_COOL: case CLIMATE_MODE_HEAT_COOL:
out_data->ac_power = 1; out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO;
out_data->fan_mode = this->other_modes_fan_speed_; out_data->fan_mode = this->other_modes_fan_speed_;
break; break;
case CLIMATE_MODE_HEAT: case CLIMATE_MODE_HEAT:
out_data->ac_power = 1; out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT;
out_data->fan_mode = this->other_modes_fan_speed_; out_data->fan_mode = this->other_modes_fan_speed_;
break; break;
case CLIMATE_MODE_DRY: case CLIMATE_MODE_DRY:
out_data->ac_power = 1; out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY;
out_data->fan_mode = this->other_modes_fan_speed_; out_data->fan_mode = this->other_modes_fan_speed_;
break; break;
case CLIMATE_MODE_FAN_ONLY: case CLIMATE_MODE_FAN_ONLY:
out_data->ac_power = 1; out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::FAN;
out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode
break; break;
case CLIMATE_MODE_COOL: case CLIMATE_MODE_COOL:
out_data->ac_power = 1; out_data->ac_power = 1;
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL; out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL;
@ -327,32 +291,49 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
} }
// Set swing mode // Set swing mode
if (climate_control.swing_mode.has_value()) { if (climate_control.swing_mode.has_value()) {
switch (climate_control.swing_mode.value()) { if (this->use_alternative_swing_control_) {
case CLIMATE_SWING_OFF: switch (climate_control.swing_mode.value()) {
out_data->use_swing_bits = 0; case CLIMATE_SWING_OFF:
out_data->swing_both = 0; out_data->swing_mode = 0;
break; break;
case CLIMATE_SWING_VERTICAL: case CLIMATE_SWING_VERTICAL:
out_data->swing_both = 0; out_data->swing_mode = 1;
out_data->vertical_swing = 1; break;
out_data->horizontal_swing = 0; case CLIMATE_SWING_HORIZONTAL:
break; out_data->swing_mode = 2;
case CLIMATE_SWING_HORIZONTAL: break;
out_data->swing_both = 0; case CLIMATE_SWING_BOTH:
out_data->vertical_swing = 0; out_data->swing_mode = 3;
out_data->horizontal_swing = 1; break;
break; }
case CLIMATE_SWING_BOTH: } else {
out_data->swing_both = 1; switch (climate_control.swing_mode.value()) {
out_data->use_swing_bits = 0; case CLIMATE_SWING_OFF:
out_data->vertical_swing = 0; out_data->use_swing_bits = 0;
out_data->horizontal_swing = 0; out_data->swing_mode = 0;
break; break;
case CLIMATE_SWING_VERTICAL:
out_data->swing_mode = 0;
out_data->vertical_swing = 1;
out_data->horizontal_swing = 0;
break;
case CLIMATE_SWING_HORIZONTAL:
out_data->swing_mode = 0;
out_data->vertical_swing = 0;
out_data->horizontal_swing = 1;
break;
case CLIMATE_SWING_BOTH:
out_data->swing_mode = 1;
out_data->use_swing_bits = 0;
out_data->vertical_swing = 0;
out_data->horizontal_swing = 0;
break;
}
} }
} }
if (climate_control.target_temperature.has_value()) { if (climate_control.target_temperature.has_value()) {
float target_temp = climate_control.target_temperature.value(); float target_temp = climate_control.target_temperature.value();
out_data->set_point = target_temp - 16; // set the temperature with offset 16 out_data->set_point = ((int) target_temp) - 16; // set the temperature with offset 16
out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0; out_data->half_degree = (target_temp - ((int) target_temp) >= 0.49) ? 1 : 0;
} }
if (out_data->ac_power == 0) { if (out_data->ac_power == 0) {
@ -383,7 +364,7 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
} }
out_data->display_status = this->display_status_ ? 0 : 1; out_data->display_status = this->display_status_ ? 0 : 1;
out_data->health_mode = this->health_mode_ ? 1 : 0; out_data->health_mode = this->health_mode_ ? 1 : 0;
return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer, return haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer,
sizeof(smartair2_protocol::HaierPacketControl)); sizeof(smartair2_protocol::HaierPacketControl));
} }
@ -459,13 +440,19 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
// Do something only if display status changed // Do something only if display status changed
if (this->mode == CLIMATE_MODE_OFF) { if (this->mode == CLIMATE_MODE_OFF) {
// AC just turned on from remote need to turn off display // AC just turned on from remote need to turn off display
this->set_force_send_control_(true); this->force_send_control_ = true;
} else { } else {
this->display_status_ = disp_status; this->display_status_ = disp_status;
} }
} }
} }
} }
{
// Health mode
bool old_health_mode = this->health_mode_;
this->health_mode_ = packet.control.health_mode == 1;
should_publish = should_publish || (old_health_mode != this->health_mode_);
}
{ {
// Climate mode // Climate mode
ClimateMode old_mode = this->mode; ClimateMode old_mode = this->mode;
@ -493,70 +480,57 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
} }
should_publish = should_publish || (old_mode != this->mode); should_publish = should_publish || (old_mode != this->mode);
} }
{
// Health mode
bool old_health_mode = this->health_mode_;
this->health_mode_ = packet.control.health_mode == 1;
should_publish = should_publish || (old_health_mode != this->health_mode_);
}
{ {
// Swing mode // Swing mode
ClimateSwingMode old_swing_mode = this->swing_mode; ClimateSwingMode old_swing_mode = this->swing_mode;
if (packet.control.swing_both == 0) { if (this->use_alternative_swing_control_) {
if (packet.control.vertical_swing != 0) { switch (packet.control.swing_mode) {
this->swing_mode = CLIMATE_SWING_VERTICAL; case 1:
} else if (packet.control.horizontal_swing != 0) { this->swing_mode = CLIMATE_SWING_VERTICAL;
this->swing_mode = CLIMATE_SWING_HORIZONTAL; break;
} else { case 2:
this->swing_mode = CLIMATE_SWING_OFF; this->swing_mode = CLIMATE_SWING_HORIZONTAL;
break;
case 3:
this->swing_mode = CLIMATE_SWING_BOTH;
break;
default:
this->swing_mode = CLIMATE_SWING_OFF;
break;
} }
} else { } else {
swing_mode = CLIMATE_SWING_BOTH; if (packet.control.swing_mode == 0) {
if (packet.control.vertical_swing != 0) {
this->swing_mode = CLIMATE_SWING_VERTICAL;
} else if (packet.control.horizontal_swing != 0) {
this->swing_mode = CLIMATE_SWING_HORIZONTAL;
} else {
this->swing_mode = CLIMATE_SWING_OFF;
}
} else {
swing_mode = CLIMATE_SWING_BOTH;
}
} }
should_publish = should_publish || (old_swing_mode != this->swing_mode); should_publish = should_publish || (old_swing_mode != this->swing_mode);
} }
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now(); this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
if (this->forced_publish_ || should_publish) { if (should_publish) {
#if (HAIER_LOG_LEVEL > 4)
std::chrono::high_resolution_clock::time_point _publish_start = std::chrono::high_resolution_clock::now();
#endif
this->publish_state(); this->publish_state();
#if (HAIER_LOG_LEVEL > 4)
ESP_LOGV(TAG, "Publish delay: %lld ms",
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::high_resolution_clock::now() -
_publish_start)
.count());
#endif
this->forced_publish_ = false;
} }
if (should_publish) { if (should_publish) {
ESP_LOGI(TAG, "HVAC values changed"); ESP_LOGI(TAG, "HVAC values changed");
} }
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, int log_level = should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG;
"HVAC Mode = 0x%X", packet.control.ac_mode); esp_log_printf_(log_level, TAG, __LINE__, "HVAC Mode = 0x%X", packet.control.ac_mode);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, esp_log_printf_(log_level, TAG, __LINE__, "Fan speed Status = 0x%X", packet.control.fan_mode);
"Fan speed Status = 0x%X", packet.control.fan_mode); esp_log_printf_(log_level, TAG, __LINE__, "Horizontal Swing Status = 0x%X", packet.control.horizontal_swing);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__, esp_log_printf_(log_level, TAG, __LINE__, "Vertical Swing Status = 0x%X", packet.control.vertical_swing);
"Horizontal Swing Status = 0x%X", packet.control.horizontal_swing); esp_log_printf_(log_level, TAG, __LINE__, "Set Point Status = 0x%X", packet.control.set_point);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Vertical Swing Status = 0x%X", packet.control.vertical_swing);
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), TAG, __LINE__,
"Set Point Status = 0x%X", packet.control.set_point);
return haier_protocol::HandlerError::HANDLER_OK; return haier_protocol::HandlerError::HANDLER_OK;
} }
bool Smartair2Climate::is_message_invalid(uint8_t message_type) { void Smartair2Climate::set_alternative_swing_control(bool swing_control) {
return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID; this->use_alternative_swing_control_ = swing_control;
}
void Smartair2Climate::set_phase(HaierClimateBase::ProtocolPhases phase) {
int old_phase = (int) this->protocol_phase_;
int new_phase = (int) phase;
int min_p = std::min(old_phase, new_phase);
int max_p = std::max(old_phase, new_phase);
if ((min_p % 2 != 0) || (max_p - min_p > 1))
this->timeouts_counter_ = 0;
HaierClimateBase::set_phase(phase);
} }
} // namespace haier } // namespace haier

View file

@ -13,27 +13,27 @@ class Smartair2Climate : public HaierClimateBase {
Smartair2Climate &operator=(const Smartair2Climate &) = delete; Smartair2Climate &operator=(const Smartair2Climate &) = delete;
~Smartair2Climate(); ~Smartair2Climate();
void dump_config() override; void dump_config() override;
void set_alternative_swing_control(bool swing_control);
protected: protected:
void set_handlers() override; void set_handlers() override;
void process_phase(std::chrono::steady_clock::time_point now) override; void process_phase(std::chrono::steady_clock::time_point now) override;
haier_protocol::HaierMessage get_power_message(bool state) override;
haier_protocol::HaierMessage get_control_message() override; haier_protocol::HaierMessage get_control_message() override;
bool is_message_invalid(uint8_t message_type) override; // Answer handlers
void set_phase(HaierClimateBase::ProtocolPhases phase) override; haier_protocol::HandlerError status_handler_(haier_protocol::FrameType request_type,
// Answer and timeout handlers haier_protocol::FrameType message_type, const uint8_t *data,
haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data,
size_t data_size); size_t data_size);
haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_device_version_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError get_device_id_answer_handler_(haier_protocol::FrameType request_type,
haier_protocol::FrameType message_type,
const uint8_t *data, size_t data_size); const uint8_t *data, size_t data_size);
haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type, haier_protocol::HandlerError messages_timeout_handler_with_cycle_for_init_(haier_protocol::FrameType message_type);
const uint8_t *data, size_t data_size);
haier_protocol::HandlerError initial_messages_timeout_handler_(uint8_t message_type);
// Helper functions // Helper functions
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size); haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
std::unique_ptr<uint8_t[]> last_status_message_; bool use_alternative_swing_control_;
unsigned int timeouts_counter_;
}; };
} // namespace haier } // namespace haier

View file

@ -41,8 +41,9 @@ struct HaierPacketControl {
// 24 // 24
uint8_t : 8; uint8_t : 8;
// 25 // 25
uint8_t swing_both; // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define uint8_t swing_mode; // In normal mode: If 1 - swing both direction, if 0 - horizontal_swing and
// vertical/horizontal/off // vertical_swing define vertical/horizontal/off
// In alternative mode: 0 - off, 01 - vertical, 02 - horizontal, 03 - both
// 26 // 26
uint8_t : 3; uint8_t : 3;
uint8_t use_fahrenheit : 1; uint8_t use_fahrenheit : 1;
@ -82,19 +83,6 @@ struct HaierStatus {
HaierPacketControl control; HaierPacketControl control;
}; };
enum class FrameType : uint8_t {
CONTROL = 0x01,
STATUS = 0x02,
INVALID = 0x03,
CONFIRM = 0x05,
GET_DEVICE_VERSION = 0x61,
GET_DEVICE_VERSION_RESPONSE = 0x62,
GET_DEVICE_ID = 0x70,
GET_DEVICE_ID_RESPONSE = 0x71,
REPORT_NETWORK_STATUS = 0xF7,
NO_COMMAND = 0xFF,
};
} // namespace smartair2_protocol } // namespace smartair2_protocol
} // namespace haier } // namespace haier
} // namespace esphome } // namespace esphome

View file

@ -40,6 +40,8 @@ const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; }
void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
if (this->lock_->traits.get_assumed_state()) if (this->lock_->traits.get_assumed_state())
root[MQTT_OPTIMISTIC] = true; root[MQTT_OPTIMISTIC] = true;
if (this->lock_->traits.get_supports_open())
root[MQTT_PAYLOAD_OPEN] = "OPEN";
} }
bool MQTTLockComponent::send_initial_state() { return this->publish_state(); } bool MQTTLockComponent::send_initial_state() { return this->publish_state(); }

View file

@ -154,6 +154,7 @@ void SSD1306::setup() {
// Set V_COM (0xDB) // Set V_COM (0xDB)
this->command(SSD1306_COMMAND_SET_VCOM_DETECT); this->command(SSD1306_COMMAND_SET_VCOM_DETECT);
switch (this->model_) { switch (this->model_) {
case SH1106_MODEL_128_64:
case SH1107_MODEL_128_64: case SH1107_MODEL_128_64:
case SH1107_MODEL_128_128: case SH1107_MODEL_128_128:
this->command(0x35); this->command(0x35);

View file

@ -403,6 +403,10 @@ async def to_code(config):
lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))), lambda ap: cg.add(var.set_ap(wifi_network(conf, ap, ip_config))),
) )
cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT])) cg.add(var.set_ap_timeout(conf[CONF_AP_TIMEOUT]))
cg.add_define("USE_WIFI_AP")
elif CORE.is_esp32 and CORE.using_esp_idf:
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_SOFTAP_SUPPORT", False)
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT])) cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE])) cg.add(var.set_power_save_mode(config[CONF_POWER_SAVE_MODE]))

View file

@ -82,6 +82,7 @@ void WiFiComponent::start() {
} else { } else {
this->start_scanning(); this->start_scanning();
} }
#ifdef USE_WIFI_AP
} else if (this->has_ap()) { } else if (this->has_ap()) {
this->setup_ap_config_(); this->setup_ap_config_();
if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) { if (this->output_power_.has_value() && !this->wifi_apply_output_power_(*this->output_power_)) {
@ -94,6 +95,7 @@ void WiFiComponent::start() {
captive_portal::global_captive_portal->start(); captive_portal::global_captive_portal->start();
} }
#endif #endif
#endif // USE_WIFI_AP
} }
#ifdef USE_IMPROV #ifdef USE_IMPROV
if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) { if (!this->has_sta() && esp32_improv::global_improv_component != nullptr) {
@ -160,6 +162,7 @@ void WiFiComponent::loop() {
return; return;
} }
#ifdef USE_WIFI_AP
if (this->has_ap() && !this->ap_setup_) { if (this->has_ap() && !this->ap_setup_) {
if (now - this->last_connected_ > this->ap_timeout_) { if (now - this->last_connected_ > this->ap_timeout_) {
ESP_LOGI(TAG, "Starting fallback AP!"); ESP_LOGI(TAG, "Starting fallback AP!");
@ -170,6 +173,7 @@ void WiFiComponent::loop() {
#endif #endif
} }
} }
#endif // USE_WIFI_AP
#ifdef USE_IMPROV #ifdef USE_IMPROV
if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) { if (esp32_improv::global_improv_component != nullptr && !esp32_improv::global_improv_component->is_active()) {
@ -201,11 +205,16 @@ void WiFiComponent::set_fast_connect(bool fast_connect) { this->fast_connect_ =
void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; } void WiFiComponent::set_btm(bool btm) { this->btm_ = btm; }
void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; } void WiFiComponent::set_rrm(bool rrm) { this->rrm_ = rrm; }
#endif #endif
network::IPAddress WiFiComponent::get_ip_address() { network::IPAddress WiFiComponent::get_ip_address() {
if (this->has_sta()) if (this->has_sta())
return this->wifi_sta_ip(); return this->wifi_sta_ip();
#ifdef USE_WIFI_AP
if (this->has_ap()) if (this->has_ap())
return this->wifi_soft_ap_ip(); return this->wifi_soft_ap_ip();
#endif // USE_WIFI_AP
return {}; return {};
} }
network::IPAddress WiFiComponent::get_dns_address(int num) { network::IPAddress WiFiComponent::get_dns_address(int num) {
@ -220,6 +229,8 @@ std::string WiFiComponent::get_use_address() const {
return this->use_address_; return this->use_address_;
} }
void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
#ifdef USE_WIFI_AP
void WiFiComponent::setup_ap_config_() { void WiFiComponent::setup_ap_config_() {
this->wifi_mode_({}, true); this->wifi_mode_({}, true);
@ -257,13 +268,16 @@ void WiFiComponent::setup_ap_config_() {
} }
} }
float WiFiComponent::get_loop_priority() const {
return 10.0f; // before other loop components
}
void WiFiComponent::set_ap(const WiFiAP &ap) { void WiFiComponent::set_ap(const WiFiAP &ap) {
this->ap_ = ap; this->ap_ = ap;
this->has_ap_ = true; this->has_ap_ = true;
} }
#endif // USE_WIFI_AP
float WiFiComponent::get_loop_priority() const {
return 10.0f; // before other loop components
}
void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); } void WiFiComponent::add_sta(const WiFiAP &ap) { this->sta_.push_back(ap); }
void WiFiComponent::set_sta(const WiFiAP &ap) { void WiFiComponent::set_sta(const WiFiAP &ap) {
this->clear_sta(); this->clear_sta();

View file

@ -194,6 +194,7 @@ class WiFiComponent : public Component {
void add_sta(const WiFiAP &ap); void add_sta(const WiFiAP &ap);
void clear_sta(); void clear_sta();
#ifdef USE_WIFI_AP
/** Setup an Access Point that should be created if no connection to a station can be made. /** Setup an Access Point that should be created if no connection to a station can be made.
* *
* This can also be used without set_sta(). Then the AP will always be active. * This can also be used without set_sta(). Then the AP will always be active.
@ -203,6 +204,7 @@ class WiFiComponent : public Component {
*/ */
void set_ap(const WiFiAP &ap); void set_ap(const WiFiAP &ap);
WiFiAP get_ap() { return this->ap_; } WiFiAP get_ap() { return this->ap_; }
#endif // USE_WIFI_AP
void enable(); void enable();
void disable(); void disable();
@ -300,7 +302,11 @@ class WiFiComponent : public Component {
protected: protected:
static std::string format_mac_addr(const uint8_t mac[6]); static std::string format_mac_addr(const uint8_t mac[6]);
#ifdef USE_WIFI_AP
void setup_ap_config_(); void setup_ap_config_();
#endif // USE_WIFI_AP
void print_connect_params_(); void print_connect_params_();
void wifi_loop_(); void wifi_loop_();
@ -314,8 +320,12 @@ class WiFiComponent : public Component {
void wifi_pre_setup_(); void wifi_pre_setup_();
WiFiSTAConnectStatus wifi_sta_connect_status_(); WiFiSTAConnectStatus wifi_sta_connect_status_();
bool wifi_scan_start_(bool passive); bool wifi_scan_start_(bool passive);
#ifdef USE_WIFI_AP
bool wifi_ap_ip_config_(optional<ManualIP> manual_ip); bool wifi_ap_ip_config_(optional<ManualIP> manual_ip);
bool wifi_start_ap_(const WiFiAP &ap); bool wifi_start_ap_(const WiFiAP &ap);
#endif // USE_WIFI_AP
bool wifi_disconnect_(); bool wifi_disconnect_();
int32_t wifi_channel_(); int32_t wifi_channel_();
network::IPAddress wifi_subnet_mask_(); network::IPAddress wifi_subnet_mask_();

View file

@ -597,6 +597,8 @@ void WiFiComponent::wifi_scan_done_callback_() {
WiFi.scanDelete(); WiFi.scanDelete();
this->scan_done_ = true; this->scan_done_ = true;
} }
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) { bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
esp_err_t err; esp_err_t err;
@ -654,6 +656,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return true; return true;
} }
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -692,11 +695,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return true; return true;
} }
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
tcpip_adapter_ip_info_t ip; tcpip_adapter_ip_info_t ip;
tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip); tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_AP, &ip);
return network::IPAddress(&ip.ip); return network::IPAddress(&ip.ip);
} }
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); } bool WiFiComponent::wifi_disconnect_() { return esp_wifi_disconnect(); }
bssid_t WiFiComponent::wifi_bssid() { bssid_t WiFiComponent::wifi_bssid() {

View file

@ -688,6 +688,8 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
} }
this->scan_done_ = true; this->scan_done_ = true;
} }
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) { bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -753,6 +755,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return true; return true;
} }
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -790,11 +793,14 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return true; return true;
} }
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
struct ip_info ip {}; struct ip_info ip {};
wifi_get_ip_info(SOFTAP_IF, &ip); wifi_get_ip_info(SOFTAP_IF, &ip);
return network::IPAddress(&ip.ip); return network::IPAddress(&ip.ip);
} }
#endif // USE_WIFI_AP
bssid_t WiFiComponent::wifi_bssid() { bssid_t WiFiComponent::wifi_bssid() {
bssid_t bssid{}; bssid_t bssid{};
uint8_t *raw_bssid = WiFi.BSSID(); uint8_t *raw_bssid = WiFi.BSSID();

View file

@ -17,7 +17,11 @@
#ifdef USE_WIFI_WPA2_EAP #ifdef USE_WIFI_WPA2_EAP
#include <esp_wpa2.h> #include <esp_wpa2.h>
#endif #endif
#ifdef USE_WIFI_AP
#include "dhcpserver/dhcpserver.h" #include "dhcpserver/dhcpserver.h"
#endif // USE_WIFI_AP
#include "lwip/err.h" #include "lwip/err.h"
#include "lwip/dns.h" #include "lwip/dns.h"
@ -35,15 +39,19 @@ static const char *const TAG = "wifi_esp32";
static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static EventGroupHandle_t s_wifi_event_group; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static QueueHandle_t s_event_queue; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_sta_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) #ifdef USE_WIFI_AP
static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static esp_netif_t *s_ap_netif = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) #endif // USE_WIFI_AP
static bool s_ap_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_sta_got_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_wifi_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) static bool s_ap_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connect_not_found = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connect_error = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_sta_connecting = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static bool s_wifi_started = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
struct IDFWiFiEvent { struct IDFWiFiEvent {
esp_event_base_t event_base; esp_event_base_t event_base;
@ -159,7 +167,11 @@ void WiFiComponent::wifi_pre_setup_() {
} }
s_sta_netif = esp_netif_create_default_wifi_sta(); s_sta_netif = esp_netif_create_default_wifi_sta();
#ifdef USE_WIFI_AP
s_ap_netif = esp_netif_create_default_wifi_ap(); s_ap_netif = esp_netif_create_default_wifi_ap();
#endif // USE_WIFI_AP
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
// cfg.nvs_enable = false; // cfg.nvs_enable = false;
err = esp_wifi_init(&cfg); err = esp_wifi_init(&cfg);
@ -674,6 +686,11 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
return; return;
} }
if (it.number == 0) {
// no results
return;
}
uint16_t number = it.number; uint16_t number = it.number;
std::vector<wifi_ap_record_t> records(number); std::vector<wifi_ap_record_t> records(number);
err = esp_wifi_scan_get_ap_records(&number, records.data()); err = esp_wifi_scan_get_ap_records(&number, records.data());
@ -761,6 +778,8 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
scan_done_ = false; scan_done_ = false;
return true; return true;
} }
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) { bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
esp_err_t err; esp_err_t err;
@ -816,6 +835,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return true; return true;
} }
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -853,6 +873,8 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return true; return true;
} }
#endif // USE_WIFI_AP
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { network::IPAddress WiFiComponent::wifi_soft_ap_ip() {
esp_netif_ip_info_t ip; esp_netif_ip_info_t ip;
esp_netif_get_ip_info(s_sta_netif, &ip); esp_netif_get_ip_info(s_sta_netif, &ip);

View file

@ -412,6 +412,8 @@ void WiFiComponent::wifi_scan_done_callback_() {
WiFi.scanDelete(); WiFi.scanDelete();
this->scan_done_ = true; this->scan_done_ = true;
} }
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) { bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -423,6 +425,7 @@ bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
return WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0)); return WiFi.softAPConfig(IPAddress(192, 168, 4, 1), IPAddress(192, 168, 4, 1), IPAddress(255, 255, 255, 0));
} }
} }
bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) { bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
// enable AP // enable AP
if (!this->wifi_mode_({}, true)) if (!this->wifi_mode_({}, true))
@ -438,7 +441,10 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(), return WiFi.softAP(ap.get_ssid().c_str(), ap.get_password().empty() ? NULL : ap.get_password().c_str(),
ap.get_channel().value_or(1), ap.get_hidden()); ap.get_channel().value_or(1), ap.get_hidden());
} }
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; } network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {WiFi.softAPIP()}; }
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); } bool WiFiComponent::wifi_disconnect_() { return WiFi.disconnect(); }
bssid_t WiFiComponent::wifi_bssid() { bssid_t WiFiComponent::wifi_bssid() {

View file

@ -138,6 +138,7 @@ bool WiFiComponent::wifi_scan_start_(bool passive) {
return true; return true;
} }
#ifdef USE_WIFI_AP
bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) { bool WiFiComponent::wifi_ap_ip_config_(optional<ManualIP> manual_ip) {
// TODO: // TODO:
return false; return false;
@ -151,7 +152,9 @@ bool WiFiComponent::wifi_start_ap_(const WiFiAP &ap) {
return true; return true;
} }
network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; } network::IPAddress WiFiComponent::wifi_soft_ap_ip() { return {(const ip_addr_t *) WiFi.localIP()}; }
#endif // USE_WIFI_AP
bool WiFiComponent::wifi_disconnect_() { bool WiFiComponent::wifi_disconnect_() {
int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA); int err = cyw43_wifi_leave(&cyw43_state, CYW43_ITF_STA);

View file

@ -50,6 +50,7 @@
#define USE_TOUCHSCREEN #define USE_TOUCHSCREEN
#define USE_UART_DEBUGGER #define USE_UART_DEBUGGER
#define USE_WIFI #define USE_WIFI
#define USE_WIFI_AP
// Arduino-specific feature flags // Arduino-specific feature flags
#ifdef USE_ARDUINO #ifdef USE_ARDUINO

View file

@ -0,0 +1,8 @@
from __future__ import annotations
EVENT_ENTRY_ADDED = "entry_added"
EVENT_ENTRY_REMOVED = "entry_removed"
EVENT_ENTRY_UPDATED = "entry_updated"
EVENT_ENTRY_STATE_CHANGED = "entry_state_changed"
SENTINEL = object()

View file

@ -3,7 +3,9 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
import threading import threading
from typing import TYPE_CHECKING from dataclasses import dataclass
from functools import partial
from typing import TYPE_CHECKING, Any, Callable
from ..zeroconf import DiscoveredImport from ..zeroconf import DiscoveredImport
from .entries import DashboardEntries from .entries import DashboardEntries
@ -12,16 +14,55 @@ from .settings import DashboardSettings
if TYPE_CHECKING: if TYPE_CHECKING:
from .status.mdns import MDNSStatus from .status.mdns import MDNSStatus
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@dataclass
class Event:
"""Dashboard Event."""
event_type: str
data: dict[str, Any]
class EventBus:
"""Dashboard event bus."""
def __init__(self) -> None:
"""Initialize the Dashboard event bus."""
self._listeners: dict[str, set[Callable[[Event], None]]] = {}
def async_add_listener(
self, event_type: str, listener: Callable[[Event], None]
) -> Callable[[], None]:
"""Add a listener to the event bus."""
self._listeners.setdefault(event_type, set()).add(listener)
return partial(self._async_remove_listener, event_type, listener)
def _async_remove_listener(
self, event_type: str, listener: Callable[[Event], None]
) -> None:
"""Remove a listener from the event bus."""
self._listeners[event_type].discard(listener)
def async_fire(self, event_type: str, event_data: dict[str, Any]) -> None:
"""Fire an event."""
event = Event(event_type, event_data)
_LOGGER.debug("Firing event: %s", event)
for listener in self._listeners.get(event_type, set()):
listener(event)
class ESPHomeDashboard: class ESPHomeDashboard:
"""Class that represents the dashboard.""" """Class that represents the dashboard."""
__slots__ = ( __slots__ = (
"bus",
"entries", "entries",
"loop", "loop",
"ping_result",
"import_result", "import_result",
"stop_event", "stop_event",
"ping_request", "ping_request",
@ -32,9 +73,9 @@ class ESPHomeDashboard:
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize the ESPHomeDashboard.""" """Initialize the ESPHomeDashboard."""
self.bus = EventBus()
self.entries: DashboardEntries | None = None self.entries: DashboardEntries | None = None
self.loop: asyncio.AbstractEventLoop | None = None self.loop: asyncio.AbstractEventLoop | None = None
self.ping_result: dict[str, bool | None] = {}
self.import_result: dict[str, DiscoveredImport] = {} self.import_result: dict[str, DiscoveredImport] = {}
self.stop_event = threading.Event() self.stop_event = threading.Event()
self.ping_request: asyncio.Event | None = None self.ping_request: asyncio.Event | None = None
@ -46,7 +87,7 @@ class ESPHomeDashboard:
"""Setup the dashboard.""" """Setup the dashboard."""
self.loop = asyncio.get_running_loop() self.loop = asyncio.get_running_loop()
self.ping_request = asyncio.Event() self.ping_request = asyncio.Event()
self.entries = DashboardEntries(self.settings.config_dir) self.entries = DashboardEntries(self)
async def async_run(self) -> None: async def async_run(self) -> None:
"""Run the dashboard.""" """Run the dashboard."""

View file

@ -3,24 +3,80 @@ from __future__ import annotations
import asyncio import asyncio
import logging import logging
import os import os
from collections import defaultdict
from typing import TYPE_CHECKING, Any
from esphome import const, util from esphome import const, util
from esphome.storage_json import StorageJSON, ext_storage_path from esphome.storage_json import StorageJSON, ext_storage_path
from .const import (
EVENT_ENTRY_ADDED,
EVENT_ENTRY_REMOVED,
EVENT_ENTRY_STATE_CHANGED,
EVENT_ENTRY_UPDATED,
)
from .enum import StrEnum
if TYPE_CHECKING:
from .core import ESPHomeDashboard
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DashboardCacheKeyType = tuple[int, int, float, int] DashboardCacheKeyType = tuple[int, int, float, int]
# Currently EntryState is a simple
# online/offline/unknown enum, but in the future
# it may be expanded to include more states
class EntryState(StrEnum):
ONLINE = "online"
OFFLINE = "offline"
UNKNOWN = "unknown"
_BOOL_TO_ENTRY_STATE = {
True: EntryState.ONLINE,
False: EntryState.OFFLINE,
None: EntryState.UNKNOWN,
}
_ENTRY_STATE_TO_BOOL = {
EntryState.ONLINE: True,
EntryState.OFFLINE: False,
EntryState.UNKNOWN: None,
}
def bool_to_entry_state(value: bool) -> EntryState:
"""Convert a bool to an entry state."""
return _BOOL_TO_ENTRY_STATE[value]
def entry_state_to_bool(value: EntryState) -> bool | None:
"""Convert an entry state to a bool."""
return _ENTRY_STATE_TO_BOOL[value]
class DashboardEntries: class DashboardEntries:
"""Represents all dashboard entries.""" """Represents all dashboard entries."""
__slots__ = ("_loop", "_config_dir", "_entries", "_loaded_entries", "_update_lock") __slots__ = (
"_dashboard",
"_loop",
"_config_dir",
"_entries",
"_entry_states",
"_loaded_entries",
"_update_lock",
"_name_to_entry",
)
def __init__(self, config_dir: str) -> None: def __init__(self, dashboard: ESPHomeDashboard) -> None:
"""Initialize the DashboardEntries.""" """Initialize the DashboardEntries."""
self._dashboard = dashboard
self._loop = asyncio.get_running_loop() self._loop = asyncio.get_running_loop()
self._config_dir = config_dir self._config_dir = dashboard.settings.config_dir
# Entries are stored as # Entries are stored as
# { # {
# "path/to/file.yaml": DashboardEntry, # "path/to/file.yaml": DashboardEntry,
@ -29,11 +85,16 @@ class DashboardEntries:
self._entries: dict[str, DashboardEntry] = {} self._entries: dict[str, DashboardEntry] = {}
self._loaded_entries = False self._loaded_entries = False
self._update_lock = asyncio.Lock() self._update_lock = asyncio.Lock()
self._name_to_entry: dict[str, set[DashboardEntry]] = defaultdict(set)
def get(self, path: str) -> DashboardEntry | None: def get(self, path: str) -> DashboardEntry | None:
"""Get an entry by path.""" """Get an entry by path."""
return self._entries.get(path) return self._entries.get(path)
def get_by_name(self, name: str) -> set[DashboardEntry] | None:
"""Get an entry by name."""
return self._name_to_entry.get(name)
async def _async_all(self) -> list[DashboardEntry]: async def _async_all(self) -> list[DashboardEntry]:
"""Return all entries.""" """Return all entries."""
return list(self._entries.values()) return list(self._entries.values())
@ -46,6 +107,25 @@ class DashboardEntries:
"""Return all entries.""" """Return all entries."""
return list(self._entries.values()) return list(self._entries.values())
def set_state(self, entry: DashboardEntry, state: EntryState) -> None:
"""Set the state for an entry."""
asyncio.run_coroutine_threadsafe(
self._async_set_state(entry, state), self._loop
).result()
async def _async_set_state(self, entry: DashboardEntry, state: EntryState) -> None:
"""Set the state for an entry."""
self.async_set_state(entry, state)
def async_set_state(self, entry: DashboardEntry, state: EntryState) -> None:
"""Set the state for an entry."""
if entry.state == state:
return
entry.state = state
self._dashboard.bus.async_fire(
EVENT_ENTRY_STATE_CHANGED, {"entry": entry, "state": state}
)
async def async_request_update_entries(self) -> None: async def async_request_update_entries(self) -> None:
"""Request an update of the dashboard entries from disk. """Request an update of the dashboard entries from disk.
@ -81,38 +161,48 @@ class DashboardEntries:
path_to_cache_key = await self._loop.run_in_executor( path_to_cache_key = await self._loop.run_in_executor(
None, self._get_path_to_cache_key None, self._get_path_to_cache_key
) )
entries = self._entries
name_to_entry = self._name_to_entry
added: dict[DashboardEntry, DashboardCacheKeyType] = {} added: dict[DashboardEntry, DashboardCacheKeyType] = {}
updated: dict[DashboardEntry, DashboardCacheKeyType] = {} updated: dict[DashboardEntry, DashboardCacheKeyType] = {}
removed: set[DashboardEntry] = { removed: set[DashboardEntry] = {
entry entry
for filename, entry in self._entries.items() for filename, entry in entries.items()
if filename not in path_to_cache_key if filename not in path_to_cache_key
} }
entries = self._entries original_names: dict[DashboardEntry, str] = {}
for path, cache_key in path_to_cache_key.items(): for path, cache_key in path_to_cache_key.items():
if entry := self._entries.get(path): if not (entry := entries.get(path)):
if entry.cache_key != cache_key:
updated[entry] = cache_key
else:
entry = DashboardEntry(path, cache_key) entry = DashboardEntry(path, cache_key)
added[entry] = cache_key added[entry] = cache_key
continue
if entry.cache_key != cache_key:
updated[entry] = cache_key
original_names[entry] = entry.name
if added or updated: if added or updated:
await self._loop.run_in_executor( await self._loop.run_in_executor(
None, self._load_entries, {**added, **updated} None, self._load_entries, {**added, **updated}
) )
bus = self._dashboard.bus
for entry in added: for entry in added:
_LOGGER.debug("Added dashboard entry %s", entry.path)
entries[entry.path] = entry entries[entry.path] = entry
name_to_entry[entry.name].add(entry)
bus.async_fire(EVENT_ENTRY_ADDED, {"entry": entry})
if entry in removed: for entry in removed:
_LOGGER.debug("Removed dashboard entry %s", entry.path) del entries[entry.path]
entries.pop(entry.path) name_to_entry[entry.name].discard(entry)
bus.async_fire(EVENT_ENTRY_REMOVED, {"entry": entry})
for entry in updated: for entry in updated:
_LOGGER.debug("Updated dashboard entry %s", entry.path) if (original_name := original_names[entry]) != (current_name := entry.name):
# In the future we can fire events when entries are added/removed/updated name_to_entry[original_name].discard(entry)
name_to_entry[current_name].add(entry)
bus.async_fire(EVENT_ENTRY_UPDATED, {"entry": entry})
def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]: def _get_path_to_cache_key(self) -> dict[str, DashboardCacheKeyType]:
"""Return a dict of path to cache key.""" """Return a dict of path to cache key."""
@ -152,29 +242,64 @@ class DashboardEntry:
This class is thread-safe and read-only. This class is thread-safe and read-only.
""" """
__slots__ = ("path", "filename", "_storage_path", "cache_key", "storage") __slots__ = (
"path",
"filename",
"_storage_path",
"cache_key",
"storage",
"state",
"_to_dict",
)
def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None: def __init__(self, path: str, cache_key: DashboardCacheKeyType) -> None:
"""Initialize the DashboardEntry.""" """Initialize the DashboardEntry."""
self.path = path self.path = path
self.filename = os.path.basename(path) self.filename: str = os.path.basename(path)
self._storage_path = ext_storage_path(self.filename) self._storage_path = ext_storage_path(self.filename)
self.cache_key = cache_key self.cache_key = cache_key
self.storage: StorageJSON | None = None self.storage: StorageJSON | None = None
self.state = EntryState.UNKNOWN
self._to_dict: dict[str, Any] | None = None
def __repr__(self): def __repr__(self):
"""Return the representation of this entry.""" """Return the representation of this entry."""
return ( return (
f"DashboardEntry({self.path} " f"DashboardEntry(path={self.path} "
f"address={self.address} " f"address={self.address} "
f"web_port={self.web_port} " f"web_port={self.web_port} "
f"name={self.name} " f"name={self.name} "
f"no_mdns={self.no_mdns})" f"no_mdns={self.no_mdns} "
f"state={self.state} "
")"
) )
def to_dict(self) -> dict[str, Any]:
"""Return a dict representation of this entry.
The dict includes the loaded configuration but not
the current state of the entry.
"""
if self._to_dict is None:
self._to_dict = {
"name": self.name,
"friendly_name": self.friendly_name,
"configuration": self.filename,
"loaded_integrations": sorted(self.loaded_integrations),
"deployed_version": self.update_old,
"current_version": self.update_new,
"path": self.path,
"comment": self.comment,
"address": self.address,
"web_port": self.web_port,
"target_platform": self.target_platform,
}
return self._to_dict
def load_from_disk(self, cache_key: DashboardCacheKeyType | None = None) -> None: def load_from_disk(self, cache_key: DashboardCacheKeyType | None = None) -> None:
"""Load this entry from disk.""" """Load this entry from disk."""
self.storage = StorageJSON.load(self._storage_path) self.storage = StorageJSON.load(self._storage_path)
self._to_dict = None
# #
# Currently StorageJSON.load() will return None if the file does not exist # Currently StorageJSON.load() will return None if the file does not exist
# #
@ -256,7 +381,7 @@ class DashboardEntry:
return const.__version__ return const.__version__
@property @property
def loaded_integrations(self) -> list[str]: def loaded_integrations(self) -> set[str]:
if self.storage is None: if self.storage is None:
return [] return []
return self.storage.loaded_integrations return self.storage.loaded_integrations

19
esphome/dashboard/enum.py Normal file
View file

@ -0,0 +1,19 @@
"""Enum backports from standard lib."""
from __future__ import annotations
from enum import Enum
from typing import Any
class StrEnum(str, Enum):
"""Partial backport of Python 3.11's StrEnum for our basic use cases."""
def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum:
"""Create a new StrEnum instance."""
if not isinstance(value, str):
raise TypeError(f"{value!r} is not a string")
return super().__new__(cls, value, *args, **kwargs)
def __str__(self) -> str:
"""Return self.value."""
return str(self.value)

View file

@ -3,6 +3,7 @@ from __future__ import annotations
import hmac import hmac
import os import os
from pathlib import Path from pathlib import Path
from typing import Any
from esphome.core import CORE from esphome.core import CORE
from esphome.helpers import get_bool_env from esphome.helpers import get_bool_env
@ -69,7 +70,8 @@ class DashboardSettings:
# Compare password in constant running time (to prevent timing attacks) # Compare password in constant running time (to prevent timing attacks)
return hmac.compare_digest(self.password_hash, password_hash(password)) return hmac.compare_digest(self.password_hash, password_hash(password))
def rel_path(self, *args): def rel_path(self, *args: Any) -> str:
"""Return a path relative to the ESPHome config folder."""
joined_path = os.path.join(self.config_dir, *args) joined_path = os.path.join(self.config_dir, *args)
# Raises ValueError if not relative to ESPHome config folder # Raises ValueError if not relative to ESPHome config folder
Path(joined_path).resolve().relative_to(self.absolute_config_dir) Path(joined_path).resolve().relative_to(self.absolute_config_dir)

View file

@ -10,7 +10,9 @@ from esphome.zeroconf import (
DashboardStatus, DashboardStatus,
) )
from ..const import SENTINEL
from ..core import DASHBOARD from ..core import DASHBOARD
from ..entries import DashboardEntry, bool_to_entry_state
class MDNSStatus: class MDNSStatus:
@ -22,17 +24,8 @@ class MDNSStatus:
self.aiozc: AsyncEsphomeZeroconf | None = None self.aiozc: AsyncEsphomeZeroconf | None = None
# This is the current mdns state for each host (True, False, None) # This is the current mdns state for each host (True, False, None)
self.host_mdns_state: dict[str, bool | None] = {} self.host_mdns_state: dict[str, bool | None] = {}
# This is the hostnames to filenames mapping
self.host_name_to_filename: dict[str, str] = {}
self.filename_to_host_name: dict[str, str] = {}
# This is a set of host names to track (i.e no_mdns = false)
self.host_name_with_mdns_enabled: set[set] = set()
self._loop = asyncio.get_running_loop() self._loop = asyncio.get_running_loop()
def filename_to_host_name_thread_safe(self, filename: str) -> str | None:
"""Resolve a filename to an address in a thread-safe manner."""
return self.filename_to_host_name.get(filename)
async def async_resolve_host(self, host_name: str) -> str | None: async def async_resolve_host(self, host_name: str) -> str | None:
"""Resolve a host name to an address in a thread-safe manner.""" """Resolve a host name to an address in a thread-safe manner."""
if aiozc := self.aiozc: if aiozc := self.aiozc:
@ -42,53 +35,47 @@ class MDNSStatus:
async def async_refresh_hosts(self): async def async_refresh_hosts(self):
"""Refresh the hosts to track.""" """Refresh the hosts to track."""
dashboard = DASHBOARD dashboard = DASHBOARD
entries = dashboard.entries.async_all()
host_name_with_mdns_enabled = self.host_name_with_mdns_enabled
host_mdns_state = self.host_mdns_state host_mdns_state = self.host_mdns_state
host_name_to_filename = self.host_name_to_filename entries = dashboard.entries
filename_to_host_name = self.filename_to_host_name poll_names: dict[str, set[DashboardEntry]] = {}
ping_result = dashboard.ping_result for entry in entries.async_all():
for entry in entries:
name = entry.name
# If no_mdns is set, remove it from the set
if entry.no_mdns: if entry.no_mdns:
host_name_with_mdns_enabled.discard(name)
continue continue
# We are tracking this host
host_name_with_mdns_enabled.add(name)
filename = entry.filename
# If we just adopted/imported this host, we likely # If we just adopted/imported this host, we likely
# already have a state for it, so we should make sure # already have a state for it, so we should make sure
# to set it so the dashboard shows it as online # to set it so the dashboard shows it as online
if name in host_mdns_state: if entry.loaded_integrations and "api" not in entry.loaded_integrations:
ping_result[filename] = host_mdns_state[name] # No api available so we have to poll since
# the device won't respond to a request to ._esphomelib._tcp.local.
poll_names.setdefault(entry.name, set()).add(entry)
elif (online := host_mdns_state.get(entry.name, SENTINEL)) != SENTINEL:
entries.async_set_state(entry, bool_to_entry_state(online))
# Make sure the mapping is up to date if poll_names and self.aiozc:
# so when we get an mdns update we can map it back results = await asyncio.gather(
# to the filename *(self.aiozc.async_resolve_host(name) for name in poll_names)
host_name_to_filename[name] = filename )
filename_to_host_name[filename] = name for name, address in zip(poll_names, results):
result = bool(address)
host_mdns_state[name] = result
for entry in poll_names[name]:
entries.async_set_state(entry, bool_to_entry_state(result))
async def async_run(self) -> None: async def async_run(self) -> None:
dashboard = DASHBOARD dashboard = DASHBOARD
entries = dashboard.entries
aiozc = AsyncEsphomeZeroconf() aiozc = AsyncEsphomeZeroconf()
self.aiozc = aiozc self.aiozc = aiozc
host_mdns_state = self.host_mdns_state host_mdns_state = self.host_mdns_state
host_name_to_filename = self.host_name_to_filename
host_name_with_mdns_enabled = self.host_name_with_mdns_enabled
ping_result = dashboard.ping_result
def on_update(dat: dict[str, bool | None]) -> None: def on_update(dat: dict[str, bool | None]) -> None:
"""Update the global PING_RESULT dict.""" """Update the entry state."""
for name, result in dat.items(): for name, result in dat.items():
host_mdns_state[name] = result host_mdns_state[name] = result
if name in host_name_with_mdns_enabled: if matching_entries := entries.get_by_name(name):
filename = host_name_to_filename[name] for entry in matching_entries:
ping_result[filename] = result if not entry.no_mdns:
entries.async_set_state(entry, bool_to_entry_state(result))
stat = DashboardStatus(on_update) stat = DashboardStatus(on_update)
imports = DashboardImportDiscovery() imports = DashboardImportDiscovery()
@ -100,10 +87,11 @@ class MDNSStatus:
[stat.browser_callback, imports.browser_callback], [stat.browser_callback, imports.browser_callback],
) )
ping_request = dashboard.ping_request
while not dashboard.stop_event.is_set(): while not dashboard.stop_event.is_set():
await self.async_refresh_hosts() await self.async_refresh_hosts()
await dashboard.ping_request.wait() await ping_request.wait()
dashboard.ping_request.clear() ping_request.clear()
await browser.async_cancel() await browser.async_cancel()
await aiozc.async_close() await aiozc.async_close()

View file

@ -8,6 +8,7 @@ import threading
from esphome import mqtt from esphome import mqtt
from ..core import DASHBOARD from ..core import DASHBOARD
from ..entries import EntryState
class MqttStatusThread(threading.Thread): class MqttStatusThread(threading.Thread):
@ -16,22 +17,23 @@ class MqttStatusThread(threading.Thread):
def run(self) -> None: def run(self) -> None:
"""Run the status thread.""" """Run the status thread."""
dashboard = DASHBOARD dashboard = DASHBOARD
entries = dashboard.entries.all() entries = dashboard.entries
current_entries = entries.all()
config = mqtt.config_from_env() config = mqtt.config_from_env()
topic = "esphome/discover/#" topic = "esphome/discover/#"
def on_message(client, userdata, msg): def on_message(client, userdata, msg):
nonlocal entries nonlocal current_entries
payload = msg.payload.decode(errors="backslashreplace") payload = msg.payload.decode(errors="backslashreplace")
if len(payload) > 0: if len(payload) > 0:
data = json.loads(payload) data = json.loads(payload)
if "name" not in data: if "name" not in data:
return return
for entry in entries: for entry in current_entries:
if entry.name == data["name"]: if entry.name == data["name"]:
dashboard.ping_result[entry.filename] = True entries.set_state(entry, EntryState.ONLINE)
return return
def on_connect(client, userdata, flags, return_code): def on_connect(client, userdata, flags, return_code):
@ -51,12 +53,11 @@ class MqttStatusThread(threading.Thread):
client.loop_start() client.loop_start()
while not dashboard.stop_event.wait(2): while not dashboard.stop_event.wait(2):
entries = dashboard.entries.all() current_entries = entries.all()
# will be set to true on on_message # will be set to true on on_message
for entry in entries: for entry in current_entries:
if entry.no_mdns: if entry.no_mdns:
dashboard.ping_result[entry.filename] = False entries.set_state(entry, EntryState.OFFLINE)
client.publish("esphome/discover", None, retain=False) client.publish("esphome/discover", None, retain=False)
dashboard.mqtt_ping_request.wait() dashboard.mqtt_ping_request.wait()

View file

@ -5,7 +5,7 @@ import os
from typing import cast from typing import cast
from ..core import DASHBOARD from ..core import DASHBOARD
from ..entries import DashboardEntry from ..entries import DashboardEntry, bool_to_entry_state
from ..util.itertools import chunked from ..util.itertools import chunked
from ..util.subprocess import async_system_command_status from ..util.subprocess import async_system_command_status
@ -26,14 +26,14 @@ class PingStatus:
async def async_run(self) -> None: async def async_run(self) -> None:
"""Run the ping status.""" """Run the ping status."""
dashboard = DASHBOARD dashboard = DASHBOARD
entries = dashboard.entries
while not dashboard.stop_event.is_set(): while not dashboard.stop_event.is_set():
# Only ping if the dashboard is open # Only ping if the dashboard is open
await dashboard.ping_request.wait() await dashboard.ping_request.wait()
dashboard.ping_result.clear() current_entries = dashboard.entries.async_all()
entries = dashboard.entries.async_all()
to_ping: list[DashboardEntry] = [ to_ping: list[DashboardEntry] = [
entry for entry in entries if entry.address is not None entry for entry in current_entries if entry.address is not None
] ]
for ping_group in chunked(to_ping, 16): for ping_group in chunked(to_ping, 16):
ping_group = cast(list[DashboardEntry], ping_group) ping_group = cast(list[DashboardEntry], ping_group)
@ -46,4 +46,4 @@ class PingStatus:
result = False result = False
elif isinstance(result, BaseException): elif isinstance(result, BaseException):
raise result raise result
dashboard.ping_result[entry.filename] = result entries.async_set_state(entry, bool_to_entry_state(result))

View file

@ -0,0 +1,55 @@
import logging
import os
import tempfile
from pathlib import Path
_LOGGER = logging.getLogger(__name__)
def write_utf8_file(
filename: Path,
utf8_str: str,
private: bool = False,
) -> None:
"""Write a file and rename it into place.
Writes all or nothing.
"""
write_file(filename, utf8_str.encode("utf-8"), private)
# from https://github.com/home-assistant/core/blob/dev/homeassistant/util/file.py
def write_file(
filename: Path,
utf8_data: bytes,
private: bool = False,
) -> None:
"""Write a file and rename it into place.
Writes all or nothing.
"""
tmp_filename = ""
try:
# Modern versions of Python tempfile create this file with mode 0o600
with tempfile.NamedTemporaryFile(
mode="wb", dir=os.path.dirname(filename), delete=False
) as fdesc:
fdesc.write(utf8_data)
tmp_filename = fdesc.name
if not private:
os.fchmod(fdesc.fileno(), 0o644)
os.replace(tmp_filename, filename)
finally:
if os.path.exists(tmp_filename):
try:
os.remove(tmp_filename)
except OSError as err:
# If we are cleaning up then something else went wrong, so
# we should suppress likely follow-on errors in the cleanup
_LOGGER.error(
"File replacement cleanup failed for %s while saving %s: %s",
tmp_filename,
filename,
err,
)

View file

@ -37,6 +37,8 @@ from esphome.util import get_serial_ports, shlex_quote
from esphome.yaml_util import FastestAvailableSafeLoader from esphome.yaml_util import FastestAvailableSafeLoader
from .core import DASHBOARD from .core import DASHBOARD
from .entries import EntryState, entry_state_to_bool
from .util.file import write_file
from .util.subprocess import async_run_system_command from .util.subprocess import async_run_system_command
from .util.text import friendly_name_slugify from .util.text import friendly_name_slugify
@ -269,14 +271,15 @@ class EsphomePortCommandWebSocket(EsphomeCommandWebSocket):
) -> list[str]: ) -> list[str]:
"""Build the command to run.""" """Build the command to run."""
dashboard = DASHBOARD dashboard = DASHBOARD
entries = dashboard.entries
configuration = json_message["configuration"] configuration = json_message["configuration"]
config_file = settings.rel_path(configuration) config_file = settings.rel_path(configuration)
port = json_message["port"] port = json_message["port"]
if ( if (
port == "OTA" port == "OTA"
and (mdns := dashboard.mdns_status) and (mdns := dashboard.mdns_status)
and (host_name := mdns.filename_to_host_name_thread_safe(configuration)) and (entry := entries.get(config_file))
and (address := await mdns.async_resolve_host(host_name)) and (address := await mdns.async_resolve_host(entry.name))
): ):
port = address port = address
@ -315,7 +318,9 @@ class EsphomeRenameHandler(EsphomeCommandWebSocket):
return return
# Remove the old ping result from the cache # Remove the old ping result from the cache
DASHBOARD.ping_result.pop(self.old_name, None) entries = DASHBOARD.entries
if entry := entries.get(self.old_name):
entries.async_set_state(entry, EntryState.UNKNOWN)
class EsphomeUploadHandler(EsphomePortCommandWebSocket): class EsphomeUploadHandler(EsphomePortCommandWebSocket):
@ -521,9 +526,19 @@ class DownloadListRequestHandler(BaseHandler):
class DownloadBinaryRequestHandler(BaseHandler): class DownloadBinaryRequestHandler(BaseHandler):
def _load_file(self, path: str, compressed: bool) -> bytes:
"""Load a file from disk and compress it if requested."""
with open(path, "rb") as f:
data = f.read()
if compressed:
return gzip.compress(data, 9)
return data
@authenticated @authenticated
@bind_config @bind_config
async def get(self, configuration=None): async def get(self, configuration: str | None = None):
"""Download a binary file."""
loop = asyncio.get_running_loop()
compressed = self.get_argument("compressed", "0") == "1" compressed = self.get_argument("compressed", "0") == "1"
storage_path = ext_storage_path(configuration) storage_path = ext_storage_path(configuration)
@ -580,11 +595,8 @@ class DownloadBinaryRequestHandler(BaseHandler):
self.send_error(404) self.send_error(404)
return return
with open(path, "rb") as f: data = await loop.run_in_executor(None, self._load_file, path, compressed)
data = f.read() self.write(data)
if compressed:
data = gzip.compress(data, 9)
self.write(data)
self.finish() self.finish()
@ -609,22 +621,7 @@ class ListDevicesHandler(BaseHandler):
self.write( self.write(
json.dumps( json.dumps(
{ {
"configured": [ "configured": [entry.to_dict() for entry in entries],
{
"name": entry.name,
"friendly_name": entry.friendly_name,
"configuration": entry.filename,
"loaded_integrations": entry.loaded_integrations,
"deployed_version": entry.update_old,
"current_version": entry.update_new,
"path": entry.path,
"comment": entry.comment,
"address": entry.address,
"web_port": entry.web_port,
"target_platform": entry.target_platform,
}
for entry in entries
],
"importable": [ "importable": [
{ {
"name": res.device_name, "name": res.device_name,
@ -728,7 +725,15 @@ class PingRequestHandler(BaseHandler):
if settings.status_use_mqtt: if settings.status_use_mqtt:
dashboard.mqtt_ping_request.set() dashboard.mqtt_ping_request.set()
self.set_header("content-type", "application/json") self.set_header("content-type", "application/json")
self.write(json.dumps(dashboard.ping_result))
self.write(
json.dumps(
{
entry.filename: entry_state_to_bool(entry.state)
for entry in dashboard.entries.async_all()
}
)
)
class InfoRequestHandler(BaseHandler): class InfoRequestHandler(BaseHandler):
@ -750,19 +755,35 @@ class InfoRequestHandler(BaseHandler):
class EditRequestHandler(BaseHandler): class EditRequestHandler(BaseHandler):
@authenticated @authenticated
@bind_config @bind_config
def get(self, configuration=None): async def get(self, configuration: str | None = None):
"""Get the content of a file."""
loop = asyncio.get_running_loop()
filename = settings.rel_path(configuration) filename = settings.rel_path(configuration)
content = "" content = await loop.run_in_executor(None, self._read_file, filename)
if os.path.isfile(filename):
with open(file=filename, encoding="utf-8") as f:
content = f.read()
self.write(content) self.write(content)
def _read_file(self, filename: str) -> bytes:
"""Read a file and return the content as bytes."""
with open(file=filename, encoding="utf-8") as f:
return f.read()
def _write_file(self, filename: str, content: bytes) -> None:
"""Write a file with the given content."""
write_file(filename, content)
@authenticated @authenticated
@bind_config @bind_config
def post(self, configuration=None): async def post(self, configuration: str | None = None):
with open(file=settings.rel_path(configuration), mode="wb") as f: """Write the content of a file."""
f.write(self.request.body) loop = asyncio.get_running_loop()
config_file = settings.rel_path(configuration)
await loop.run_in_executor(
None, self._write_file, config_file, self.request.body
)
# Ensure the StorageJSON is updated as well
await async_run_system_command(
[*DASHBOARD_COMMAND, "compile", "--only-generate", config_file]
)
self.set_status(200) self.set_status(200)
@ -785,9 +806,6 @@ class DeleteRequestHandler(BaseHandler):
if build_folder is not None: if build_folder is not None:
shutil.rmtree(build_folder, os.path.join(trash_path, name)) shutil.rmtree(build_folder, os.path.join(trash_path, name))
# Remove the old ping result from the cache
DASHBOARD.ping_result.pop(configuration, None)
class UndoDeleteRequestHandler(BaseHandler): class UndoDeleteRequestHandler(BaseHandler):
@authenticated @authenticated

View file

@ -1,21 +1,15 @@
from __future__ import annotations
import binascii import binascii
import codecs import codecs
from datetime import datetime
import json import json
import logging import logging
import os import os
from typing import Optional from datetime import datetime
from esphome import const from esphome import const
from esphome.const import CONF_DISABLED, CONF_MDNS
from esphome.core import CORE from esphome.core import CORE
from esphome.helpers import write_file_if_changed from esphome.helpers import write_file_if_changed
from esphome.const import (
CONF_MDNS,
CONF_DISABLED,
)
from esphome.types import CoreType from esphome.types import CoreType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,48 +34,47 @@ def trash_storage_path() -> str:
class StorageJSON: class StorageJSON:
def __init__( def __init__(
self, self,
storage_version, storage_version: int,
name, name: str,
friendly_name, friendly_name: str,
comment, comment: str,
esphome_version, esphome_version: str,
src_version, src_version: int | None,
address, address: str,
web_port, web_port: int | None,
target_platform, target_platform: str,
build_path, build_path: str,
firmware_bin_path, firmware_bin_path: str,
loaded_integrations, loaded_integrations: set[str],
no_mdns, no_mdns: bool,
): ) -> None:
# Version of the storage JSON schema # Version of the storage JSON schema
assert storage_version is None or isinstance(storage_version, int) assert storage_version is None or isinstance(storage_version, int)
self.storage_version: int = storage_version self.storage_version = storage_version
# The name of the node # The name of the node
self.name: str = name self.name = name
# The friendly name of the node # The friendly name of the node
self.friendly_name: str = friendly_name self.friendly_name = friendly_name
# The comment of the node # The comment of the node
self.comment: str = comment self.comment = comment
# The esphome version this was compiled with # The esphome version this was compiled with
self.esphome_version: str = esphome_version self.esphome_version = esphome_version
# The version of the file in src/main.cpp - Used to migrate the file # The version of the file in src/main.cpp - Used to migrate the file
assert src_version is None or isinstance(src_version, int) assert src_version is None or isinstance(src_version, int)
self.src_version: int = src_version self.src_version = src_version
# Address of the ESP, for example livingroom.local or a static IP # Address of the ESP, for example livingroom.local or a static IP
self.address: str = address self.address = address
# Web server port of the ESP, for example 80 # Web server port of the ESP, for example 80
assert web_port is None or isinstance(web_port, int) assert web_port is None or isinstance(web_port, int)
self.web_port: int = web_port self.web_port = web_port
# The type of hardware in use, like "ESP32", "ESP32C3", "ESP8266", etc. # The type of hardware in use, like "ESP32", "ESP32C3", "ESP8266", etc.
self.target_platform: str = target_platform self.target_platform = target_platform
# The absolute path to the platformio project # The absolute path to the platformio project
self.build_path: str = build_path self.build_path = build_path
# The absolute path to the firmware binary # The absolute path to the firmware binary
self.firmware_bin_path: str = firmware_bin_path self.firmware_bin_path = firmware_bin_path
# A list of strings of names of loaded integrations # A set of strings of names of loaded integrations
self.loaded_integrations: list[str] = loaded_integrations self.loaded_integrations = loaded_integrations
self.loaded_integrations.sort()
# Is mDNS disabled # Is mDNS disabled
self.no_mdns = no_mdns self.no_mdns = no_mdns
@ -98,7 +91,7 @@ class StorageJSON:
"esp_platform": self.target_platform, "esp_platform": self.target_platform,
"build_path": self.build_path, "build_path": self.build_path,
"firmware_bin_path": self.firmware_bin_path, "firmware_bin_path": self.firmware_bin_path,
"loaded_integrations": self.loaded_integrations, "loaded_integrations": sorted(self.loaded_integrations),
"no_mdns": self.no_mdns, "no_mdns": self.no_mdns,
} }
@ -109,9 +102,7 @@ class StorageJSON:
write_file_if_changed(path, self.to_json()) write_file_if_changed(path, self.to_json())
@staticmethod @staticmethod
def from_esphome_core( def from_esphome_core(esph: CoreType, old: StorageJSON | None) -> StorageJSON:
esph: CoreType, old: Optional["StorageJSON"]
) -> "StorageJSON":
hardware = esph.target_platform.upper() hardware = esph.target_platform.upper()
if esph.is_esp32: if esph.is_esp32:
from esphome.components import esp32 from esphome.components import esp32
@ -129,7 +120,7 @@ class StorageJSON:
target_platform=hardware, target_platform=hardware,
build_path=esph.build_path, build_path=esph.build_path,
firmware_bin_path=esph.firmware_bin, firmware_bin_path=esph.firmware_bin,
loaded_integrations=list(esph.loaded_integrations), loaded_integrations=esph.loaded_integrations,
no_mdns=( no_mdns=(
CONF_MDNS in esph.config CONF_MDNS in esph.config
and CONF_DISABLED in esph.config[CONF_MDNS] and CONF_DISABLED in esph.config[CONF_MDNS]
@ -140,7 +131,7 @@ class StorageJSON:
@staticmethod @staticmethod
def from_wizard( def from_wizard(
name: str, friendly_name: str, address: str, platform: str name: str, friendly_name: str, address: str, platform: str
) -> "StorageJSON": ) -> StorageJSON:
return StorageJSON( return StorageJSON(
storage_version=1, storage_version=1,
name=name, name=name,
@ -153,12 +144,12 @@ class StorageJSON:
target_platform=platform, target_platform=platform,
build_path=None, build_path=None,
firmware_bin_path=None, firmware_bin_path=None,
loaded_integrations=[], loaded_integrations=set(),
no_mdns=False, no_mdns=False,
) )
@staticmethod @staticmethod
def _load_impl(path: str) -> Optional["StorageJSON"]: def _load_impl(path: str) -> StorageJSON | None:
with codecs.open(path, "r", encoding="utf-8") as f_handle: with codecs.open(path, "r", encoding="utf-8") as f_handle:
storage = json.load(f_handle) storage = json.load(f_handle)
storage_version = storage["storage_version"] storage_version = storage["storage_version"]
@ -174,7 +165,7 @@ class StorageJSON:
esp_platform = storage.get("esp_platform") esp_platform = storage.get("esp_platform")
build_path = storage.get("build_path") build_path = storage.get("build_path")
firmware_bin_path = storage.get("firmware_bin_path") firmware_bin_path = storage.get("firmware_bin_path")
loaded_integrations = storage.get("loaded_integrations", []) loaded_integrations = set(storage.get("loaded_integrations", []))
no_mdns = storage.get("no_mdns", False) no_mdns = storage.get("no_mdns", False)
return StorageJSON( return StorageJSON(
storage_version, storage_version,
@ -193,7 +184,7 @@ class StorageJSON:
) )
@staticmethod @staticmethod
def load(path: str) -> Optional["StorageJSON"]: def load(path: str) -> StorageJSON | None:
try: try:
return StorageJSON._load_impl(path) return StorageJSON._load_impl(path)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
@ -215,7 +206,7 @@ class EsphomeStorageJSON:
# The last time ESPHome checked for an update as an isoformat encoded str # The last time ESPHome checked for an update as an isoformat encoded str
self.last_update_check_str: str = last_update_check self.last_update_check_str: str = last_update_check
# Cache of the version gotten in the last version check # Cache of the version gotten in the last version check
self.remote_version: Optional[str] = remote_version self.remote_version: str | None = remote_version
def as_dict(self) -> dict: def as_dict(self) -> dict:
return { return {
@ -226,7 +217,7 @@ class EsphomeStorageJSON:
} }
@property @property
def last_update_check(self) -> Optional[datetime]: def last_update_check(self) -> datetime | None:
try: try:
return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S") return datetime.strptime(self.last_update_check_str, "%Y-%m-%dT%H:%M:%S")
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
@ -243,7 +234,7 @@ class EsphomeStorageJSON:
write_file_if_changed(path, self.to_json()) write_file_if_changed(path, self.to_json())
@staticmethod @staticmethod
def _load_impl(path: str) -> Optional["EsphomeStorageJSON"]: def _load_impl(path: str) -> EsphomeStorageJSON | None:
with codecs.open(path, "r", encoding="utf-8") as f_handle: with codecs.open(path, "r", encoding="utf-8") as f_handle:
storage = json.load(f_handle) storage = json.load(f_handle)
storage_version = storage["storage_version"] storage_version = storage["storage_version"]
@ -255,14 +246,14 @@ class EsphomeStorageJSON:
) )
@staticmethod @staticmethod
def load(path: str) -> Optional["EsphomeStorageJSON"]: def load(path: str) -> EsphomeStorageJSON | None:
try: try:
return EsphomeStorageJSON._load_impl(path) return EsphomeStorageJSON._load_impl(path)
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
return None return None
@staticmethod @staticmethod
def get_default() -> "EsphomeStorageJSON": def get_default() -> EsphomeStorageJSON:
return EsphomeStorageJSON( return EsphomeStorageJSON(
storage_version=1, storage_version=1,
cookie_secret=binascii.hexlify(os.urandom(64)).decode(), cookie_secret=binascii.hexlify(os.urandom(64)).decode(),

View file

@ -169,7 +169,9 @@ class DashboardImportDiscovery:
def _make_host_resolver(host: str) -> HostResolver: def _make_host_resolver(host: str) -> HostResolver:
"""Create a new HostResolver for the given host name.""" """Create a new HostResolver for the given host name."""
name = host.partition(".")[0] name = host.partition(".")[0]
info = HostResolver(ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}") info = HostResolver(
ESPHOME_SERVICE_TYPE, f"{name}.{ESPHOME_SERVICE_TYPE}", server=f"{name}.local."
)
return info return info

View file

@ -39,7 +39,7 @@ lib_deps =
bblanchon/ArduinoJson@6.18.5 ; json bblanchon/ArduinoJson@6.18.5 ; json
wjtje/qr-code-generator-library@1.7.0 ; qr_code wjtje/qr-code-generator-library@1.7.0 ; qr_code
functionpointer/arduino-MLX90393@1.0.0 ; mlx90393 functionpointer/arduino-MLX90393@1.0.0 ; mlx90393
pavlodn/HaierProtocol@0.9.20 ; haier pavlodn/HaierProtocol@0.9.24 ; haier
; This is using the repository until a new release is published to PlatformIO ; This is using the repository until a new release is published to PlatformIO
https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library https://github.com/Sensirion/arduino-gas-index-algorithm.git#3.2.1 ; Sensirion Gas Index Algorithm Arduino Library
build_flags = build_flags =

View file

@ -10,7 +10,7 @@ platformio==6.1.11 # When updating platformio, also update Dockerfile
esptool==4.6.2 esptool==4.6.2
click==8.1.7 click==8.1.7
esphome-dashboard==20231107.0 esphome-dashboard==20231107.0
aioesphomeapi==18.4.1 aioesphomeapi==18.5.5
zeroconf==0.127.0 zeroconf==0.127.0
# esp-idf requires this, but doesn't bundle it by default # esp-idf requires this, but doesn't bundle it by default

View file

View file

View file

@ -0,0 +1,53 @@
import os
from pathlib import Path
from unittest.mock import patch
import py
import pytest
from esphome.dashboard.util.file import write_file, write_utf8_file
def test_write_utf8_file(tmp_path: Path) -> None:
write_utf8_file(tmp_path.joinpath("foo.txt"), "foo")
assert tmp_path.joinpath("foo.txt").read_text() == "foo"
with pytest.raises(OSError):
write_utf8_file(Path("/not-writable"), "bar")
def test_write_file(tmp_path: Path) -> None:
write_file(tmp_path.joinpath("foo.txt"), b"foo")
assert tmp_path.joinpath("foo.txt").read_text() == "foo"
def test_write_utf8_file_fails_at_rename(
tmpdir: py.path.local, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that if rename fails not not remove, we do not log the failed cleanup."""
test_dir = tmpdir.mkdir("files")
test_file = Path(test_dir / "test.json")
with pytest.raises(OSError), patch(
"esphome.dashboard.util.file.os.replace", side_effect=OSError
):
write_utf8_file(test_file, '{"some":"data"}', False)
assert not os.path.exists(test_file)
assert "File replacement cleanup failed" not in caplog.text
def test_write_utf8_file_fails_at_rename_and_remove(
tmpdir: py.path.local, caplog: pytest.LogCaptureFixture
) -> None:
"""Test that if rename and remove both fail, we log the failed cleanup."""
test_dir = tmpdir.mkdir("files")
test_file = Path(test_dir / "test.json")
with pytest.raises(OSError), patch(
"esphome.dashboard.util.file.os.remove", side_effect=OSError
), patch("esphome.dashboard.util.file.os.replace", side_effect=OSError):
write_utf8_file(test_file, '{"some":"data"}', False)
assert "File replacement cleanup failed" in caplog.text