mirror of
https://github.com/esphome/esphome.git
synced 2024-11-21 22:48:10 +01:00
Upgraded Haier climate component implementation (#4521)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Co-authored-by: Pavlo Dudnytskyi <pdudnytskyi@astrata.eu> Co-authored-by: esphomebot <esphome@nabucasa.com>
This commit is contained in:
parent
d4099d68a7
commit
5a8e93ed0a
18 changed files with 2758 additions and 365 deletions
|
@ -102,7 +102,7 @@ esphome/components/gpio/* @esphome/core
|
||||||
esphome/components/gps/* @coogle
|
esphome/components/gps/* @coogle
|
||||||
esphome/components/graph/* @synco
|
esphome/components/graph/* @synco
|
||||||
esphome/components/growatt_solar/* @leeuwte
|
esphome/components/growatt_solar/* @leeuwte
|
||||||
esphome/components/haier/* @Yarikx
|
esphome/components/haier/* @paveldn
|
||||||
esphome/components/havells_solar/* @sourabhjaiswal
|
esphome/components/havells_solar/* @sourabhjaiswal
|
||||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||||
esphome/components/hbridge/light/* @DotNetDann
|
esphome/components/hbridge/light/* @DotNetDann
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
CODEOWNERS = ["@Yarikx"]
|
|
130
esphome/components/haier/automation.h
Normal file
130
esphome/components/haier/automation.h
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/automation.h"
|
||||||
|
#include "haier_base.h"
|
||||||
|
#include "hon_climate.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
template<typename... Ts> class DisplayOnAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
DisplayOnAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->set_display_state(true); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HaierClimateBase *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class DisplayOffAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
DisplayOffAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->set_display_state(false); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HaierClimateBase *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class BeeperOnAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
BeeperOnAction(HonClimate *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->set_beeper_state(true); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HonClimate *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class BeeperOffAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
BeeperOffAction(HonClimate *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->set_beeper_state(false); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HonClimate *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class VerticalAirflowAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
VerticalAirflowAction(HonClimate *parent) : parent_(parent) {}
|
||||||
|
TEMPLATABLE_VALUE(AirflowVerticalDirection, direction)
|
||||||
|
void play(Ts... x) { this->parent_->set_vertical_airflow(this->direction_.value(x...)); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HonClimate *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class HorizontalAirflowAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
HorizontalAirflowAction(HonClimate *parent) : parent_(parent) {}
|
||||||
|
TEMPLATABLE_VALUE(AirflowHorizontalDirection, direction)
|
||||||
|
void play(Ts... x) { this->parent_->set_horizontal_airflow(this->direction_.value(x...)); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HonClimate *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class HealthOnAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
HealthOnAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->set_health_mode(true); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HaierClimateBase *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class HealthOffAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
HealthOffAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->set_health_mode(false); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HaierClimateBase *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class StartSelfCleaningAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
StartSelfCleaningAction(HonClimate *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->start_self_cleaning(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HonClimate *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class StartSteriCleaningAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
StartSteriCleaningAction(HonClimate *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->start_steri_cleaning(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HonClimate *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class PowerOnAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
PowerOnAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->send_power_on_command(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HaierClimateBase *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class PowerOffAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
PowerOffAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->send_power_off_command(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HaierClimateBase *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Ts> class PowerToggleAction : public Action<Ts...> {
|
||||||
|
public:
|
||||||
|
PowerToggleAction(HaierClimateBase *parent) : parent_(parent) {}
|
||||||
|
void play(Ts... x) { this->parent_->toggle_power(); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
HaierClimateBase *parent_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
|
@ -1,43 +1,364 @@
|
||||||
from esphome.components import climate
|
import logging
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.components import uart
|
import esphome.final_validate as fv
|
||||||
from esphome.components.climate import ClimateSwingMode
|
from esphome.components import uart, sensor, climate, logger
|
||||||
from esphome.const import CONF_ID, CONF_SUPPORTED_SWING_MODES
|
from esphome import automation
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_BEEPER,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_LEVEL,
|
||||||
|
CONF_LOGGER,
|
||||||
|
CONF_LOGS,
|
||||||
|
CONF_MAX_TEMPERATURE,
|
||||||
|
CONF_MIN_TEMPERATURE,
|
||||||
|
CONF_PROTOCOL,
|
||||||
|
CONF_SUPPORTED_MODES,
|
||||||
|
CONF_SUPPORTED_SWING_MODES,
|
||||||
|
CONF_VISUAL,
|
||||||
|
CONF_WIFI,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
ICON_THERMOMETER,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
)
|
||||||
|
from esphome.components.climate import (
|
||||||
|
ClimateSwingMode,
|
||||||
|
ClimateMode,
|
||||||
|
)
|
||||||
|
|
||||||
DEPENDENCIES = ["uart"]
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
PROTOCOL_MIN_TEMPERATURE = 16.0
|
||||||
|
PROTOCOL_MAX_TEMPERATURE = 30.0
|
||||||
|
PROTOCOL_TEMPERATURE_STEP = 1.0
|
||||||
|
|
||||||
|
CODEOWNERS = ["@paveldn"]
|
||||||
|
AUTO_LOAD = ["sensor"]
|
||||||
|
DEPENDENCIES = ["climate", "uart"]
|
||||||
|
CONF_WIFI_SIGNAL = "wifi_signal"
|
||||||
|
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
|
||||||
|
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
|
||||||
|
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
|
||||||
|
|
||||||
|
|
||||||
|
PROTOCOL_HON = "HON"
|
||||||
|
PROTOCOL_SMARTAIR2 = "SMARTAIR2"
|
||||||
|
PROTOCOLS_SUPPORTED = [PROTOCOL_HON, PROTOCOL_SMARTAIR2]
|
||||||
|
|
||||||
haier_ns = cg.esphome_ns.namespace("haier")
|
haier_ns = cg.esphome_ns.namespace("haier")
|
||||||
HaierClimate = haier_ns.class_(
|
HaierClimateBase = haier_ns.class_(
|
||||||
"HaierClimate", climate.Climate, cg.PollingComponent, uart.UARTDevice
|
"HaierClimateBase", uart.UARTDevice, climate.Climate, cg.Component
|
||||||
)
|
)
|
||||||
|
HonClimate = haier_ns.class_("HonClimate", HaierClimateBase)
|
||||||
|
Smartair2Climate = haier_ns.class_("Smartair2Climate", HaierClimateBase)
|
||||||
|
|
||||||
ALLOWED_CLIMATE_SWING_MODES = {
|
|
||||||
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
|
AirflowVerticalDirection = haier_ns.enum("AirflowVerticalDirection")
|
||||||
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
|
AIRFLOW_VERTICAL_DIRECTION_OPTIONS = {
|
||||||
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
|
"UP": AirflowVerticalDirection.UP,
|
||||||
|
"CENTER": AirflowVerticalDirection.CENTER,
|
||||||
|
"DOWN": AirflowVerticalDirection.DOWN,
|
||||||
}
|
}
|
||||||
|
|
||||||
validate_swing_modes = cv.enum(ALLOWED_CLIMATE_SWING_MODES, upper=True)
|
AirflowHorizontalDirection = haier_ns.enum("AirflowHorizontalDirection")
|
||||||
|
AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
|
||||||
|
"LEFT": AirflowHorizontalDirection.LEFT,
|
||||||
|
"CENTER": AirflowHorizontalDirection.CENTER,
|
||||||
|
"RIGHT": AirflowHorizontalDirection.RIGHT,
|
||||||
|
}
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.All(
|
SUPPORTED_SWING_MODES_OPTIONS = {
|
||||||
|
"OFF": ClimateSwingMode.CLIMATE_SWING_OFF, # always available
|
||||||
|
"VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL, # always available
|
||||||
|
"HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
|
||||||
|
"BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
|
||||||
|
}
|
||||||
|
|
||||||
|
SUPPORTED_CLIMATE_MODES_OPTIONS = {
|
||||||
|
"OFF": ClimateMode.CLIMATE_MODE_OFF, # always available
|
||||||
|
"AUTO": ClimateMode.CLIMATE_MODE_AUTO, # always available
|
||||||
|
"COOL": ClimateMode.CLIMATE_MODE_COOL,
|
||||||
|
"HEAT": ClimateMode.CLIMATE_MODE_HEAT,
|
||||||
|
"DRY": ClimateMode.CLIMATE_MODE_DRY,
|
||||||
|
"FAN_ONLY": ClimateMode.CLIMATE_MODE_FAN_ONLY,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_visual(config):
|
||||||
|
if CONF_VISUAL in config:
|
||||||
|
visual_config = config[CONF_VISUAL]
|
||||||
|
if CONF_MIN_TEMPERATURE in visual_config:
|
||||||
|
min_temp = visual_config[CONF_MIN_TEMPERATURE]
|
||||||
|
if min_temp < PROTOCOL_MIN_TEMPERATURE:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Configured visual minimum temperature {min_temp} is lower than supported by Haier protocol is {PROTOCOL_MIN_TEMPERATURE}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
config[CONF_VISUAL][CONF_MIN_TEMPERATURE] = PROTOCOL_MIN_TEMPERATURE
|
||||||
|
if CONF_MAX_TEMPERATURE in visual_config:
|
||||||
|
max_temp = visual_config[CONF_MAX_TEMPERATURE]
|
||||||
|
if max_temp > PROTOCOL_MAX_TEMPERATURE:
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"Configured visual maximum temperature {max_temp} is higher than supported by Haier protocol is {PROTOCOL_MAX_TEMPERATURE}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
config[CONF_VISUAL][CONF_MAX_TEMPERATURE] = PROTOCOL_MAX_TEMPERATURE
|
||||||
|
else:
|
||||||
|
config[CONF_VISUAL] = {
|
||||||
|
CONF_MIN_TEMPERATURE: PROTOCOL_MIN_TEMPERATURE,
|
||||||
|
CONF_MAX_TEMPERATURE: PROTOCOL_MAX_TEMPERATURE,
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
BASE_CONFIG_SCHEMA = (
|
||||||
climate.CLIMATE_SCHEMA.extend(
|
climate.CLIMATE_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.declare_id(HaierClimate),
|
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
|
||||||
cv.Optional(CONF_SUPPORTED_SWING_MODES): cv.ensure_list(
|
cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)
|
||||||
validate_swing_modes
|
|
||||||
),
|
),
|
||||||
|
cv.Optional(
|
||||||
|
CONF_SUPPORTED_SWING_MODES,
|
||||||
|
default=[
|
||||||
|
"OFF",
|
||||||
|
"VERTICAL",
|
||||||
|
"HORIZONTAL",
|
||||||
|
"BOTH",
|
||||||
|
],
|
||||||
|
): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.extend(cv.polling_component_schema("5s"))
|
.extend(uart.UART_DEVICE_SCHEMA)
|
||||||
.extend(uart.UART_DEVICE_SCHEMA),
|
.extend(cv.COMPONENT_SCHEMA)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.typed_schema(
|
||||||
|
{
|
||||||
|
PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(Smartair2Climate),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(HonClimate),
|
||||||
|
cv.Optional(CONF_WIFI_SIGNAL, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_BEEPER, default=True): cv.boolean,
|
||||||
|
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
|
icon=ICON_THERMOMETER,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
key=CONF_PROTOCOL,
|
||||||
|
default_type=PROTOCOL_SMARTAIR2,
|
||||||
|
upper=True,
|
||||||
|
),
|
||||||
|
validate_visual,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Actions
|
||||||
|
DisplayOnAction = haier_ns.class_("DisplayOnAction", automation.Action)
|
||||||
|
DisplayOffAction = haier_ns.class_("DisplayOffAction", automation.Action)
|
||||||
|
BeeperOnAction = haier_ns.class_("BeeperOnAction", automation.Action)
|
||||||
|
BeeperOffAction = haier_ns.class_("BeeperOffAction", automation.Action)
|
||||||
|
StartSelfCleaningAction = haier_ns.class_("StartSelfCleaningAction", automation.Action)
|
||||||
|
StartSteriCleaningAction = haier_ns.class_(
|
||||||
|
"StartSteriCleaningAction", automation.Action
|
||||||
|
)
|
||||||
|
VerticalAirflowAction = haier_ns.class_("VerticalAirflowAction", automation.Action)
|
||||||
|
HorizontalAirflowAction = haier_ns.class_("HorizontalAirflowAction", automation.Action)
|
||||||
|
HealthOnAction = haier_ns.class_("HealthOnAction", automation.Action)
|
||||||
|
HealthOffAction = haier_ns.class_("HealthOffAction", automation.Action)
|
||||||
|
PowerOnAction = haier_ns.class_("PowerOnAction", automation.Action)
|
||||||
|
PowerOffAction = haier_ns.class_("PowerOffAction", automation.Action)
|
||||||
|
PowerToggleAction = haier_ns.class_("PowerToggleAction", automation.Action)
|
||||||
|
|
||||||
|
HAIER_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(HaierClimateBase),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
HAIER_HON_BASE_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(HonClimate),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.display_on", DisplayOnAction, HAIER_BASE_ACTION_SCHEMA
|
||||||
|
)
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.display_off", DisplayOffAction, HAIER_BASE_ACTION_SCHEMA
|
||||||
|
)
|
||||||
|
async def display_action_to_code(config, action_id, template_arg, args):
|
||||||
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.beeper_on", BeeperOnAction, HAIER_HON_BASE_ACTION_SCHEMA
|
||||||
|
)
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.beeper_off", BeeperOffAction, HAIER_HON_BASE_ACTION_SCHEMA
|
||||||
|
)
|
||||||
|
async def beeper_action_to_code(config, action_id, template_arg, args):
|
||||||
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
# Start self cleaning or steri-cleaning action action
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.start_self_cleaning",
|
||||||
|
StartSelfCleaningAction,
|
||||||
|
HAIER_HON_BASE_ACTION_SCHEMA,
|
||||||
|
)
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.start_steri_cleaning",
|
||||||
|
StartSteriCleaningAction,
|
||||||
|
HAIER_HON_BASE_ACTION_SCHEMA,
|
||||||
|
)
|
||||||
|
async def start_cleaning_to_code(config, action_id, template_arg, args):
|
||||||
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
# Set vertical airflow direction action
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.set_vertical_airflow",
|
||||||
|
VerticalAirflowAction,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(HonClimate),
|
||||||
|
cv.Required(CONF_VERTICAL_AIRFLOW): cv.templatable(
|
||||||
|
cv.enum(AIRFLOW_VERTICAL_DIRECTION_OPTIONS, upper=True)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def haier_set_vertical_airflow_to_code(config, action_id, template_arg, args):
|
||||||
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
|
template_ = await cg.templatable(
|
||||||
|
config[CONF_VERTICAL_AIRFLOW], args, AirflowVerticalDirection
|
||||||
|
)
|
||||||
|
cg.add(var.set_direction(template_))
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
# Set horizontal airflow direction action
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.set_horizontal_airflow",
|
||||||
|
HorizontalAirflowAction,
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.use_id(HonClimate),
|
||||||
|
cv.Required(CONF_HORIZONTAL_AIRFLOW): cv.templatable(
|
||||||
|
cv.enum(AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS, upper=True)
|
||||||
|
),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
async def haier_set_horizontal_airflow_to_code(config, action_id, template_arg, args):
|
||||||
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
|
template_ = await cg.templatable(
|
||||||
|
config[CONF_HORIZONTAL_AIRFLOW], args, AirflowHorizontalDirection
|
||||||
|
)
|
||||||
|
cg.add(var.set_direction(template_))
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.health_on", HealthOnAction, HAIER_BASE_ACTION_SCHEMA
|
||||||
|
)
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.health_off", HealthOffAction, HAIER_BASE_ACTION_SCHEMA
|
||||||
|
)
|
||||||
|
async def health_action_to_code(config, action_id, template_arg, args):
|
||||||
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.power_on", PowerOnAction, HAIER_BASE_ACTION_SCHEMA
|
||||||
|
)
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.power_off", PowerOffAction, HAIER_BASE_ACTION_SCHEMA
|
||||||
|
)
|
||||||
|
@automation.register_action(
|
||||||
|
"climate.haier.power_toggle", PowerToggleAction, HAIER_BASE_ACTION_SCHEMA
|
||||||
|
)
|
||||||
|
async def power_action_to_code(config, action_id, template_arg, args):
|
||||||
|
paren = await cg.get_variable(config[CONF_ID])
|
||||||
|
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||||
|
return var
|
||||||
|
|
||||||
|
|
||||||
|
def _final_validate(config):
|
||||||
|
full_config = fv.full_config.get()
|
||||||
|
if CONF_LOGGER in full_config:
|
||||||
|
_level = "NONE"
|
||||||
|
logger_config = full_config[CONF_LOGGER]
|
||||||
|
if CONF_LOGS in logger_config:
|
||||||
|
if "haier.protocol" in logger_config[CONF_LOGS]:
|
||||||
|
_level = logger_config[CONF_LOGS]["haier.protocol"]
|
||||||
|
else:
|
||||||
|
_level = logger_config[CONF_LEVEL]
|
||||||
|
_LOGGER.info("Detected log level for Haier protocol: %s", _level)
|
||||||
|
if _level not in logger.LOG_LEVEL_SEVERITY:
|
||||||
|
raise cv.Invalid("Unknown log level for Haier protocol")
|
||||||
|
_severity = logger.LOG_LEVEL_SEVERITY.index(_level)
|
||||||
|
cg.add_build_flag(f"-DHAIER_LOG_LEVEL={_severity}")
|
||||||
|
else:
|
||||||
|
_LOGGER.info(
|
||||||
|
"No logger component found, logging for Haier protocol is disabled"
|
||||||
|
)
|
||||||
|
cg.add_build_flag("-DHAIER_LOG_LEVEL=0")
|
||||||
|
if (
|
||||||
|
(CONF_WIFI_SIGNAL in config)
|
||||||
|
and (config[CONF_WIFI_SIGNAL])
|
||||||
|
and CONF_WIFI not in full_config
|
||||||
|
):
|
||||||
|
raise cv.Invalid(
|
||||||
|
f"No WiFi configured, if you want to use haier climate without WiFi add {CONF_WIFI_SIGNAL}: false to climate configuration"
|
||||||
|
)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|
||||||
|
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
|
cg.add(haier_ns.init_haier_protocol_logging())
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await climate.register_climate(var, config)
|
|
||||||
await uart.register_uart_device(var, config)
|
await uart.register_uart_device(var, config)
|
||||||
|
await climate.register_climate(var, config)
|
||||||
|
|
||||||
|
if (CONF_WIFI_SIGNAL in config) and (config[CONF_WIFI_SIGNAL]):
|
||||||
|
cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL]))
|
||||||
|
if CONF_BEEPER in config:
|
||||||
|
cg.add(var.set_beeper_state(config[CONF_BEEPER]))
|
||||||
|
if CONF_OUTDOOR_TEMPERATURE in config:
|
||||||
|
sens = await sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
|
||||||
|
cg.add(var.set_outdoor_temperature_sensor(sens))
|
||||||
|
if CONF_SUPPORTED_MODES in config:
|
||||||
|
cg.add(var.set_supported_modes(config[CONF_SUPPORTED_MODES]))
|
||||||
if CONF_SUPPORTED_SWING_MODES in config:
|
if CONF_SUPPORTED_SWING_MODES in config:
|
||||||
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
|
cg.add(var.set_supported_swing_modes(config[CONF_SUPPORTED_SWING_MODES]))
|
||||||
|
# https://github.com/paveldn/HaierProtocol
|
||||||
|
cg.add_library("pavlodn/HaierProtocol", "0.9.18")
|
||||||
|
|
|
@ -1,302 +0,0 @@
|
||||||
#include <cmath>
|
|
||||||
#include "haier.h"
|
|
||||||
#include "esphome/core/macros.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace haier {
|
|
||||||
|
|
||||||
static const char *const TAG = "haier";
|
|
||||||
|
|
||||||
static const uint8_t TEMPERATURE = 13;
|
|
||||||
static const uint8_t HUMIDITY = 15;
|
|
||||||
|
|
||||||
static const uint8_t MODE = 23;
|
|
||||||
|
|
||||||
static const uint8_t FAN_SPEED = 25;
|
|
||||||
|
|
||||||
static const uint8_t SWING = 27;
|
|
||||||
|
|
||||||
static const uint8_t POWER = 29;
|
|
||||||
static const uint8_t POWER_MASK = 1;
|
|
||||||
|
|
||||||
static const uint8_t SET_TEMPERATURE = 35;
|
|
||||||
static const uint8_t DECIMAL_MASK = (1 << 5);
|
|
||||||
|
|
||||||
static const uint8_t CRC = 36;
|
|
||||||
|
|
||||||
static const uint8_t COMFORT_PRESET_MASK = (1 << 3);
|
|
||||||
|
|
||||||
static const uint8_t MIN_VALID_TEMPERATURE = 16;
|
|
||||||
static const uint8_t MAX_VALID_TEMPERATURE = 50;
|
|
||||||
static const float TEMPERATURE_STEP = 0.5f;
|
|
||||||
|
|
||||||
static const uint8_t POLL_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 1, 90};
|
|
||||||
static const uint8_t OFF_REQ[13] = {255, 255, 10, 0, 0, 0, 0, 0, 1, 1, 77, 3, 92};
|
|
||||||
|
|
||||||
void HaierClimate::dump_config() {
|
|
||||||
ESP_LOGCONFIG(TAG, "Haier:");
|
|
||||||
ESP_LOGCONFIG(TAG, " Update interval: %u", this->get_update_interval());
|
|
||||||
this->dump_traits_(TAG);
|
|
||||||
this->check_uart_settings(9600);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::loop() {
|
|
||||||
if (this->available() >= sizeof(this->data_)) {
|
|
||||||
this->read_array(this->data_, sizeof(this->data_));
|
|
||||||
if (this->data_[0] != 255 || this->data_[1] != 255)
|
|
||||||
return;
|
|
||||||
|
|
||||||
read_state_(this->data_, sizeof(this->data_));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::update() {
|
|
||||||
this->write_array(POLL_REQ, sizeof(POLL_REQ));
|
|
||||||
dump_message_("Poll sent", POLL_REQ, sizeof(POLL_REQ));
|
|
||||||
}
|
|
||||||
|
|
||||||
climate::ClimateTraits HaierClimate::traits() {
|
|
||||||
auto traits = climate::ClimateTraits();
|
|
||||||
|
|
||||||
traits.set_visual_min_temperature(MIN_VALID_TEMPERATURE);
|
|
||||||
traits.set_visual_max_temperature(MAX_VALID_TEMPERATURE);
|
|
||||||
traits.set_visual_temperature_step(TEMPERATURE_STEP);
|
|
||||||
|
|
||||||
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL, climate::CLIMATE_MODE_COOL,
|
|
||||||
climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_DRY});
|
|
||||||
|
|
||||||
traits.set_supported_fan_modes({
|
|
||||||
climate::CLIMATE_FAN_AUTO,
|
|
||||||
climate::CLIMATE_FAN_LOW,
|
|
||||||
climate::CLIMATE_FAN_MEDIUM,
|
|
||||||
climate::CLIMATE_FAN_HIGH,
|
|
||||||
});
|
|
||||||
|
|
||||||
traits.set_supported_swing_modes(this->supported_swing_modes_);
|
|
||||||
traits.set_supports_current_temperature(true);
|
|
||||||
traits.set_supports_two_point_target_temperature(false);
|
|
||||||
|
|
||||||
traits.add_supported_preset(climate::CLIMATE_PRESET_NONE);
|
|
||||||
traits.add_supported_preset(climate::CLIMATE_PRESET_COMFORT);
|
|
||||||
|
|
||||||
return traits;
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::read_state_(const uint8_t *data, uint8_t size) {
|
|
||||||
dump_message_("Received state", data, size);
|
|
||||||
|
|
||||||
uint8_t check = data[CRC];
|
|
||||||
|
|
||||||
uint8_t crc = get_checksum_(data, size);
|
|
||||||
|
|
||||||
if (check != crc) {
|
|
||||||
ESP_LOGW(TAG, "Invalid checksum");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->current_temperature = data[TEMPERATURE];
|
|
||||||
|
|
||||||
this->target_temperature = data[SET_TEMPERATURE] + MIN_VALID_TEMPERATURE;
|
|
||||||
|
|
||||||
if (data[POWER] & DECIMAL_MASK) {
|
|
||||||
this->target_temperature += 0.5f;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (data[MODE]) {
|
|
||||||
case MODE_SMART:
|
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
|
||||||
break;
|
|
||||||
case MODE_COOL:
|
|
||||||
this->mode = climate::CLIMATE_MODE_COOL;
|
|
||||||
break;
|
|
||||||
case MODE_HEAT:
|
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
|
||||||
break;
|
|
||||||
case MODE_ONLY_FAN:
|
|
||||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
|
||||||
break;
|
|
||||||
case MODE_DRY:
|
|
||||||
this->mode = climate::CLIMATE_MODE_DRY;
|
|
||||||
break;
|
|
||||||
default: // other modes are unsupported
|
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (data[FAN_SPEED]) {
|
|
||||||
case FAN_AUTO:
|
|
||||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FAN_MIN:
|
|
||||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FAN_MIDDLE:
|
|
||||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case FAN_MAX:
|
|
||||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (data[SWING]) {
|
|
||||||
case SWING_OFF:
|
|
||||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SWING_VERTICAL:
|
|
||||||
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SWING_HORIZONTAL:
|
|
||||||
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case SWING_BOTH:
|
|
||||||
this->swing_mode = climate::CLIMATE_SWING_BOTH;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data[POWER] & COMFORT_PRESET_MASK) {
|
|
||||||
this->preset = climate::CLIMATE_PRESET_COMFORT;
|
|
||||||
} else {
|
|
||||||
this->preset = climate::CLIMATE_PRESET_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((data[POWER] & POWER_MASK) == 0) {
|
|
||||||
this->mode = climate::CLIMATE_MODE_OFF;
|
|
||||||
}
|
|
||||||
|
|
||||||
this->publish_state();
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::control(const climate::ClimateCall &call) {
|
|
||||||
if (call.get_mode().has_value()) {
|
|
||||||
switch (call.get_mode().value()) {
|
|
||||||
case climate::CLIMATE_MODE_OFF:
|
|
||||||
send_data_(OFF_REQ, sizeof(OFF_REQ));
|
|
||||||
break;
|
|
||||||
|
|
||||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
|
||||||
case climate::CLIMATE_MODE_AUTO:
|
|
||||||
data_[POWER] |= POWER_MASK;
|
|
||||||
data_[MODE] = MODE_SMART;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_MODE_HEAT:
|
|
||||||
data_[POWER] |= POWER_MASK;
|
|
||||||
data_[MODE] = MODE_HEAT;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_MODE_COOL:
|
|
||||||
data_[POWER] |= POWER_MASK;
|
|
||||||
data_[MODE] = MODE_COOL;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
|
||||||
data_[POWER] |= POWER_MASK;
|
|
||||||
data_[MODE] = MODE_ONLY_FAN;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case climate::CLIMATE_MODE_DRY:
|
|
||||||
data_[POWER] |= POWER_MASK;
|
|
||||||
data_[MODE] = MODE_DRY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.get_preset().has_value()) {
|
|
||||||
if (call.get_preset().value() == climate::CLIMATE_PRESET_COMFORT) {
|
|
||||||
data_[POWER] |= COMFORT_PRESET_MASK;
|
|
||||||
} else {
|
|
||||||
data_[POWER] &= ~COMFORT_PRESET_MASK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.get_target_temperature().has_value()) {
|
|
||||||
float target = call.get_target_temperature().value() - MIN_VALID_TEMPERATURE;
|
|
||||||
|
|
||||||
data_[SET_TEMPERATURE] = (uint8_t) target;
|
|
||||||
|
|
||||||
if ((int) target == std::lroundf(target)) {
|
|
||||||
data_[POWER] &= ~DECIMAL_MASK;
|
|
||||||
} else {
|
|
||||||
data_[POWER] |= DECIMAL_MASK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.get_fan_mode().has_value()) {
|
|
||||||
switch (call.get_fan_mode().value()) {
|
|
||||||
case climate::CLIMATE_FAN_AUTO:
|
|
||||||
data_[FAN_SPEED] = FAN_AUTO;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_FAN_LOW:
|
|
||||||
data_[FAN_SPEED] = FAN_MIN;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_FAN_MEDIUM:
|
|
||||||
data_[FAN_SPEED] = FAN_MIDDLE;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_FAN_HIGH:
|
|
||||||
data_[FAN_SPEED] = FAN_MAX;
|
|
||||||
break;
|
|
||||||
|
|
||||||
default: // other modes are unsupported
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (call.get_swing_mode().has_value()) {
|
|
||||||
switch (call.get_swing_mode().value()) {
|
|
||||||
case climate::CLIMATE_SWING_OFF:
|
|
||||||
data_[SWING] = SWING_OFF;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_SWING_VERTICAL:
|
|
||||||
data_[SWING] = SWING_VERTICAL;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_SWING_HORIZONTAL:
|
|
||||||
data_[SWING] = SWING_HORIZONTAL;
|
|
||||||
break;
|
|
||||||
case climate::CLIMATE_SWING_BOTH:
|
|
||||||
data_[SWING] = SWING_BOTH;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parts of the message that must have specific values for "send" command.
|
|
||||||
// The meaning of those values is unknown at the moment.
|
|
||||||
data_[9] = 1;
|
|
||||||
data_[10] = 77;
|
|
||||||
data_[11] = 95;
|
|
||||||
data_[17] = 0;
|
|
||||||
|
|
||||||
// Compute checksum
|
|
||||||
uint8_t crc = get_checksum_(data_, sizeof(data_));
|
|
||||||
data_[CRC] = crc;
|
|
||||||
|
|
||||||
send_data_(data_, sizeof(data_));
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::send_data_(const uint8_t *message, uint8_t size) {
|
|
||||||
this->write_array(message, size);
|
|
||||||
|
|
||||||
dump_message_("Sent message", message, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void HaierClimate::dump_message_(const char *title, const uint8_t *message, uint8_t size) {
|
|
||||||
ESP_LOGV(TAG, "%s:", title);
|
|
||||||
for (int i = 0; i < size; i++) {
|
|
||||||
ESP_LOGV(TAG, " byte %02d - %d", i, message[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t HaierClimate::get_checksum_(const uint8_t *message, size_t size) {
|
|
||||||
uint8_t position = size - 1;
|
|
||||||
uint8_t crc = 0;
|
|
||||||
|
|
||||||
for (int i = 2; i < position; i++)
|
|
||||||
crc += message[i];
|
|
||||||
|
|
||||||
return crc;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace haier
|
|
||||||
} // namespace esphome
|
|
|
@ -1,37 +0,0 @@
|
||||||
#pragma once
|
|
||||||
|
|
||||||
#include "esphome/core/component.h"
|
|
||||||
#include "esphome/components/climate/climate.h"
|
|
||||||
#include "esphome/components/uart/uart.h"
|
|
||||||
|
|
||||||
namespace esphome {
|
|
||||||
namespace haier {
|
|
||||||
|
|
||||||
enum Mode : uint8_t { MODE_SMART = 0, MODE_COOL = 1, MODE_HEAT = 2, MODE_ONLY_FAN = 3, MODE_DRY = 4 };
|
|
||||||
enum FanSpeed : uint8_t { FAN_MAX = 0, FAN_MIDDLE = 1, FAN_MIN = 2, FAN_AUTO = 3 };
|
|
||||||
enum SwingMode : uint8_t { SWING_OFF = 0, SWING_VERTICAL = 1, SWING_HORIZONTAL = 2, SWING_BOTH = 3 };
|
|
||||||
|
|
||||||
class HaierClimate : public climate::Climate, public uart::UARTDevice, public PollingComponent {
|
|
||||||
public:
|
|
||||||
void loop() override;
|
|
||||||
void update() override;
|
|
||||||
void dump_config() override;
|
|
||||||
void control(const climate::ClimateCall &call) override;
|
|
||||||
void set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
|
|
||||||
this->supported_swing_modes_ = modes;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected:
|
|
||||||
climate::ClimateTraits traits() override;
|
|
||||||
void read_state_(const uint8_t *data, uint8_t size);
|
|
||||||
void send_data_(const uint8_t *message, uint8_t size);
|
|
||||||
void dump_message_(const char *title, const uint8_t *message, uint8_t size);
|
|
||||||
uint8_t get_checksum_(const uint8_t *message, size_t size);
|
|
||||||
|
|
||||||
private:
|
|
||||||
uint8_t data_[37];
|
|
||||||
std::set<climate::ClimateSwingMode> supported_swing_modes_{};
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace haier
|
|
||||||
} // namespace esphome
|
|
311
esphome/components/haier/haier_base.cpp
Normal file
311
esphome/components/haier/haier_base.cpp
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
#include "esphome/components/climate/climate.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
#include "haier_base.h"
|
||||||
|
|
||||||
|
using namespace esphome::climate;
|
||||||
|
using namespace esphome::uart;
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
static const char *const TAG = "haier.climate";
|
||||||
|
constexpr size_t COMMUNICATION_TIMEOUT_MS = 60000;
|
||||||
|
constexpr size_t STATUS_REQUEST_INTERVAL_MS = 5000;
|
||||||
|
constexpr size_t PROTOCOL_INITIALIZATION_INTERVAL = 10000;
|
||||||
|
constexpr size_t DEFAULT_MESSAGES_INTERVAL_MS = 2000;
|
||||||
|
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) {
|
||||||
|
static const char *phase_names[] = {
|
||||||
|
"SENDING_INIT_1",
|
||||||
|
"WAITING_ANSWER_INIT_1",
|
||||||
|
"SENDING_INIT_2",
|
||||||
|
"WAITING_ANSWER_INIT_2",
|
||||||
|
"SENDING_FIRST_STATUS_REQUEST",
|
||||||
|
"WAITING_FIRST_STATUS_ANSWER",
|
||||||
|
"SENDING_ALARM_STATUS_REQUEST",
|
||||||
|
"WAITING_ALARM_STATUS_ANSWER",
|
||||||
|
"IDLE",
|
||||||
|
"SENDING_STATUS_REQUEST",
|
||||||
|
"WAITING_STATUS_ANSWER",
|
||||||
|
"SENDING_UPDATE_SIGNAL_REQUEST",
|
||||||
|
"WAITING_UPDATE_SIGNAL_ANSWER",
|
||||||
|
"SENDING_SIGNAL_LEVEL",
|
||||||
|
"WAITING_SIGNAL_LEVEL_ANSWER",
|
||||||
|
"SENDING_CONTROL",
|
||||||
|
"WAITING_CONTROL_ANSWER",
|
||||||
|
"SENDING_POWER_ON_COMMAND",
|
||||||
|
"WAITING_POWER_ON_ANSWER",
|
||||||
|
"SENDING_POWER_OFF_COMMAND",
|
||||||
|
"WAITING_POWER_OFF_ANSWER",
|
||||||
|
"UNKNOWN" // Should be the last!
|
||||||
|
};
|
||||||
|
int phase_index = (int) phase;
|
||||||
|
if ((phase_index > (int) ProtocolPhases::NUM_PROTOCOL_PHASES) || (phase_index < 0))
|
||||||
|
phase_index = (int) ProtocolPhases::NUM_PROTOCOL_PHASES;
|
||||||
|
return phase_names[phase_index];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
HaierClimateBase::HaierClimateBase()
|
||||||
|
: haier_protocol_(*this),
|
||||||
|
protocol_phase_(ProtocolPhases::SENDING_INIT_1),
|
||||||
|
action_request_(ActionRequest::NO_ACTION),
|
||||||
|
display_status_(true),
|
||||||
|
health_mode_(false),
|
||||||
|
force_send_control_(false),
|
||||||
|
forced_publish_(false),
|
||||||
|
forced_request_status_(false),
|
||||||
|
first_control_attempt_(false),
|
||||||
|
reset_protocol_request_(false) {
|
||||||
|
this->traits_ = climate::ClimateTraits();
|
||||||
|
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_AUTO});
|
||||||
|
this->traits_.set_supported_fan_modes(
|
||||||
|
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
|
||||||
|
this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
|
||||||
|
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
|
||||||
|
this->traits_.set_supports_current_temperature(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
HaierClimateBase::~HaierClimateBase() {}
|
||||||
|
|
||||||
|
void HaierClimateBase::set_phase_(ProtocolPhases 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));
|
||||||
|
#else
|
||||||
|
ESP_LOGV(TAG, "Phase transition: %d => %d", (int) this->protocol_phase_, (int) phase);
|
||||||
|
#endif
|
||||||
|
this->protocol_phase_ = phase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
return this->check_timeout_(now, this->last_request_timestamp_, CONTROL_MESSAGES_INTERVAL_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::is_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now) {
|
||||||
|
return this->check_timeout_(now, this->last_request_timestamp_, PROTOCOL_INITIALIZATION_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::get_display_state() const { return this->display_status_; }
|
||||||
|
|
||||||
|
void HaierClimateBase::set_display_state(bool state) {
|
||||||
|
if (this->display_status_ != state) {
|
||||||
|
this->display_status_ = state;
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HaierClimateBase::get_health_mode() const { return this->health_mode_; }
|
||||||
|
|
||||||
|
void HaierClimateBase::set_health_mode(bool state) {
|
||||||
|
if (this->health_mode_ != state) {
|
||||||
|
this->health_mode_ = state;
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::send_power_on_command() { this->action_request_ = ActionRequest::TURN_POWER_ON; }
|
||||||
|
|
||||||
|
void HaierClimateBase::send_power_off_command() { this->action_request_ = ActionRequest::TURN_POWER_OFF; }
|
||||||
|
|
||||||
|
void HaierClimateBase::toggle_power() { this->action_request_ = ActionRequest::TOGGLE_POWER; }
|
||||||
|
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
|
||||||
|
this->traits_.set_supported_swing_modes(modes);
|
||||||
|
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF); // Always available
|
||||||
|
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); // Always available
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
|
||||||
|
this->traits_.set_supported_modes(modes);
|
||||||
|
this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available
|
||||||
|
this->traits_.add_supported_mode(climate::CLIMATE_MODE_AUTO); // Always available
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HaierClimateBase::answer_preprocess_(uint8_t request_message_type,
|
||||||
|
uint8_t expected_request_message_type,
|
||||||
|
uint8_t answer_message_type,
|
||||||
|
uint8_t expected_answer_message_type,
|
||||||
|
ProtocolPhases expected_phase) {
|
||||||
|
haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
|
||||||
|
if ((expected_request_message_type != NO_COMMAND) && (request_message_type != expected_request_message_type))
|
||||||
|
result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
|
||||||
|
if ((expected_answer_message_type != NO_COMMAND) && (answer_message_type != expected_answer_message_type))
|
||||||
|
result = haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
|
||||||
|
if ((expected_phase != ProtocolPhases::UNKNOWN) && (expected_phase != this->protocol_phase_))
|
||||||
|
result = haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
|
||||||
|
if (is_message_invalid(answer_message_type))
|
||||||
|
result = haier_protocol::HandlerError::INVALID_ANSWER;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HaierClimateBase::timeout_default_handler_(uint8_t request_type) {
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %s", request_type, phase_to_string_(this->protocol_phase_));
|
||||||
|
#else
|
||||||
|
ESP_LOGW(TAG, "Answer timeout for command %02X, phase %d", request_type, (int) this->protocol_phase_);
|
||||||
|
#endif
|
||||||
|
if (this->protocol_phase_ > ProtocolPhases::IDLE) {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
} else {
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
}
|
||||||
|
return haier_protocol::HandlerError::HANDLER_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::setup() {
|
||||||
|
ESP_LOGI(TAG, "Haier initialization...");
|
||||||
|
// Set timestamp here to give AC time to boot
|
||||||
|
this->last_request_timestamp_ = std::chrono::steady_clock::now();
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
this->set_answers_handlers();
|
||||||
|
this->haier_protocol_.set_default_timeout_handler(
|
||||||
|
std::bind(&esphome::haier::HaierClimateBase::timeout_default_handler_, this, std::placeholders::_1));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::dump_config() {
|
||||||
|
LOG_CLIMATE("", "Haier Climate", this);
|
||||||
|
ESP_LOGCONFIG(TAG, " Device communication status: %s",
|
||||||
|
(this->protocol_phase_ >= ProtocolPhases::IDLE) ? "established" : "none");
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::loop() {
|
||||||
|
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() >
|
||||||
|
COMMUNICATION_TIMEOUT_MS) ||
|
||||||
|
(this->reset_protocol_request_)) {
|
||||||
|
if (this->protocol_phase_ >= ProtocolPhases::IDLE) {
|
||||||
|
// No status too long, reseting protocol
|
||||||
|
if (this->reset_protocol_request_) {
|
||||||
|
this->reset_protocol_request_ = false;
|
||||||
|
ESP_LOGW(TAG, "Protocol reset requested");
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Communication timeout, reseting protocol");
|
||||||
|
}
|
||||||
|
this->last_valid_status_timestamp_ = now;
|
||||||
|
this->set_force_send_control_(false);
|
||||||
|
if (this->hvac_settings_.valid)
|
||||||
|
this->hvac_settings_.reset();
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
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) ||
|
||||||
|
(this->protocol_phase_ == ProtocolPhases::SENDING_STATUS_REQUEST) ||
|
||||||
|
(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
|
||||||
|
// procedure or waiting for an answer
|
||||||
|
if (this->action_request_ != ActionRequest::NO_ACTION) {
|
||||||
|
this->process_pending_action();
|
||||||
|
} else if (this->hvac_settings_.valid || this->force_send_control_) {
|
||||||
|
ESP_LOGV(TAG, "Control packet is pending...");
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_CONTROL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->process_phase(now);
|
||||||
|
this->haier_protocol_.loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::process_pending_action() {
|
||||||
|
ActionRequest request = this->action_request_;
|
||||||
|
if (this->action_request_ == ActionRequest::TOGGLE_POWER) {
|
||||||
|
request = this->mode == CLIMATE_MODE_OFF ? ActionRequest::TURN_POWER_ON : ActionRequest::TURN_POWER_OFF;
|
||||||
|
}
|
||||||
|
switch (request) {
|
||||||
|
case ActionRequest::TURN_POWER_ON:
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_POWER_ON_COMMAND);
|
||||||
|
break;
|
||||||
|
case ActionRequest::TURN_POWER_OFF:
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_POWER_OFF_COMMAND);
|
||||||
|
break;
|
||||||
|
case ActionRequest::TOGGLE_POWER:
|
||||||
|
case ActionRequest::NO_ACTION:
|
||||||
|
// shouldn't get here, do nothing
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGW(TAG, "Unsupported action: %d", (uint8_t) this->action_request_);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->action_request_ = ActionRequest::NO_ACTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
ClimateTraits HaierClimateBase::traits() { return traits_; }
|
||||||
|
|
||||||
|
void HaierClimateBase::control(const ClimateCall &call) {
|
||||||
|
ESP_LOGD("Control", "Control call");
|
||||||
|
if (this->protocol_phase_ < ProtocolPhases::IDLE) {
|
||||||
|
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.
|
||||||
|
}
|
||||||
|
if (this->hvac_settings_.valid) {
|
||||||
|
ESP_LOGW(TAG, "Overriding old valid settings before they were applied!");
|
||||||
|
}
|
||||||
|
{
|
||||||
|
if (call.get_mode().has_value())
|
||||||
|
this->hvac_settings_.mode = call.get_mode();
|
||||||
|
if (call.get_fan_mode().has_value())
|
||||||
|
this->hvac_settings_.fan_mode = call.get_fan_mode();
|
||||||
|
if (call.get_swing_mode().has_value())
|
||||||
|
this->hvac_settings_.swing_mode = call.get_swing_mode();
|
||||||
|
if (call.get_target_temperature().has_value())
|
||||||
|
this->hvac_settings_.target_temperature = call.get_target_temperature();
|
||||||
|
if (call.get_preset().has_value())
|
||||||
|
this->hvac_settings_.preset = call.get_preset();
|
||||||
|
this->hvac_settings_.valid = true;
|
||||||
|
}
|
||||||
|
this->first_control_attempt_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::HvacSettings::reset() {
|
||||||
|
this->valid = false;
|
||||||
|
this->mode.reset();
|
||||||
|
this->fan_mode.reset();
|
||||||
|
this->swing_mode.reset();
|
||||||
|
this->target_temperature.reset();
|
||||||
|
this->preset.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::set_force_send_control_(bool status) {
|
||||||
|
this->force_send_control_ = status;
|
||||||
|
if (status) {
|
||||||
|
this->first_control_attempt_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HaierClimateBase::send_message_(const haier_protocol::HaierMessage &command, bool use_crc) {
|
||||||
|
this->haier_protocol_.send_message(command, use_crc);
|
||||||
|
this->last_request_timestamp_ = std::chrono::steady_clock::now();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
142
esphome/components/haier/haier_base.h
Normal file
142
esphome/components/haier/haier_base.h
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <set>
|
||||||
|
#include "esphome/components/climate/climate.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
// HaierProtocol
|
||||||
|
#include <protocol/haier_protocol.h>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
enum class ActionRequest : uint8_t {
|
||||||
|
NO_ACTION = 0,
|
||||||
|
TURN_POWER_ON = 1,
|
||||||
|
TURN_POWER_OFF = 2,
|
||||||
|
TOGGLE_POWER = 3,
|
||||||
|
START_SELF_CLEAN = 4, // only hOn
|
||||||
|
START_STERI_CLEAN = 5, // only hOn
|
||||||
|
};
|
||||||
|
|
||||||
|
class HaierClimateBase : public esphome::Component,
|
||||||
|
public esphome::climate::Climate,
|
||||||
|
public esphome::uart::UARTDevice,
|
||||||
|
public haier_protocol::ProtocolStream {
|
||||||
|
public:
|
||||||
|
HaierClimateBase();
|
||||||
|
HaierClimateBase(const HaierClimateBase &) = delete;
|
||||||
|
HaierClimateBase &operator=(const HaierClimateBase &) = delete;
|
||||||
|
~HaierClimateBase();
|
||||||
|
void setup() override;
|
||||||
|
void loop() override;
|
||||||
|
void control(const esphome::climate::ClimateCall &call) override;
|
||||||
|
void dump_config() override;
|
||||||
|
float get_setup_priority() const override { return esphome::setup_priority::HARDWARE; }
|
||||||
|
void set_fahrenheit(bool fahrenheit);
|
||||||
|
void set_display_state(bool state);
|
||||||
|
bool get_display_state() const;
|
||||||
|
void set_health_mode(bool state);
|
||||||
|
bool get_health_mode() const;
|
||||||
|
void send_power_on_command();
|
||||||
|
void send_power_off_command();
|
||||||
|
void toggle_power();
|
||||||
|
void reset_protocol() { this->reset_protocol_request_ = true; };
|
||||||
|
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
|
||||||
|
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
|
||||||
|
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
|
||||||
|
size_t read_array(uint8_t *data, size_t len) noexcept override {
|
||||||
|
return esphome::uart::UARTDevice::read_array(data, len) ? len : 0;
|
||||||
|
};
|
||||||
|
void write_array(const uint8_t *data, size_t len) noexcept override {
|
||||||
|
esphome::uart::UARTDevice::write_array(data, len);
|
||||||
|
};
|
||||||
|
bool can_send_message() const { return haier_protocol_.get_outgoing_queue_size() == 0; };
|
||||||
|
|
||||||
|
protected:
|
||||||
|
enum class ProtocolPhases {
|
||||||
|
UNKNOWN = -1,
|
||||||
|
// INITIALIZATION
|
||||||
|
SENDING_INIT_1 = 0,
|
||||||
|
WAITING_ANSWER_INIT_1 = 1,
|
||||||
|
SENDING_INIT_2 = 2,
|
||||||
|
WAITING_ANSWER_INIT_2 = 3,
|
||||||
|
SENDING_FIRST_STATUS_REQUEST = 4,
|
||||||
|
WAITING_FIRST_STATUS_ANSWER = 5,
|
||||||
|
SENDING_ALARM_STATUS_REQUEST = 6,
|
||||||
|
WAITING_ALARM_STATUS_ANSWER = 7,
|
||||||
|
// FUNCTIONAL STATE
|
||||||
|
IDLE = 8,
|
||||||
|
SENDING_STATUS_REQUEST = 9,
|
||||||
|
WAITING_STATUS_ANSWER = 10,
|
||||||
|
SENDING_UPDATE_SIGNAL_REQUEST = 11,
|
||||||
|
WAITING_UPDATE_SIGNAL_ANSWER = 12,
|
||||||
|
SENDING_SIGNAL_LEVEL = 13,
|
||||||
|
WAITING_SIGNAL_LEVEL_ANSWER = 14,
|
||||||
|
SENDING_CONTROL = 15,
|
||||||
|
WAITING_CONTROL_ANSWER = 16,
|
||||||
|
SENDING_POWER_ON_COMMAND = 17,
|
||||||
|
WAITING_POWER_ON_ANSWER = 18,
|
||||||
|
SENDING_POWER_OFF_COMMAND = 19,
|
||||||
|
WAITING_POWER_OFF_ANSWER = 20,
|
||||||
|
NUM_PROTOCOL_PHASES
|
||||||
|
};
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
const char *phase_to_string_(ProtocolPhases phase);
|
||||||
|
#endif
|
||||||
|
virtual void set_answers_handlers() = 0;
|
||||||
|
virtual void process_phase(std::chrono::steady_clock::time_point now) = 0;
|
||||||
|
virtual haier_protocol::HaierMessage get_control_message() = 0;
|
||||||
|
virtual bool is_message_invalid(uint8_t message_type) = 0;
|
||||||
|
virtual void process_pending_action();
|
||||||
|
esphome::climate::ClimateTraits traits() override;
|
||||||
|
// Answers handlers
|
||||||
|
haier_protocol::HandlerError answer_preprocess_(uint8_t request_message_type, uint8_t expected_request_message_type,
|
||||||
|
uint8_t answer_message_type, uint8_t expected_answer_message_type,
|
||||||
|
ProtocolPhases expected_phase);
|
||||||
|
// Timeout handler
|
||||||
|
haier_protocol::HandlerError timeout_default_handler_(uint8_t request_type);
|
||||||
|
// Helper functions
|
||||||
|
void set_force_send_control_(bool status);
|
||||||
|
void send_message_(const haier_protocol::HaierMessage &command, bool use_crc);
|
||||||
|
void set_phase_(ProtocolPhases phase);
|
||||||
|
bool check_timeout_(std::chrono::steady_clock::time_point now, std::chrono::steady_clock::time_point tpoint,
|
||||||
|
size_t timeout);
|
||||||
|
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_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_protocol_initialisation_interval_exceded_(std::chrono::steady_clock::time_point now);
|
||||||
|
|
||||||
|
struct HvacSettings {
|
||||||
|
esphome::optional<esphome::climate::ClimateMode> mode;
|
||||||
|
esphome::optional<esphome::climate::ClimateFanMode> fan_mode;
|
||||||
|
esphome::optional<esphome::climate::ClimateSwingMode> swing_mode;
|
||||||
|
esphome::optional<float> target_temperature;
|
||||||
|
esphome::optional<esphome::climate::ClimatePreset> preset;
|
||||||
|
bool valid;
|
||||||
|
HvacSettings() : valid(false){};
|
||||||
|
void reset();
|
||||||
|
};
|
||||||
|
haier_protocol::ProtocolHandler haier_protocol_;
|
||||||
|
ProtocolPhases protocol_phase_;
|
||||||
|
ActionRequest action_request_;
|
||||||
|
uint8_t fan_mode_speed_;
|
||||||
|
uint8_t other_modes_fan_speed_;
|
||||||
|
bool display_status_;
|
||||||
|
bool health_mode_;
|
||||||
|
bool force_send_control_;
|
||||||
|
bool forced_publish_;
|
||||||
|
bool forced_request_status_;
|
||||||
|
bool first_control_attempt_;
|
||||||
|
bool reset_protocol_request_;
|
||||||
|
esphome::climate::ClimateTraits traits_;
|
||||||
|
HvacSettings hvac_settings_;
|
||||||
|
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_status_request_; // To request AC status
|
||||||
|
std::chrono::steady_clock::time_point control_request_timestamp_; // To send control message
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
857
esphome/components/haier/hon_climate.cpp
Normal file
857
esphome/components/haier/hon_climate.cpp
Normal file
|
@ -0,0 +1,857 @@
|
||||||
|
#include <chrono>
|
||||||
|
#include <string>
|
||||||
|
#include "esphome/components/climate/climate.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
#ifdef USE_WIFI
|
||||||
|
#include "esphome/components/wifi/wifi_component.h"
|
||||||
|
#endif
|
||||||
|
#include "hon_climate.h"
|
||||||
|
#include "hon_packet.h"
|
||||||
|
|
||||||
|
using namespace esphome::climate;
|
||||||
|
using namespace esphome::uart;
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
static const char *const TAG = "haier.climate";
|
||||||
|
constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
|
||||||
|
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
|
||||||
|
|
||||||
|
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
|
||||||
|
switch (direction) {
|
||||||
|
case AirflowVerticalDirection::HEALTH_UP:
|
||||||
|
return hon_protocol::VerticalSwingMode::HEALTH_UP;
|
||||||
|
case AirflowVerticalDirection::MAX_UP:
|
||||||
|
return hon_protocol::VerticalSwingMode::MAX_UP;
|
||||||
|
case AirflowVerticalDirection::UP:
|
||||||
|
return hon_protocol::VerticalSwingMode::UP;
|
||||||
|
case AirflowVerticalDirection::DOWN:
|
||||||
|
return hon_protocol::VerticalSwingMode::DOWN;
|
||||||
|
case AirflowVerticalDirection::HEALTH_DOWN:
|
||||||
|
return hon_protocol::VerticalSwingMode::HEALTH_DOWN;
|
||||||
|
default:
|
||||||
|
return hon_protocol::VerticalSwingMode::CENTER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hon_protocol::HorizontalSwingMode get_horizontal_swing_mode(AirflowHorizontalDirection direction) {
|
||||||
|
switch (direction) {
|
||||||
|
case AirflowHorizontalDirection::MAX_LEFT:
|
||||||
|
return hon_protocol::HorizontalSwingMode::MAX_LEFT;
|
||||||
|
case AirflowHorizontalDirection::LEFT:
|
||||||
|
return hon_protocol::HorizontalSwingMode::LEFT;
|
||||||
|
case AirflowHorizontalDirection::RIGHT:
|
||||||
|
return hon_protocol::HorizontalSwingMode::RIGHT;
|
||||||
|
case AirflowHorizontalDirection::MAX_RIGHT:
|
||||||
|
return hon_protocol::HorizontalSwingMode::MAX_RIGHT;
|
||||||
|
default:
|
||||||
|
return hon_protocol::HorizontalSwingMode::CENTER;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HonClimate::HonClimate()
|
||||||
|
: last_status_message_(new uint8_t[sizeof(hon_protocol::HaierPacketControl)]),
|
||||||
|
cleaning_status_(CleaningState::NO_CLEANING),
|
||||||
|
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},
|
||||||
|
outdoor_sensor_(nullptr),
|
||||||
|
send_wifi_signal_(true) {
|
||||||
|
this->traits_.set_supported_presets({
|
||||||
|
climate::CLIMATE_PRESET_NONE,
|
||||||
|
climate::CLIMATE_PRESET_ECO,
|
||||||
|
climate::CLIMATE_PRESET_BOOST,
|
||||||
|
climate::CLIMATE_PRESET_SLEEP,
|
||||||
|
});
|
||||||
|
this->fan_mode_speed_ = (uint8_t) hon_protocol::FanMode::FAN_MID;
|
||||||
|
this->other_modes_fan_speed_ = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
|
||||||
|
}
|
||||||
|
|
||||||
|
HonClimate::~HonClimate() {}
|
||||||
|
|
||||||
|
void HonClimate::set_beeper_state(bool state) { this->beeper_status_ = state; }
|
||||||
|
|
||||||
|
bool HonClimate::get_beeper_state() const { return this->beeper_status_; }
|
||||||
|
|
||||||
|
void HonClimate::set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
|
||||||
|
|
||||||
|
AirflowVerticalDirection HonClimate::get_vertical_airflow() const { return this->vertical_direction_; };
|
||||||
|
|
||||||
|
void HonClimate::set_vertical_airflow(AirflowVerticalDirection direction) {
|
||||||
|
if (direction > AirflowVerticalDirection::DOWN) {
|
||||||
|
this->vertical_direction_ = AirflowVerticalDirection::CENTER;
|
||||||
|
} else {
|
||||||
|
this->vertical_direction_ = direction;
|
||||||
|
}
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
AirflowHorizontalDirection HonClimate::get_horizontal_airflow() const { return this->horizontal_direction_; }
|
||||||
|
|
||||||
|
void HonClimate::set_horizontal_airflow(AirflowHorizontalDirection direction) {
|
||||||
|
if (direction > AirflowHorizontalDirection::RIGHT) {
|
||||||
|
this->horizontal_direction_ = AirflowHorizontalDirection::CENTER;
|
||||||
|
} else {
|
||||||
|
this->horizontal_direction_ = direction;
|
||||||
|
}
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string HonClimate::get_cleaning_status_text() const {
|
||||||
|
switch (this->cleaning_status_) {
|
||||||
|
case CleaningState::SELF_CLEAN:
|
||||||
|
return "Self clean";
|
||||||
|
case CleaningState::STERI_CLEAN:
|
||||||
|
return "56°C Steri-Clean";
|
||||||
|
default:
|
||||||
|
return "No cleaning";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CleaningState HonClimate::get_cleaning_status() const { return this->cleaning_status_; }
|
||||||
|
|
||||||
|
void HonClimate::start_self_cleaning() {
|
||||||
|
if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
|
||||||
|
ESP_LOGI(TAG, "Sending self cleaning start request");
|
||||||
|
this->action_request_ = ActionRequest::START_SELF_CLEAN;
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::start_steri_cleaning() {
|
||||||
|
if (this->cleaning_status_ == CleaningState::NO_CLEANING) {
|
||||||
|
ESP_LOGI(TAG, "Sending steri cleaning start request");
|
||||||
|
this->action_request_ = ActionRequest::START_STERI_CLEAN;
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::set_send_wifi(bool send_wifi) { this->send_wifi_signal_ = send_wifi; }
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result = this->answer_preprocess_(
|
||||||
|
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, message_type,
|
||||||
|
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_1);
|
||||||
|
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
if (data_size < sizeof(hon_protocol::DeviceVersionAnswer)) {
|
||||||
|
// Wrong structure
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||||
|
}
|
||||||
|
// All OK
|
||||||
|
hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data;
|
||||||
|
char tmp[9];
|
||||||
|
tmp[8] = 0;
|
||||||
|
strncpy(tmp, answr->protocol_version, 8);
|
||||||
|
this->hvac_protocol_version_ = std::string(tmp);
|
||||||
|
strncpy(tmp, answr->software_version, 8);
|
||||||
|
this->hvac_software_version_ = std::string(tmp);
|
||||||
|
strncpy(tmp, answr->hardware_version, 8);
|
||||||
|
this->hvac_hardware_version_ = std::string(tmp);
|
||||||
|
strncpy(tmp, answr->device_name, 8);
|
||||||
|
this->hvac_device_name_ = std::string(tmp);
|
||||||
|
this->hvac_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_functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
|
||||||
|
this->hvac_functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
|
||||||
|
this->hvac_functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
|
||||||
|
this->hvac_hardware_info_available_ = true;
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_2);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_INIT_1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result = this->answer_preprocess_(
|
||||||
|
request_type, (uint8_t) hon_protocol::FrameType::GET_DEVICE_ID, message_type,
|
||||||
|
(uint8_t) hon_protocol::FrameType::GET_DEVICE_ID_RESPONSE, ProtocolPhases::WAITING_ANSWER_INIT_2);
|
||||||
|
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_INIT_1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::status_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result =
|
||||||
|
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::CONTROL, message_type,
|
||||||
|
(uint8_t) hon_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
|
||||||
|
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
result = this->process_status_message_(data, data_size);
|
||||||
|
if (result != haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_INIT_1);
|
||||||
|
} else {
|
||||||
|
if (data_size >= sizeof(hon_protocol::HaierPacketControl) + 2) {
|
||||||
|
memcpy(this->last_status_message_.get(), data + 2, sizeof(hon_protocol::HaierPacketControl));
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
|
||||||
|
sizeof(hon_protocol::HaierPacketControl));
|
||||||
|
}
|
||||||
|
if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) {
|
||||||
|
ESP_LOGI(TAG, "First HVAC status received");
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
|
||||||
|
} else if ((this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) ||
|
||||||
|
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_ON_ANSWER) ||
|
||||||
|
(this->protocol_phase_ == ProtocolPhases::WAITING_POWER_OFF_ANSWER)) {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
this->set_force_send_control_(false);
|
||||||
|
if (this->hvac_settings_.valid)
|
||||||
|
this->hvac_settings_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_INIT_1);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::get_management_information_answer_handler_(uint8_t request_type,
|
||||||
|
uint8_t message_type,
|
||||||
|
const uint8_t *data,
|
||||||
|
size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result =
|
||||||
|
this->answer_preprocess_(request_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION,
|
||||||
|
message_type, (uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION_RESPONSE,
|
||||||
|
ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
|
||||||
|
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_SIGNAL_LEVEL);
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::report_network_status_answer_handler_(uint8_t request_type,
|
||||||
|
uint8_t 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) {
|
||||||
|
if (request_type == (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS) {
|
||||||
|
if (message_type != (uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS_RESPONSE) {
|
||||||
|
// Unexpected answer to request
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
|
||||||
|
}
|
||||||
|
if (this->protocol_phase_ != ProtocolPhases::WAITING_ALARM_STATUS_ANSWER) {
|
||||||
|
// Don't expect this answer now
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
|
||||||
|
}
|
||||||
|
memcpy(this->active_alarms_, data + 2, 8);
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return haier_protocol::HandlerError::HANDLER_OK;
|
||||||
|
} else {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
return haier_protocol::HandlerError::UNSUPORTED_MESSAGE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::set_answers_handlers() {
|
||||||
|
// Set handlers
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::GET_DEVICE_VERSION),
|
||||||
|
std::bind(&HonClimate::get_device_version_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::GET_DEVICE_ID),
|
||||||
|
std::bind(&HonClimate::get_device_id_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::CONTROL),
|
||||||
|
std::bind(&HonClimate::status_handler_, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3,
|
||||||
|
std::placeholders::_4));
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION),
|
||||||
|
std::bind(&HonClimate::get_management_information_answer_handler_, this, std::placeholders::_1,
|
||||||
|
std::placeholders::_2, std::placeholders::_3, std::placeholders::_4));
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::GET_ALARM_STATUS),
|
||||||
|
std::bind(&HonClimate::get_alarm_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (hon_protocol::FrameType::REPORT_NETWORK_STATUS),
|
||||||
|
std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::dump_config() {
|
||||||
|
HaierClimateBase::dump_config();
|
||||||
|
ESP_LOGCONFIG(TAG, " Protocol version: hOn");
|
||||||
|
if (this->hvac_hardware_info_available_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Device protocol version: %s", this->hvac_protocol_version_.c_str());
|
||||||
|
ESP_LOGCONFIG(TAG, " Device software version: %s", this->hvac_software_version_.c_str());
|
||||||
|
ESP_LOGCONFIG(TAG, " Device hardware version: %s", this->hvac_hardware_version_.c_str());
|
||||||
|
ESP_LOGCONFIG(TAG, " Device name: %s", this->hvac_device_name_.c_str());
|
||||||
|
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s", (this->hvac_functions_[0] ? " interactive" : ""),
|
||||||
|
(this->hvac_functions_[1] ? " controller-device" : ""), (this->hvac_functions_[2] ? " crc" : ""),
|
||||||
|
(this->hvac_functions_[3] ? " multinode" : ""), (this->hvac_functions_[4] ? " role" : ""));
|
||||||
|
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
|
||||||
|
switch (this->protocol_phase_) {
|
||||||
|
case ProtocolPhases::SENDING_INIT_1:
|
||||||
|
if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) {
|
||||||
|
this->hvac_hardware_info_available_ = false;
|
||||||
|
// Indicate device capabilities:
|
||||||
|
// bit 0 - if 1 module support interactive mode
|
||||||
|
// bit 1 - if 1 module support controller-device mode
|
||||||
|
// bit 2 - if 1 module support crc
|
||||||
|
// bit 3 - if 1 module support multiple devices
|
||||||
|
// bit 4..bit 15 - not used
|
||||||
|
uint8_t module_capabilities[2] = {0b00000000, 0b00000111};
|
||||||
|
static const haier_protocol::HaierMessage DEVICE_VERSION_REQUEST(
|
||||||
|
(uint8_t) hon_protocol::FrameType::GET_DEVICE_VERSION, module_capabilities, sizeof(module_capabilities));
|
||||||
|
this->send_message_(DEVICE_VERSION_REQUEST, this->use_crc_);
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_INIT_2:
|
||||||
|
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);
|
||||||
|
this->send_message_(DEVICEID_REQUEST, this->use_crc_);
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_ANSWER_INIT_2);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
|
||||||
|
case ProtocolPhases::SENDING_STATUS_REQUEST:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage STATUS_REQUEST(
|
||||||
|
(uint8_t) hon_protocol::FrameType::CONTROL, (uint16_t) hon_protocol::SubcomandsControl::GET_USER_DATA);
|
||||||
|
this->send_message_(STATUS_REQUEST, this->use_crc_);
|
||||||
|
this->last_status_request_ = now;
|
||||||
|
this->set_phase_((ProtocolPhases) ((uint8_t) this->protocol_phase_ + 1));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
#ifdef USE_WIFI
|
||||||
|
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage UPDATE_SIGNAL_REQUEST(
|
||||||
|
(uint8_t) hon_protocol::FrameType::GET_MANAGEMENT_INFORMATION);
|
||||||
|
this->send_message_(UPDATE_SIGNAL_REQUEST, this->use_crc_);
|
||||||
|
this->last_signal_request_ = now;
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static uint8_t wifi_status_data[4] = {0x00, 0x00, 0x00, 0x00};
|
||||||
|
if (wifi::global_wifi_component->is_connected()) {
|
||||||
|
wifi_status_data[1] = 0;
|
||||||
|
int8_t rssi = wifi::global_wifi_component->wifi_rssi();
|
||||||
|
wifi_status_data[3] = uint8_t((128 + rssi) / 1.28f);
|
||||||
|
ESP_LOGD(TAG, "WiFi signal is: %ddBm => %d%%", rssi, wifi_status_data[3]);
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "WiFi is not connected");
|
||||||
|
wifi_status_data[1] = 1;
|
||||||
|
wifi_status_data[3] = 0;
|
||||||
|
}
|
||||||
|
haier_protocol::HaierMessage wifi_status_request((uint8_t) hon_protocol::FrameType::REPORT_NETWORK_STATUS,
|
||||||
|
wifi_status_data, sizeof(wifi_status_data));
|
||||||
|
this->send_message_(wifi_status_request, this->use_crc_);
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
|
||||||
|
break;
|
||||||
|
#else
|
||||||
|
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
|
||||||
|
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
|
||||||
|
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
|
||||||
|
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(
|
||||||
|
(uint8_t) hon_protocol::FrameType::GET_ALARM_STATUS);
|
||||||
|
this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_ALARM_STATUS_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_CONTROL:
|
||||||
|
if (this->first_control_attempt_) {
|
||||||
|
this->control_request_timestamp_ = now;
|
||||||
|
this->first_control_attempt_ = false;
|
||||||
|
}
|
||||||
|
if (this->is_control_message_timeout_exceeded_(now)) {
|
||||||
|
ESP_LOGW(TAG, "Sending control packet timeout!");
|
||||||
|
this->set_force_send_control_(false);
|
||||||
|
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)) {
|
||||||
|
haier_protocol::HaierMessage control_message = get_control_message();
|
||||||
|
this->send_message_(control_message, this->use_crc_);
|
||||||
|
ESP_LOGI(TAG, "Control packet sent");
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_CONTROL_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_POWER_ON_COMMAND:
|
||||||
|
case ProtocolPhases::SENDING_POWER_OFF_COMMAND:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
uint8_t pwr_cmd_buf[2] = {0x00, 0x00};
|
||||||
|
if (this->protocol_phase_ == ProtocolPhases::SENDING_POWER_ON_COMMAND)
|
||||||
|
pwr_cmd_buf[1] = 0x01;
|
||||||
|
haier_protocol::HaierMessage power_cmd((uint8_t) hon_protocol::FrameType::CONTROL,
|
||||||
|
((uint16_t) hon_protocol::SubcomandsControl::SET_SINGLE_PARAMETER) + 1,
|
||||||
|
pwr_cmd_buf, sizeof(pwr_cmd_buf));
|
||||||
|
this->send_message_(power_cmd, this->use_crc_);
|
||||||
|
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_ANSWER_INIT_1:
|
||||||
|
case ProtocolPhases::WAITING_ANSWER_INIT_2:
|
||||||
|
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: {
|
||||||
|
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST);
|
||||||
|
this->forced_request_status_ = false;
|
||||||
|
}
|
||||||
|
#ifdef USE_WIFI
|
||||||
|
else if (this->send_wifi_signal_ &&
|
||||||
|
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
|
||||||
|
SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
|
||||||
|
#endif
|
||||||
|
} break;
|
||||||
|
default:
|
||||||
|
// Shouldn't get here
|
||||||
|
#if (HAIER_LOG_LEVEL > 4)
|
||||||
|
ESP_LOGE(TAG, "Wrong protocol handler state: %s (%d), resetting communication",
|
||||||
|
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);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HaierMessage HonClimate::get_control_message() {
|
||||||
|
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;
|
||||||
|
bool has_hvac_settings = false;
|
||||||
|
if (this->hvac_settings_.valid) {
|
||||||
|
has_hvac_settings = true;
|
||||||
|
HvacSettings climate_control;
|
||||||
|
climate_control = this->hvac_settings_;
|
||||||
|
if (climate_control.mode.has_value()) {
|
||||||
|
switch (climate_control.mode.value()) {
|
||||||
|
case CLIMATE_MODE_OFF:
|
||||||
|
out_data->ac_power = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_MODE_AUTO:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::AUTO;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
case CLIMATE_MODE_HEAT:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::HEAT;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
case CLIMATE_MODE_DRY:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::DRY;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
case CLIMATE_MODE_FAN_ONLY:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::FAN;
|
||||||
|
out_data->fan_mode = this->fan_mode_speed_; // Auto doesn't work in fan only mode
|
||||||
|
// Disabling boost and eco mode for Fan only
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_MODE_COOL:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) hon_protocol::ConditioningMode::COOL;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported climate mode");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set fan speed, if we are in fan mode, reject auto in fan mode
|
||||||
|
if (climate_control.fan_mode.has_value()) {
|
||||||
|
switch (climate_control.fan_mode.value()) {
|
||||||
|
case CLIMATE_FAN_LOW:
|
||||||
|
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_LOW;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_MEDIUM:
|
||||||
|
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_MID;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_HIGH:
|
||||||
|
out_data->fan_mode = (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
|
||||||
|
out_data->fan_mode = (uint8_t) hon_protocol::FanMode::FAN_AUTO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported fan mode");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set swing mode
|
||||||
|
if (climate_control.swing_mode.has_value()) {
|
||||||
|
switch (climate_control.swing_mode.value()) {
|
||||||
|
case CLIMATE_SWING_OFF:
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_VERTICAL:
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_HORIZONTAL:
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_BOTH:
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) hon_protocol::HorizontalSwingMode::AUTO;
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) hon_protocol::VerticalSwingMode::AUTO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (climate_control.target_temperature.has_value()) {
|
||||||
|
out_data->set_point =
|
||||||
|
climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16.
|
||||||
|
}
|
||||||
|
if (out_data->ac_power == 0) {
|
||||||
|
// If AC is off - no presets alowed
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
out_data->sleep_mode = 0;
|
||||||
|
} else if (climate_control.preset.has_value()) {
|
||||||
|
switch (climate_control.preset.value()) {
|
||||||
|
case CLIMATE_PRESET_NONE:
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
out_data->sleep_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_ECO:
|
||||||
|
// Eco is not supported in Fan only mode
|
||||||
|
out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
out_data->sleep_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_BOOST:
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
// Boost is not supported in Fan only mode
|
||||||
|
out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
|
||||||
|
out_data->sleep_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_AWAY:
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
out_data->sleep_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_SLEEP:
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
out_data->fast_mode = 0;
|
||||||
|
out_data->sleep_mode = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported preset");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (out_data->vertical_swing_mode != (uint8_t) hon_protocol::VerticalSwingMode::AUTO)
|
||||||
|
out_data->vertical_swing_mode = (uint8_t) get_vertical_swing_mode(this->vertical_direction_);
|
||||||
|
if (out_data->horizontal_swing_mode != (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)
|
||||||
|
out_data->horizontal_swing_mode = (uint8_t) get_horizontal_swing_mode(this->horizontal_direction_);
|
||||||
|
}
|
||||||
|
out_data->beeper_status = ((!this->beeper_status_) || (!has_hvac_settings)) ? 1 : 0;
|
||||||
|
control_out_buffer[4] = 0; // This byte should be cleared before setting values
|
||||||
|
out_data->display_status = this->display_status_ ? 1 : 0;
|
||||||
|
out_data->health_mode = this->health_mode_ ? 1 : 0;
|
||||||
|
switch (this->action_request_) {
|
||||||
|
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::SubcomandsControl::SET_GROUP_PARAMETERS,
|
||||||
|
control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
|
||||||
|
if (size < sizeof(hon_protocol::HaierStatus))
|
||||||
|
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||||
|
hon_protocol::HaierStatus packet;
|
||||||
|
if (size < sizeof(hon_protocol::HaierStatus))
|
||||||
|
size = sizeof(hon_protocol::HaierStatus);
|
||||||
|
memcpy(&packet, packet_buffer, size);
|
||||||
|
if (packet.sensors.error_status != 0) {
|
||||||
|
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))) {
|
||||||
|
got_valid_outdoor_temp_ = true;
|
||||||
|
float otemp = (float) (packet.sensors.outdoor_temperature + PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET);
|
||||||
|
if ((!this->outdoor_sensor_->has_state()) || (this->outdoor_sensor_->get_raw_state() != otemp))
|
||||||
|
this->outdoor_sensor_->publish_state(otemp);
|
||||||
|
}
|
||||||
|
bool should_publish = false;
|
||||||
|
{
|
||||||
|
// Extra modes/presets
|
||||||
|
optional<ClimatePreset> old_preset = this->preset;
|
||||||
|
if (packet.control.quiet_mode != 0) {
|
||||||
|
this->preset = CLIMATE_PRESET_ECO;
|
||||||
|
} else if (packet.control.fast_mode != 0) {
|
||||||
|
this->preset = CLIMATE_PRESET_BOOST;
|
||||||
|
} else if (packet.control.sleep_mode != 0) {
|
||||||
|
this->preset = CLIMATE_PRESET_SLEEP;
|
||||||
|
} else {
|
||||||
|
this->preset = CLIMATE_PRESET_NONE;
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Target temperature
|
||||||
|
float old_target_temperature = this->target_temperature;
|
||||||
|
this->target_temperature = packet.control.set_point + 16.0f;
|
||||||
|
should_publish = should_publish || (old_target_temperature != this->target_temperature);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Current temperature
|
||||||
|
float old_current_temperature = this->current_temperature;
|
||||||
|
this->current_temperature = packet.sensors.room_temperature / 2.0f;
|
||||||
|
should_publish = should_publish || (old_current_temperature != this->current_temperature);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Fan mode
|
||||||
|
optional<ClimateFanMode> old_fan_mode = this->fan_mode;
|
||||||
|
// remember the fan speed we last had for climate vs fan
|
||||||
|
if (packet.control.ac_mode == (uint8_t) hon_protocol::ConditioningMode::FAN) {
|
||||||
|
if (packet.control.fan_mode != (uint8_t) hon_protocol::FanMode::FAN_AUTO)
|
||||||
|
this->fan_mode_speed_ = packet.control.fan_mode;
|
||||||
|
} else {
|
||||||
|
this->other_modes_fan_speed_ = packet.control.fan_mode;
|
||||||
|
}
|
||||||
|
switch (packet.control.fan_mode) {
|
||||||
|
case (uint8_t) hon_protocol::FanMode::FAN_AUTO:
|
||||||
|
if (packet.control.ac_mode != (uint8_t) hon_protocol::ConditioningMode::FAN) {
|
||||||
|
this->fan_mode = CLIMATE_FAN_AUTO;
|
||||||
|
} else {
|
||||||
|
// Shouldn't accept fan speed auto in fan-only mode even if AC reports it
|
||||||
|
ESP_LOGI(TAG, "Fan speed Auto is not supported in Fan only AC mode, ignoring");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::FanMode::FAN_MID:
|
||||||
|
this->fan_mode = CLIMATE_FAN_MEDIUM;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::FanMode::FAN_LOW:
|
||||||
|
this->fan_mode = CLIMATE_FAN_LOW;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::FanMode::FAN_HIGH:
|
||||||
|
this->fan_mode = CLIMATE_FAN_HIGH;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Display status
|
||||||
|
// should be before "Climate mode" because it is changing this->mode
|
||||||
|
if (packet.control.ac_power != 0) {
|
||||||
|
// if AC is off display status always ON so process it only when AC is on
|
||||||
|
bool disp_status = packet.control.display_status != 0;
|
||||||
|
if (disp_status != this->display_status_) {
|
||||||
|
// Do something only if display status changed
|
||||||
|
if (this->mode == CLIMATE_MODE_OFF) {
|
||||||
|
// AC just turned on from remote need to turn off display
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
} else {
|
||||||
|
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_);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
CleaningState new_cleaning;
|
||||||
|
if (packet.control.steri_clean == 1) {
|
||||||
|
// Steri-cleaning
|
||||||
|
new_cleaning = CleaningState::STERI_CLEAN;
|
||||||
|
} else if (packet.control.self_cleaning_status == 1) {
|
||||||
|
// Self-cleaning
|
||||||
|
new_cleaning = CleaningState::SELF_CLEAN;
|
||||||
|
} else {
|
||||||
|
// No cleaning
|
||||||
|
new_cleaning = CleaningState::NO_CLEANING;
|
||||||
|
}
|
||||||
|
if (new_cleaning != this->cleaning_status_) {
|
||||||
|
ESP_LOGD(TAG, "Cleaning status change: %d => %d", (uint8_t) this->cleaning_status_, (uint8_t) new_cleaning);
|
||||||
|
if (new_cleaning == CleaningState::NO_CLEANING) {
|
||||||
|
// Turnuin AC off after cleaning
|
||||||
|
this->action_request_ = ActionRequest::TURN_POWER_OFF;
|
||||||
|
}
|
||||||
|
this->cleaning_status_ = new_cleaning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Climate mode
|
||||||
|
ClimateMode old_mode = this->mode;
|
||||||
|
if (packet.control.ac_power == 0) {
|
||||||
|
this->mode = CLIMATE_MODE_OFF;
|
||||||
|
} else {
|
||||||
|
// Check current hvac mode
|
||||||
|
switch (packet.control.ac_mode) {
|
||||||
|
case (uint8_t) hon_protocol::ConditioningMode::COOL:
|
||||||
|
this->mode = CLIMATE_MODE_COOL;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::ConditioningMode::HEAT:
|
||||||
|
this->mode = CLIMATE_MODE_HEAT;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::ConditioningMode::DRY:
|
||||||
|
this->mode = CLIMATE_MODE_DRY;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::ConditioningMode::FAN:
|
||||||
|
this->mode = CLIMATE_MODE_FAN_ONLY;
|
||||||
|
break;
|
||||||
|
case (uint8_t) hon_protocol::ConditioningMode::AUTO:
|
||||||
|
this->mode = CLIMATE_MODE_AUTO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (old_mode != this->mode);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Swing mode
|
||||||
|
ClimateSwingMode old_swing_mode = this->swing_mode;
|
||||||
|
if (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO) {
|
||||||
|
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
|
||||||
|
this->swing_mode = CLIMATE_SWING_BOTH;
|
||||||
|
} else {
|
||||||
|
this->swing_mode = CLIMATE_SWING_HORIZONTAL;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (packet.control.vertical_swing_mode == (uint8_t) hon_protocol::VerticalSwingMode::AUTO) {
|
||||||
|
this->swing_mode = CLIMATE_SWING_VERTICAL;
|
||||||
|
} else {
|
||||||
|
this->swing_mode = CLIMATE_SWING_OFF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (old_swing_mode != this->swing_mode);
|
||||||
|
}
|
||||||
|
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
|
||||||
|
if (this->forced_publish_ || 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();
|
||||||
|
#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) {
|
||||||
|
ESP_LOGI(TAG, "HVAC values changed");
|
||||||
|
}
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), 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__,
|
||||||
|
"Fan speed Status = 0x%X", packet.control.fan_mode);
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), 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__,
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool HonClimate::is_message_invalid(uint8_t message_type) {
|
||||||
|
return message_type == (uint8_t) hon_protocol::FrameType::INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
void HonClimate::process_pending_action() {
|
||||||
|
switch (this->action_request_) {
|
||||||
|
case ActionRequest::START_SELF_CLEAN:
|
||||||
|
case ActionRequest::START_STERI_CLEAN:
|
||||||
|
// Will reset action with control message sending
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_CONTROL);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
HaierClimateBase::process_pending_action();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
95
esphome/components/haier/hon_climate.h
Normal file
95
esphome/components/haier/hon_climate.h
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include "haier_base.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
enum class AirflowVerticalDirection : uint8_t {
|
||||||
|
HEALTH_UP = 0,
|
||||||
|
MAX_UP = 1,
|
||||||
|
UP = 2,
|
||||||
|
CENTER = 3,
|
||||||
|
DOWN = 4,
|
||||||
|
HEALTH_DOWN = 5,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class AirflowHorizontalDirection : uint8_t {
|
||||||
|
MAX_LEFT = 0,
|
||||||
|
LEFT = 1,
|
||||||
|
CENTER = 2,
|
||||||
|
RIGHT = 3,
|
||||||
|
MAX_RIGHT = 4,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class CleaningState : uint8_t {
|
||||||
|
NO_CLEANING = 0,
|
||||||
|
SELF_CLEAN = 1,
|
||||||
|
STERI_CLEAN = 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
class HonClimate : public HaierClimateBase {
|
||||||
|
public:
|
||||||
|
HonClimate();
|
||||||
|
HonClimate(const HonClimate &) = delete;
|
||||||
|
HonClimate &operator=(const HonClimate &) = delete;
|
||||||
|
~HonClimate();
|
||||||
|
void dump_config() override;
|
||||||
|
void set_beeper_state(bool state);
|
||||||
|
bool get_beeper_state() const;
|
||||||
|
void set_outdoor_temperature_sensor(esphome::sensor::Sensor *sensor);
|
||||||
|
AirflowVerticalDirection get_vertical_airflow() const;
|
||||||
|
void set_vertical_airflow(AirflowVerticalDirection direction);
|
||||||
|
AirflowHorizontalDirection get_horizontal_airflow() const;
|
||||||
|
void set_horizontal_airflow(AirflowHorizontalDirection direction);
|
||||||
|
std::string get_cleaning_status_text() const;
|
||||||
|
CleaningState get_cleaning_status() const;
|
||||||
|
void start_self_cleaning();
|
||||||
|
void start_steri_cleaning();
|
||||||
|
void set_send_wifi(bool send_wifi);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void set_answers_handlers() override;
|
||||||
|
void process_phase(std::chrono::steady_clock::time_point now) override;
|
||||||
|
haier_protocol::HaierMessage get_control_message() override;
|
||||||
|
bool is_message_invalid(uint8_t message_type) override;
|
||||||
|
void process_pending_action() override;
|
||||||
|
|
||||||
|
// Answers handlers
|
||||||
|
haier_protocol::HandlerError get_device_version_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size);
|
||||||
|
haier_protocol::HandlerError get_device_id_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
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,
|
||||||
|
size_t data_size);
|
||||||
|
haier_protocol::HandlerError get_management_information_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size);
|
||||||
|
haier_protocol::HandlerError report_network_status_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size);
|
||||||
|
haier_protocol::HandlerError get_alarm_status_answer_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size);
|
||||||
|
// Helper functions
|
||||||
|
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
|
||||||
|
std::unique_ptr<uint8_t[]> last_status_message_;
|
||||||
|
bool beeper_status_;
|
||||||
|
CleaningState cleaning_status_;
|
||||||
|
bool got_valid_outdoor_temp_;
|
||||||
|
AirflowVerticalDirection vertical_direction_;
|
||||||
|
AirflowHorizontalDirection horizontal_direction_;
|
||||||
|
bool hvac_hardware_info_available_;
|
||||||
|
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];
|
||||||
|
esphome::sensor::Sensor *outdoor_sensor_;
|
||||||
|
bool send_wifi_signal_;
|
||||||
|
std::chrono::steady_clock::time_point last_signal_request_; // To send WiFI signal level
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
228
esphome/components/haier/hon_packet.h
Normal file
228
esphome/components/haier/hon_packet.h
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
namespace hon_protocol {
|
||||||
|
|
||||||
|
enum class VerticalSwingMode : uint8_t {
|
||||||
|
HEALTH_UP = 0x01,
|
||||||
|
MAX_UP = 0x02,
|
||||||
|
HEALTH_DOWN = 0x03,
|
||||||
|
UP = 0x04,
|
||||||
|
CENTER = 0x06,
|
||||||
|
DOWN = 0x08,
|
||||||
|
AUTO = 0x0C
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class HorizontalSwingMode : uint8_t {
|
||||||
|
CENTER = 0x00,
|
||||||
|
MAX_LEFT = 0x03,
|
||||||
|
LEFT = 0x04,
|
||||||
|
RIGHT = 0x05,
|
||||||
|
MAX_RIGHT = 0x06,
|
||||||
|
AUTO = 0x07
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class ConditioningMode : uint8_t {
|
||||||
|
AUTO = 0x00,
|
||||||
|
COOL = 0x01,
|
||||||
|
DRY = 0x02,
|
||||||
|
HEALTHY_DRY = 0x03,
|
||||||
|
HEAT = 0x04,
|
||||||
|
ENERGY_SAVING = 0x05,
|
||||||
|
FAN = 0x06
|
||||||
|
};
|
||||||
|
|
||||||
|
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 };
|
||||||
|
|
||||||
|
struct HaierPacketControl {
|
||||||
|
// Control bytes starts here
|
||||||
|
// 10
|
||||||
|
uint8_t set_point; // Target temperature with 16°C offset (0x00 = 16°C)
|
||||||
|
// 11
|
||||||
|
uint8_t vertical_swing_mode : 4; // See enum VerticalSwingMode
|
||||||
|
uint8_t : 0;
|
||||||
|
// 12
|
||||||
|
uint8_t fan_mode : 3; // See enum FanMode
|
||||||
|
uint8_t special_mode : 2; // See enum SpecialMode
|
||||||
|
uint8_t ac_mode : 3; // See enum ConditioningMode
|
||||||
|
// 13
|
||||||
|
uint8_t : 8;
|
||||||
|
// 14
|
||||||
|
uint8_t ten_degree : 1; // 10 degree status
|
||||||
|
uint8_t display_status : 1; // If 0 disables AC's display
|
||||||
|
uint8_t half_degree : 1; // Use half degree
|
||||||
|
uint8_t intelegence_status : 1; // Intelligence status
|
||||||
|
uint8_t pmv_status : 1; // Comfort/PMV status
|
||||||
|
uint8_t use_fahrenheit : 1; // Use Fahrenheit instead of Celsius
|
||||||
|
uint8_t : 1;
|
||||||
|
uint8_t steri_clean : 1;
|
||||||
|
// 15
|
||||||
|
uint8_t ac_power : 1; // Is ac on or off
|
||||||
|
uint8_t health_mode : 1; // Health mode (negative ions) on or off
|
||||||
|
uint8_t electric_heating_status : 1; // Electric heating status
|
||||||
|
uint8_t fast_mode : 1; // Fast mode
|
||||||
|
uint8_t quiet_mode : 1; // Quiet mode
|
||||||
|
uint8_t sleep_mode : 1; // Sleep mode
|
||||||
|
uint8_t lock_remote : 1; // Disable remote
|
||||||
|
uint8_t beeper_status : 1; // If 1 disables AC's command feedback beeper (need to be set on every control command)
|
||||||
|
// 16
|
||||||
|
uint8_t target_humidity; // Target humidity (0=30% .. 3C=90%, step = 1%)
|
||||||
|
// 17
|
||||||
|
uint8_t horizontal_swing_mode : 3; // See enum HorizontalSwingMode
|
||||||
|
uint8_t : 3;
|
||||||
|
uint8_t human_sensing_status : 2; // Human sensing status
|
||||||
|
// 18
|
||||||
|
uint8_t change_filter : 1; // Filter need replacement
|
||||||
|
uint8_t : 0;
|
||||||
|
// 19
|
||||||
|
uint8_t fresh_air_status : 1; // Fresh air status
|
||||||
|
uint8_t humidification_status : 1; // Humidification status
|
||||||
|
uint8_t pm2p5_cleaning_status : 1; // PM2.5 cleaning status
|
||||||
|
uint8_t ch2o_cleaning_status : 1; // CH2O cleaning status
|
||||||
|
uint8_t self_cleaning_status : 1; // Self cleaning status
|
||||||
|
uint8_t light_status : 1; // Light status
|
||||||
|
uint8_t energy_saving_status : 1; // Energy saving status
|
||||||
|
uint8_t cleaning_time_status : 1; // Cleaning time (0 - accumulation, 1 - clear)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HaierPacketSensors {
|
||||||
|
// 20
|
||||||
|
uint8_t room_temperature; // 0.5°C step
|
||||||
|
// 21
|
||||||
|
uint8_t room_humidity; // 0%-100% with 1% step
|
||||||
|
// 22
|
||||||
|
uint8_t outdoor_temperature; // 1°C step, -64°C offset (0=-64°C)
|
||||||
|
// 23
|
||||||
|
uint8_t pm2p5_level : 2; // Indoor PM2.5 grade (00: Excellent, 01: good, 02: Medium, 03: Bad)
|
||||||
|
uint8_t air_quality : 2; // Air quality grade (00: Excellent, 01: good, 02: Medium, 03: Bad)
|
||||||
|
uint8_t human_sensing : 2; // Human presence result (00: N/A, 01: not detected, 02: One, 03: Multiple)
|
||||||
|
uint8_t : 1;
|
||||||
|
uint8_t ac_type : 1; // 00 - Heat and cool, 01 - Cool only)
|
||||||
|
// 24
|
||||||
|
uint8_t error_status; // See enum ErrorStatus
|
||||||
|
// 25
|
||||||
|
uint8_t operation_source : 2; // who is controlling AC (00: Other, 01: Remote control, 02: Button, 03: ESP)
|
||||||
|
uint8_t operation_mode_hk : 2; // Homekit only, operation mode (00: Cool, 01: Dry, 02: Heat, 03: Fan)
|
||||||
|
uint8_t : 3;
|
||||||
|
uint8_t err_confirmation : 1; // If 1 clear error status
|
||||||
|
// 26
|
||||||
|
uint16_t total_cleaning_time; // Cleaning cumulative time (1h step)
|
||||||
|
// 28
|
||||||
|
uint16_t indoor_pm2p5_value; // Indoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step)
|
||||||
|
// 30
|
||||||
|
uint16_t outdoor_pm2p5_value; // Outdoor PM2.5 value (0 ug/m3 - 4095 ug/m3, 1 ug/m3 step)
|
||||||
|
// 32
|
||||||
|
uint16_t ch2o_value; // Formaldehyde value (0 ug/m3 - 10000 ug/m3, 1 ug/m3 step)
|
||||||
|
// 34
|
||||||
|
uint16_t voc_value; // VOC value (Volatile Organic Compounds) (0 ug/m3 - 1023 ug/m3, 1 ug/m3 step)
|
||||||
|
// 36
|
||||||
|
uint16_t co2_value; // CO2 value (0 PPM - 10000 PPM, 1 PPM step)
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HaierStatus {
|
||||||
|
uint16_t subcommand;
|
||||||
|
HaierPacketControl control;
|
||||||
|
HaierPacketSensors sensors;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct DeviceVersionAnswer {
|
||||||
|
char protocol_version[8];
|
||||||
|
char software_version[8];
|
||||||
|
uint8_t encryption[3];
|
||||||
|
char hardware_version[8];
|
||||||
|
uint8_t : 8;
|
||||||
|
char device_name[8];
|
||||||
|
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_DOWNLIK = 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 SubcomandsControl : uint16_t {
|
||||||
|
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_BIG_DATA = 0x4DFE, // Request big data information from device (packet content: None)
|
||||||
|
SET_PARAMETERS = 0x5C01, // Set parameters of the device and device return parameters (packet content: parameter ID1
|
||||||
|
// + parameter data1 + parameter ID2 + parameter data 2 + ...)
|
||||||
|
SET_SINGLE_PARAMETER = 0x5D00, // Set single parameter (0x5DXX second byte define parameter ID) and return all user
|
||||||
|
// data (packet content: ???)
|
||||||
|
SET_GROUP_PARAMETERS = 0x6001, // Set group parameters to device (0x60XX second byte define parameter is group ID,
|
||||||
|
// the only group mentioned in document is 1) and return all user data (packet
|
||||||
|
// content: all values like in status packet)
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace hon_protocol
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
33
esphome/components/haier/logger_handler.cpp
Normal file
33
esphome/components/haier/logger_handler.cpp
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
#include "logger_handler.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
void esphome_logger(haier_protocol::HaierLogLevel level, const char *tag, const char *message) {
|
||||||
|
switch (level) {
|
||||||
|
case haier_protocol::HaierLogLevel::LEVEL_ERROR:
|
||||||
|
esp_log_printf_(ESPHOME_LOG_LEVEL_ERROR, tag, __LINE__, "%s", message);
|
||||||
|
break;
|
||||||
|
case haier_protocol::HaierLogLevel::LEVEL_WARNING:
|
||||||
|
esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, __LINE__, "%s", message);
|
||||||
|
break;
|
||||||
|
case haier_protocol::HaierLogLevel::LEVEL_INFO:
|
||||||
|
esp_log_printf_(ESPHOME_LOG_LEVEL_INFO, tag, __LINE__, "%s", message);
|
||||||
|
break;
|
||||||
|
case haier_protocol::HaierLogLevel::LEVEL_DEBUG:
|
||||||
|
esp_log_printf_(ESPHOME_LOG_LEVEL_DEBUG, tag, __LINE__, "%s", message);
|
||||||
|
break;
|
||||||
|
case haier_protocol::HaierLogLevel::LEVEL_VERBOSE:
|
||||||
|
esp_log_printf_(ESPHOME_LOG_LEVEL_VERBOSE, tag, __LINE__, "%s", message);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Just ignore everything else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_haier_protocol_logging() { haier_protocol::set_log_handler(esphome::haier::esphome_logger); };
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
14
esphome/components/haier/logger_handler.h
Normal file
14
esphome/components/haier/logger_handler.h
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
// HaierProtocol
|
||||||
|
#include <utils/haier_log.h>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
// This file is called in the code generated by python script
|
||||||
|
// Do not use it directly!
|
||||||
|
void init_haier_protocol_logging();
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
457
esphome/components/haier/smartair2_climate.cpp
Normal file
457
esphome/components/haier/smartair2_climate.cpp
Normal file
|
@ -0,0 +1,457 @@
|
||||||
|
#include <chrono>
|
||||||
|
#include "esphome/components/climate/climate.h"
|
||||||
|
#include "esphome/components/uart/uart.h"
|
||||||
|
#include "smartair2_climate.h"
|
||||||
|
#include "smartair2_packet.h"
|
||||||
|
|
||||||
|
using namespace esphome::climate;
|
||||||
|
using namespace esphome::uart;
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
static const char *const TAG = "haier.climate";
|
||||||
|
|
||||||
|
Smartair2Climate::Smartair2Climate()
|
||||||
|
: last_status_message_(new uint8_t[sizeof(smartair2_protocol::HaierPacketControl)]) {
|
||||||
|
this->traits_.set_supported_presets({
|
||||||
|
climate::CLIMATE_PRESET_NONE,
|
||||||
|
climate::CLIMATE_PRESET_BOOST,
|
||||||
|
climate::CLIMATE_PRESET_COMFORT,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError Smartair2Climate::status_handler_(uint8_t request_type, uint8_t message_type,
|
||||||
|
const uint8_t *data, size_t data_size) {
|
||||||
|
haier_protocol::HandlerError result =
|
||||||
|
this->answer_preprocess_(request_type, (uint8_t) smartair2_protocol::FrameType::CONTROL, message_type,
|
||||||
|
(uint8_t) smartair2_protocol::FrameType::STATUS, ProtocolPhases::UNKNOWN);
|
||||||
|
if (result == haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
result = this->process_status_message_(data, data_size);
|
||||||
|
if (result != haier_protocol::HandlerError::HANDLER_OK) {
|
||||||
|
ESP_LOGW(TAG, "Error %d while parsing Status packet", (int) result);
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||||
|
} else {
|
||||||
|
if (data_size >= sizeof(smartair2_protocol::HaierPacketControl) + 2) {
|
||||||
|
memcpy(this->last_status_message_.get(), data + 2, sizeof(smartair2_protocol::HaierPacketControl));
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size,
|
||||||
|
sizeof(smartair2_protocol::HaierPacketControl));
|
||||||
|
}
|
||||||
|
if (this->protocol_phase_ == ProtocolPhases::WAITING_FIRST_STATUS_ANSWER) {
|
||||||
|
ESP_LOGI(TAG, "First HVAC status received");
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_STATUS_ANSWER) {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
} else if (this->protocol_phase_ == ProtocolPhases::WAITING_CONTROL_ANSWER) {
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
this->set_force_send_control_(false);
|
||||||
|
if (this->hvac_settings_.valid)
|
||||||
|
this->hvac_settings_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} else {
|
||||||
|
this->set_phase_((this->protocol_phase_ >= ProtocolPhases::IDLE) ? ProtocolPhases::IDLE
|
||||||
|
: ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Smartair2Climate::set_answers_handlers() {
|
||||||
|
this->haier_protocol_.set_answer_handler(
|
||||||
|
(uint8_t) (smartair2_protocol::FrameType::CONTROL),
|
||||||
|
std::bind(&Smartair2Climate::status_handler_, this, std::placeholders::_1, std::placeholders::_2,
|
||||||
|
std::placeholders::_3, std::placeholders::_4));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Smartair2Climate::dump_config() {
|
||||||
|
HaierClimateBase::dump_config();
|
||||||
|
ESP_LOGCONFIG(TAG, " Protocol version: smartAir2");
|
||||||
|
}
|
||||||
|
|
||||||
|
void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now) {
|
||||||
|
switch (this->protocol_phase_) {
|
||||||
|
case ProtocolPhases::SENDING_INIT_1:
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::WAITING_ANSWER_INIT_1:
|
||||||
|
case ProtocolPhases::SENDING_INIT_2:
|
||||||
|
case ProtocolPhases::WAITING_ANSWER_INIT_2:
|
||||||
|
case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
|
||||||
|
case ProtocolPhases::WAITING_ALARM_STATUS_ANSWER:
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_INIT_1);
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
|
||||||
|
case ProtocolPhases::WAITING_UPDATE_SIGNAL_ANSWER:
|
||||||
|
case ProtocolPhases::SENDING_SIGNAL_LEVEL:
|
||||||
|
case ProtocolPhases::WAITING_SIGNAL_LEVEL_ANSWER:
|
||||||
|
this->set_phase_(ProtocolPhases::IDLE);
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
|
||||||
|
if (this->can_send_message() && this->is_protocol_initialisation_interval_exceded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL,
|
||||||
|
0x4D01);
|
||||||
|
this->send_message_(STATUS_REQUEST, false);
|
||||||
|
this->last_status_request_ = now;
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_FIRST_STATUS_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_STATUS_REQUEST:
|
||||||
|
if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
|
||||||
|
static const haier_protocol::HaierMessage STATUS_REQUEST((uint8_t) smartair2_protocol::FrameType::CONTROL,
|
||||||
|
0x4D01);
|
||||||
|
this->send_message_(STATUS_REQUEST, false);
|
||||||
|
this->last_status_request_ = now;
|
||||||
|
this->set_phase_(ProtocolPhases::WAITING_STATUS_ANSWER);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case ProtocolPhases::SENDING_CONTROL:
|
||||||
|
if (this->first_control_attempt_) {
|
||||||
|
this->control_request_timestamp_ = now;
|
||||||
|
this->first_control_attempt_ = false;
|
||||||
|
}
|
||||||
|
if (this->is_control_message_timeout_exceeded_(now)) {
|
||||||
|
ESP_LOGW(TAG, "Sending control packet timeout!");
|
||||||
|
this->set_force_send_control_(false);
|
||||||
|
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)) // 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;
|
||||||
|
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_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: {
|
||||||
|
if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_STATUS_REQUEST);
|
||||||
|
this->forced_request_status_ = false;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
default:
|
||||||
|
// Shouldn't get here
|
||||||
|
ESP_LOGE(TAG, "Wrong protocol handler state: %d, resetting communication", (int) this->protocol_phase_);
|
||||||
|
this->set_phase_(ProtocolPhases::SENDING_FIRST_STATUS_REQUEST);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
|
||||||
|
uint8_t control_out_buffer[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;
|
||||||
|
out_data->cntrl = 0;
|
||||||
|
if (this->hvac_settings_.valid) {
|
||||||
|
HvacSettings climate_control;
|
||||||
|
climate_control = this->hvac_settings_;
|
||||||
|
if (climate_control.mode.has_value()) {
|
||||||
|
switch (climate_control.mode.value()) {
|
||||||
|
case CLIMATE_MODE_OFF:
|
||||||
|
out_data->ac_power = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIMATE_MODE_AUTO:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::AUTO;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIMATE_MODE_HEAT:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::HEAT;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIMATE_MODE_DRY:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::DRY;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIMATE_MODE_FAN_ONLY:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
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
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CLIMATE_MODE_COOL:
|
||||||
|
out_data->ac_power = 1;
|
||||||
|
out_data->ac_mode = (uint8_t) smartair2_protocol::ConditioningMode::COOL;
|
||||||
|
out_data->fan_mode = this->other_modes_fan_speed_;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported climate mode");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set fan speed, if we are in fan mode, reject auto in fan mode
|
||||||
|
if (climate_control.fan_mode.has_value()) {
|
||||||
|
switch (climate_control.fan_mode.value()) {
|
||||||
|
case CLIMATE_FAN_LOW:
|
||||||
|
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_LOW;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_MEDIUM:
|
||||||
|
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_MID;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_HIGH:
|
||||||
|
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_HIGH;
|
||||||
|
break;
|
||||||
|
case CLIMATE_FAN_AUTO:
|
||||||
|
if (this->mode != CLIMATE_MODE_FAN_ONLY) // if we are not in fan only mode
|
||||||
|
out_data->fan_mode = (uint8_t) smartair2_protocol::FanMode::FAN_AUTO;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported fan mode");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Set swing mode
|
||||||
|
if (climate_control.swing_mode.has_value()) {
|
||||||
|
switch (climate_control.swing_mode.value()) {
|
||||||
|
case CLIMATE_SWING_OFF:
|
||||||
|
out_data->use_swing_bits = 0;
|
||||||
|
out_data->swing_both = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_VERTICAL:
|
||||||
|
out_data->swing_both = 0;
|
||||||
|
out_data->vertical_swing = 1;
|
||||||
|
out_data->horizontal_swing = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_HORIZONTAL:
|
||||||
|
out_data->swing_both = 0;
|
||||||
|
out_data->vertical_swing = 0;
|
||||||
|
out_data->horizontal_swing = 1;
|
||||||
|
break;
|
||||||
|
case CLIMATE_SWING_BOTH:
|
||||||
|
out_data->swing_both = 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()) {
|
||||||
|
out_data->set_point =
|
||||||
|
climate_control.target_temperature.value() - 16; // set the temperature at our offset, subtract 16.
|
||||||
|
}
|
||||||
|
if (out_data->ac_power == 0) {
|
||||||
|
// If AC is off - no presets alowed
|
||||||
|
out_data->turbo_mode = 0;
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
} else if (climate_control.preset.has_value()) {
|
||||||
|
switch (climate_control.preset.value()) {
|
||||||
|
case CLIMATE_PRESET_NONE:
|
||||||
|
out_data->turbo_mode = 0;
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_BOOST:
|
||||||
|
out_data->turbo_mode = 1;
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
break;
|
||||||
|
case CLIMATE_PRESET_COMFORT:
|
||||||
|
out_data->turbo_mode = 0;
|
||||||
|
out_data->quiet_mode = 1;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGE("Control", "Unsupported preset");
|
||||||
|
out_data->turbo_mode = 0;
|
||||||
|
out_data->quiet_mode = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out_data->display_status = this->display_status_ ? 0 : 1;
|
||||||
|
out_data->health_mode = this->health_mode_ ? 1 : 0;
|
||||||
|
return haier_protocol::HaierMessage((uint8_t) smartair2_protocol::FrameType::CONTROL, 0x4D5F, control_out_buffer,
|
||||||
|
sizeof(smartair2_protocol::HaierPacketControl));
|
||||||
|
}
|
||||||
|
|
||||||
|
haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
|
||||||
|
if (size < sizeof(smartair2_protocol::HaierStatus))
|
||||||
|
return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
|
||||||
|
smartair2_protocol::HaierStatus packet;
|
||||||
|
memcpy(&packet, packet_buffer, size);
|
||||||
|
bool should_publish = false;
|
||||||
|
{
|
||||||
|
// Extra modes/presets
|
||||||
|
optional<ClimatePreset> old_preset = this->preset;
|
||||||
|
if (packet.control.turbo_mode != 0) {
|
||||||
|
this->preset = CLIMATE_PRESET_BOOST;
|
||||||
|
} else if (packet.control.quiet_mode != 0) {
|
||||||
|
this->preset = CLIMATE_PRESET_COMFORT;
|
||||||
|
} else {
|
||||||
|
this->preset = CLIMATE_PRESET_NONE;
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (!old_preset.has_value()) || (old_preset.value() != this->preset.value());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Target temperature
|
||||||
|
float old_target_temperature = this->target_temperature;
|
||||||
|
this->target_temperature = packet.control.set_point + 16.0f;
|
||||||
|
should_publish = should_publish || (old_target_temperature != this->target_temperature);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Current temperature
|
||||||
|
float old_current_temperature = this->current_temperature;
|
||||||
|
this->current_temperature = packet.control.room_temperature;
|
||||||
|
should_publish = should_publish || (old_current_temperature != this->current_temperature);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Fan mode
|
||||||
|
optional<ClimateFanMode> old_fan_mode = this->fan_mode;
|
||||||
|
// remember the fan speed we last had for climate vs fan
|
||||||
|
if (packet.control.ac_mode == (uint8_t) smartair2_protocol::ConditioningMode::FAN) {
|
||||||
|
if (packet.control.fan_mode != (uint8_t) smartair2_protocol::FanMode::FAN_AUTO)
|
||||||
|
this->fan_mode_speed_ = packet.control.fan_mode;
|
||||||
|
} else {
|
||||||
|
this->other_modes_fan_speed_ = packet.control.fan_mode;
|
||||||
|
}
|
||||||
|
switch (packet.control.fan_mode) {
|
||||||
|
case (uint8_t) smartair2_protocol::FanMode::FAN_AUTO:
|
||||||
|
// Somtimes AC reports in fan only mode that fan speed is auto
|
||||||
|
// but never accept this value back
|
||||||
|
if (packet.control.ac_mode != (uint8_t) smartair2_protocol::ConditioningMode::FAN) {
|
||||||
|
this->fan_mode = CLIMATE_FAN_AUTO;
|
||||||
|
} else {
|
||||||
|
should_publish = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::FanMode::FAN_MID:
|
||||||
|
this->fan_mode = CLIMATE_FAN_MEDIUM;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::FanMode::FAN_LOW:
|
||||||
|
this->fan_mode = CLIMATE_FAN_LOW;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::FanMode::FAN_HIGH:
|
||||||
|
this->fan_mode = CLIMATE_FAN_HIGH;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
should_publish = should_publish || (!old_fan_mode.has_value()) || (old_fan_mode.value() != fan_mode.value());
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Display status
|
||||||
|
// should be before "Climate mode" because it is changing this->mode
|
||||||
|
if (packet.control.ac_power != 0) {
|
||||||
|
// if AC is off display status always ON so process it only when AC is on
|
||||||
|
bool disp_status = packet.control.display_status == 0;
|
||||||
|
if (disp_status != this->display_status_) {
|
||||||
|
// Do something only if display status changed
|
||||||
|
if (this->mode == CLIMATE_MODE_OFF) {
|
||||||
|
// AC just turned on from remote need to turn off display
|
||||||
|
this->set_force_send_control_(true);
|
||||||
|
} else {
|
||||||
|
this->display_status_ = disp_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
{
|
||||||
|
// Climate mode
|
||||||
|
ClimateMode old_mode = this->mode;
|
||||||
|
if (packet.control.ac_power == 0) {
|
||||||
|
this->mode = CLIMATE_MODE_OFF;
|
||||||
|
} else {
|
||||||
|
// Check current hvac mode
|
||||||
|
switch (packet.control.ac_mode) {
|
||||||
|
case (uint8_t) smartair2_protocol::ConditioningMode::COOL:
|
||||||
|
this->mode = CLIMATE_MODE_COOL;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::ConditioningMode::HEAT:
|
||||||
|
this->mode = CLIMATE_MODE_HEAT;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::ConditioningMode::DRY:
|
||||||
|
this->mode = CLIMATE_MODE_DRY;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::ConditioningMode::FAN:
|
||||||
|
this->mode = CLIMATE_MODE_FAN_ONLY;
|
||||||
|
break;
|
||||||
|
case (uint8_t) smartair2_protocol::ConditioningMode::AUTO:
|
||||||
|
this->mode = CLIMATE_MODE_AUTO;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
ClimateSwingMode old_swing_mode = this->swing_mode;
|
||||||
|
if (packet.control.swing_both == 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);
|
||||||
|
}
|
||||||
|
this->last_valid_status_timestamp_ = std::chrono::steady_clock::now();
|
||||||
|
if (this->forced_publish_ || 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();
|
||||||
|
#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) {
|
||||||
|
ESP_LOGI(TAG, "HVAC values changed");
|
||||||
|
}
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), 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__,
|
||||||
|
"Fan speed Status = 0x%X", packet.control.fan_mode);
|
||||||
|
esp_log_printf_((should_publish ? ESPHOME_LOG_LEVEL_INFO : ESPHOME_LOG_LEVEL_DEBUG), 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__,
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Smartair2Climate::is_message_invalid(uint8_t message_type) {
|
||||||
|
return message_type == (uint8_t) smartair2_protocol::FrameType::INVALID;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
31
esphome/components/haier/smartair2_climate.h
Normal file
31
esphome/components/haier/smartair2_climate.h
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include "haier_base.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
|
||||||
|
class Smartair2Climate : public HaierClimateBase {
|
||||||
|
public:
|
||||||
|
Smartair2Climate();
|
||||||
|
Smartair2Climate(const Smartair2Climate &) = delete;
|
||||||
|
Smartair2Climate &operator=(const Smartair2Climate &) = delete;
|
||||||
|
~Smartair2Climate();
|
||||||
|
void dump_config() override;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void set_answers_handlers() override;
|
||||||
|
void process_phase(std::chrono::steady_clock::time_point now) override;
|
||||||
|
haier_protocol::HaierMessage get_control_message() override;
|
||||||
|
bool is_message_invalid(uint8_t message_type) override;
|
||||||
|
// Answers handlers
|
||||||
|
haier_protocol::HandlerError status_handler_(uint8_t request_type, uint8_t message_type, const uint8_t *data,
|
||||||
|
size_t data_size);
|
||||||
|
// Helper functions
|
||||||
|
haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
|
||||||
|
std::unique_ptr<uint8_t[]> last_status_message_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
97
esphome/components/haier/smartair2_packet.h
Normal file
97
esphome/components/haier/smartair2_packet.h
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace haier {
|
||||||
|
namespace smartair2_protocol {
|
||||||
|
|
||||||
|
enum class ConditioningMode : uint8_t { AUTO = 0x00, COOL = 0x01, HEAT = 0x02, FAN = 0x03, DRY = 0x04 };
|
||||||
|
|
||||||
|
enum class FanMode : uint8_t { FAN_HIGH = 0x00, FAN_MID = 0x01, FAN_LOW = 0x02, FAN_AUTO = 0x03 };
|
||||||
|
|
||||||
|
struct HaierPacketControl {
|
||||||
|
// Control bytes starts here
|
||||||
|
// 10
|
||||||
|
uint8_t : 8; // Temperature high byte
|
||||||
|
// 11
|
||||||
|
uint8_t room_temperature; // current room temperature 1°C step
|
||||||
|
// 12
|
||||||
|
uint8_t : 8; // Humidity high byte
|
||||||
|
// 13
|
||||||
|
uint8_t room_humidity; // Humidity 0%-100% with 1% step
|
||||||
|
// 14
|
||||||
|
uint8_t : 8;
|
||||||
|
// 15
|
||||||
|
uint8_t cntrl; // In AC => ESP packets - 0x7F, in ESP => AC packets - 0x00
|
||||||
|
// 16
|
||||||
|
uint8_t : 8;
|
||||||
|
// 17
|
||||||
|
uint8_t : 8;
|
||||||
|
// 18
|
||||||
|
uint8_t : 8;
|
||||||
|
// 19
|
||||||
|
uint8_t : 8;
|
||||||
|
// 20
|
||||||
|
uint8_t : 8;
|
||||||
|
// 21
|
||||||
|
uint8_t ac_mode; // See enum ConditioningMode
|
||||||
|
// 22
|
||||||
|
uint8_t : 8;
|
||||||
|
// 23
|
||||||
|
uint8_t fan_mode; // See enum FanMode
|
||||||
|
// 24
|
||||||
|
uint8_t : 8;
|
||||||
|
// 25
|
||||||
|
uint8_t swing_both; // If 1 - swing both direction, if 0 - horizontal_swing and vertical_swing define
|
||||||
|
// vertical/horizontal/off
|
||||||
|
// 26
|
||||||
|
uint8_t : 3;
|
||||||
|
uint8_t use_fahrenheit : 1;
|
||||||
|
uint8_t : 3;
|
||||||
|
uint8_t lock_remote : 1; // Disable remote
|
||||||
|
// 27
|
||||||
|
uint8_t ac_power : 1; // Is ac on or off
|
||||||
|
uint8_t : 2;
|
||||||
|
uint8_t health_mode : 1; // Health mode on or off
|
||||||
|
uint8_t compressor : 1; // Compressor on or off ???
|
||||||
|
uint8_t : 1;
|
||||||
|
uint8_t ten_degree : 1; // 10 degree status (only work in heat mode)
|
||||||
|
uint8_t : 0;
|
||||||
|
// 28
|
||||||
|
uint8_t : 8;
|
||||||
|
// 29
|
||||||
|
uint8_t use_swing_bits : 1; // Indicate if horizontal_swing and vertical_swing should be used
|
||||||
|
uint8_t turbo_mode : 1; // Turbo mode
|
||||||
|
uint8_t quiet_mode : 1; // Sleep mode
|
||||||
|
uint8_t horizontal_swing : 1; // Horizontal swing (if swing_both == 0)
|
||||||
|
uint8_t vertical_swing : 1; // Vertical swing (if swing_both == 0) if vertical_swing and horizontal_swing both 0 =>
|
||||||
|
// swing off
|
||||||
|
uint8_t display_status : 1; // Led on or off
|
||||||
|
uint8_t : 0;
|
||||||
|
// 30
|
||||||
|
uint8_t : 8;
|
||||||
|
// 31
|
||||||
|
uint8_t : 8;
|
||||||
|
// 32
|
||||||
|
uint8_t : 8; // Target temperature high byte
|
||||||
|
// 33
|
||||||
|
uint8_t set_point; // Target temperature with 16°C offset, 1°C step
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HaierStatus {
|
||||||
|
uint16_t subcommand;
|
||||||
|
HaierPacketControl control;
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class FrameType : uint8_t {
|
||||||
|
CONTROL = 0x01,
|
||||||
|
STATUS = 0x02,
|
||||||
|
INVALID = 0x03,
|
||||||
|
CONFIRM = 0x05,
|
||||||
|
GET_DEVICE_VERSION = 0x61,
|
||||||
|
REPORT_NETWORK_STATUS = 0xF7,
|
||||||
|
NO_COMMAND = 0xFF,
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace smartair2_protocol
|
||||||
|
} // namespace haier
|
||||||
|
} // namespace esphome
|
|
@ -39,6 +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.18 ; 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 =
|
||||||
|
|
|
@ -944,13 +944,29 @@ climate:
|
||||||
kd_multiplier: 0.0
|
kd_multiplier: 0.0
|
||||||
deadband_output_averaging_samples: 1
|
deadband_output_averaging_samples: 1
|
||||||
- platform: haier
|
- platform: haier
|
||||||
|
protocol: hOn
|
||||||
name: Haier AC
|
name: Haier AC
|
||||||
supported_swing_modes:
|
|
||||||
- vertical
|
|
||||||
- horizontal
|
|
||||||
- both
|
|
||||||
update_interval: 10s
|
|
||||||
uart_id: uart_12
|
uart_id: uart_12
|
||||||
|
wifi_signal: true
|
||||||
|
beeper: true
|
||||||
|
outdoor_temperature:
|
||||||
|
name: Haier AC outdoor temperature
|
||||||
|
visual:
|
||||||
|
min_temperature: 16 °C
|
||||||
|
max_temperature: 30 °C
|
||||||
|
temperature_step: 1 °C
|
||||||
|
supported_modes:
|
||||||
|
- 'OFF'
|
||||||
|
- AUTO
|
||||||
|
- COOL
|
||||||
|
- HEAT
|
||||||
|
- DRY
|
||||||
|
- FAN_ONLY
|
||||||
|
supported_swing_modes:
|
||||||
|
- 'OFF'
|
||||||
|
- VERTICAL
|
||||||
|
- HORIZONTAL
|
||||||
|
- BOTH
|
||||||
|
|
||||||
sprinkler:
|
sprinkler:
|
||||||
- id: yard_sprinkler_ctrlr
|
- id: yard_sprinkler_ctrlr
|
||||||
|
|
Loading…
Reference in a new issue