1
0
mirror of https://github.com/esphome/esphome.git synced 2025-10-27 13:13:50 +00:00

Midea climate support (#1328)

* Added support for Midea IoT climate devices via UART interface (USB-dongle)

* Fixed lint checks

* Fixed lint checks

* CODEOWNERS update

* Clang-format

* Clang-format

* Add network device notification message support (show WiFi sign on devices)

* Make wifi_signal_sensor optional component

* Some optimization

* Optimizations and code formatting

* Fixed lint checks

* Fixed lint checks

* Fixed sign error

* Code changes

* Network notify repeat every 10 min

* Added log messages

* Fixed lint checks

* Refactoring: MideaClimate => MideaAC

* Using enums instead literals in Midea states

* Enum changed to be more correct

* Shrink notify frame to 32 bytes

* Fixed lint checks

* Change notify frame appliance type to common broadcast

* Control optimization

* Fixed control error

* Control command now don't reset others uncontrollable properties of device

* Fixed lint checks

* Some optimization

* on_receive callback give const Frame

* Fix control

* Fixes

* Some minor changes

* Fixed lint error

* No dependency from wifi_signal sensor for stretched WiFi icon. New option: stretched_icon instead wifi_signal_id.

* Fix option name

* Added export of outdoor temperature as sensor value

* Fixed lint errors

* Fixed pylint error

* Minor fix

* Fix temperature overflow in some cases

* Added answer on QueryNetwork command from appliance. Now don't wait for ack on 0x0D command.

* Fix lint error

* Added humidity setpoint optional sensor

* Added boolean options 'swing_horizontal' and 'swing_both'

* Added debug frame output

* Added debug frame output

* Fix lints error

* Some debug output optimization

* Fix lint check

* Some code optimization: adding templates

* Fix lint error

* Added sensors device classes

* Python code reformatted with black formatter

* RX frame debug message

RX frame debug message now prints before checking

* Remove CRC check for receiving frames

* Added experimental power usage option

* Added power usage option

* Fixed lint errors

* Major changes. See esp-docs.

* Added tests in test4.yaml

* Added tests in test1.yaml

* Added wifi dependency

* Fix test1.yaml

* Some fix :)

* One more refactoring

* One more refactoring

* One more refactoring
This commit is contained in:
Sergey V. DUDANOV
2021-03-18 00:27:50 +04:00
committed by GitHub
parent faf577a9dd
commit f34c9b33fc
13 changed files with 925 additions and 0 deletions

View File

View File

@@ -0,0 +1,69 @@
from esphome.components import climate, sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_ID,
UNIT_CELSIUS,
UNIT_PERCENT,
UNIT_WATT,
ICON_THERMOMETER,
ICON_POWER,
DEVICE_CLASS_POWER,
DEVICE_CLASS_TEMPERATURE,
ICON_WATER_PERCENT,
DEVICE_CLASS_HUMIDITY,
)
from esphome.components.midea_dongle import CONF_MIDEA_DONGLE_ID, MideaDongle
AUTO_LOAD = ["climate", "sensor", "midea_dongle"]
CODEOWNERS = ["@dudanov"]
CONF_BEEPER = "beeper"
CONF_SWING_HORIZONTAL = "swing_horizontal"
CONF_SWING_BOTH = "swing_both"
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
CONF_POWER_USAGE = "power_usage"
CONF_HUMIDITY_SETPOINT = "humidity_setpoint"
midea_ac_ns = cg.esphome_ns.namespace("midea_ac")
MideaAC = midea_ac_ns.class_("MideaAC", climate.Climate, cg.Component)
CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(MideaAC),
cv.GenerateID(CONF_MIDEA_DONGLE_ID): cv.use_id(MideaDongle),
cv.Optional(CONF_BEEPER, default=False): cv.boolean,
cv.Optional(CONF_SWING_HORIZONTAL, default=False): cv.boolean,
cv.Optional(CONF_SWING_BOTH, default=False): cv.boolean,
cv.Optional(CONF_OUTDOOR_TEMPERATURE): sensor.sensor_schema(
UNIT_CELSIUS, ICON_THERMOMETER, 0, DEVICE_CLASS_TEMPERATURE
),
cv.Optional(CONF_POWER_USAGE): sensor.sensor_schema(
UNIT_WATT, ICON_POWER, 0, DEVICE_CLASS_POWER
),
cv.Optional(CONF_HUMIDITY_SETPOINT): sensor.sensor_schema(
UNIT_PERCENT, ICON_WATER_PERCENT, 0, DEVICE_CLASS_HUMIDITY
),
}
).extend(cv.COMPONENT_SCHEMA)
)
def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
yield cg.register_component(var, config)
yield climate.register_climate(var, config)
paren = yield cg.get_variable(config[CONF_MIDEA_DONGLE_ID])
cg.add(var.set_midea_dongle_parent(paren))
cg.add(var.set_beeper_feedback(config[CONF_BEEPER]))
cg.add(var.set_swing_horizontal(config[CONF_SWING_HORIZONTAL]))
cg.add(var.set_swing_both(config[CONF_SWING_BOTH]))
if CONF_OUTDOOR_TEMPERATURE in config:
sens = yield sensor.new_sensor(config[CONF_OUTDOOR_TEMPERATURE])
cg.add(var.set_outdoor_temperature_sensor(sens))
if CONF_POWER_USAGE in config:
sens = yield sensor.new_sensor(config[CONF_POWER_USAGE])
cg.add(var.set_power_sensor(sens))
if CONF_HUMIDITY_SETPOINT in config:
sens = yield sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT])
cg.add(var.set_humidity_setpoint_sensor(sens))

View File

@@ -0,0 +1,110 @@
#include "esphome/core/log.h"
#include "midea_climate.h"
namespace esphome {
namespace midea_ac {
static const char *TAG = "midea_ac";
static void set_sensor(sensor::Sensor *sensor, float value) {
if (sensor != nullptr && (!sensor->has_state() || sensor->get_raw_state() != value))
sensor->publish_state(value);
}
template<typename T> void set_property(T &property, T value, bool &flag) {
if (property != value) {
property = value;
flag = true;
}
}
void MideaAC::on_frame(const midea_dongle::Frame &frame) {
const auto p = frame.as<PropertiesFrame>();
if (p.has_power_info()) {
set_sensor(this->power_sensor_, p.get_power_usage());
return;
} else if (!p.has_properties()) {
ESP_LOGW(TAG, "RX: frame has unknown type");
return;
}
if (p.get_type() == midea_dongle::MideaMessageType::DEVICE_CONTROL) {
ESP_LOGD(TAG, "RX: control frame");
this->ctrl_request_ = false;
} else {
ESP_LOGD(TAG, "RX: query frame");
}
if (this->ctrl_request_)
return;
this->cmd_frame_.set_properties(p); // copy properties from response
bool need_publish = false;
set_property(this->mode, p.get_mode(), need_publish);
set_property(this->target_temperature, p.get_target_temp(), need_publish);
set_property(this->current_temperature, p.get_indoor_temp(), need_publish);
set_property(this->fan_mode, p.get_fan_mode(), need_publish);
set_property(this->swing_mode, p.get_swing_mode(), need_publish);
if (need_publish)
this->publish_state();
set_sensor(this->outdoor_sensor_, p.get_outdoor_temp());
set_sensor(this->humidity_sensor_, p.get_humidity_setpoint());
}
void MideaAC::on_update() {
if (this->ctrl_request_) {
ESP_LOGD(TAG, "TX: control");
this->parent_->write_frame(this->cmd_frame_);
} else {
ESP_LOGD(TAG, "TX: query");
if (this->power_sensor_ == nullptr || this->request_num_++ % 32)
this->parent_->write_frame(this->query_frame_);
else
this->parent_->write_frame(this->power_frame_);
}
}
void MideaAC::control(const climate::ClimateCall &call) {
if (call.get_mode().has_value() && call.get_mode().value() != this->mode) {
this->cmd_frame_.set_mode(call.get_mode().value());
this->ctrl_request_ = true;
}
if (call.get_target_temperature().has_value() && call.get_target_temperature().value() != this->target_temperature) {
this->cmd_frame_.set_target_temp(call.get_target_temperature().value());
this->ctrl_request_ = true;
}
if (call.get_fan_mode().has_value() && call.get_fan_mode().value() != this->fan_mode) {
this->cmd_frame_.set_fan_mode(call.get_fan_mode().value());
this->ctrl_request_ = true;
}
if (call.get_swing_mode().has_value() && call.get_swing_mode().value() != this->swing_mode) {
this->cmd_frame_.set_swing_mode(call.get_swing_mode().value());
this->ctrl_request_ = true;
}
if (this->ctrl_request_) {
this->cmd_frame_.set_beeper_feedback(this->beeper_feedback_);
this->cmd_frame_.finalize();
}
}
climate::ClimateTraits MideaAC::traits() {
auto traits = climate::ClimateTraits();
traits.set_visual_min_temperature(17);
traits.set_visual_max_temperature(30);
traits.set_visual_temperature_step(0.5);
traits.set_supports_auto_mode(true);
traits.set_supports_cool_mode(true);
traits.set_supports_dry_mode(true);
traits.set_supports_heat_mode(true);
traits.set_supports_fan_only_mode(true);
traits.set_supports_fan_mode_auto(true);
traits.set_supports_fan_mode_low(true);
traits.set_supports_fan_mode_medium(true);
traits.set_supports_fan_mode_high(true);
traits.set_supports_swing_mode_off(true);
traits.set_supports_swing_mode_vertical(true);
traits.set_supports_swing_mode_horizontal(this->traits_swing_horizontal_);
traits.set_supports_swing_mode_both(this->traits_swing_both_);
traits.set_supports_current_temperature(true);
return traits;
}
} // namespace midea_ac
} // namespace esphome

View File

@@ -0,0 +1,47 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/midea_dongle/midea_dongle.h"
#include "esphome/components/climate/climate.h"
#include "midea_frame.h"
namespace esphome {
namespace midea_ac {
class MideaAC : public midea_dongle::MideaAppliance, public climate::Climate, public Component {
public:
float get_setup_priority() const override { return setup_priority::LATE; }
void on_frame(const midea_dongle::Frame &frame) override;
void on_update() override;
void setup() override { this->parent_->set_appliance(this); }
void set_midea_dongle_parent(midea_dongle::MideaDongle *parent) { this->parent_ = parent; }
void set_outdoor_temperature_sensor(sensor::Sensor *sensor) { this->outdoor_sensor_ = sensor; }
void set_humidity_setpoint_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
void set_power_sensor(sensor::Sensor *sensor) { this->power_sensor_ = sensor; }
void set_beeper_feedback(bool state) { this->beeper_feedback_ = state; }
void set_swing_horizontal(bool state) { this->traits_swing_horizontal_ = state; }
void set_swing_both(bool state) { this->traits_swing_both_ = state; }
protected:
/// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override;
/// Return the traits of this controller.
climate::ClimateTraits traits() override;
const QueryFrame query_frame_;
const PowerQueryFrame power_frame_;
CommandFrame cmd_frame_;
midea_dongle::MideaDongle *parent_{nullptr};
sensor::Sensor *outdoor_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
sensor::Sensor *power_sensor_{nullptr};
uint8_t request_num_{0};
bool ctrl_request_{false};
bool beeper_feedback_{false};
bool traits_swing_horizontal_{false};
bool traits_swing_both_{false};
};
} // namespace midea_ac
} // namespace esphome

View File

@@ -0,0 +1,160 @@
#include "midea_frame.h"
namespace esphome {
namespace midea_ac {
const uint8_t QueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x00,
0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x68};
const uint8_t PowerQueryFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x03, 0x41, 0x21,
0x01, 0x44, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x17, 0x6A};
const uint8_t CommandFrame::INIT[] = {0xAA, 0x22, 0xAC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x02, 0x40, 0x00,
0x00, 0x00, 0x7F, 0x7F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
float PropertiesFrame::get_target_temp() const {
float temp = static_cast<float>((this->pbuf_[12] & 0x0F) + 16);
if (this->pbuf_[12] & 0x10)
temp += 0.5;
return temp;
}
void PropertiesFrame::set_target_temp(float temp) {
uint8_t tmp = static_cast<uint8_t>(temp * 16.0) + 4;
tmp = ((tmp & 8) << 1) | (tmp >> 4);
this->pbuf_[12] &= ~0x1F;
this->pbuf_[12] |= tmp;
}
static float i16tof(int16_t in) { return static_cast<float>(in - 50) / 2.0; }
float PropertiesFrame::get_indoor_temp() const { return i16tof(this->pbuf_[21]); }
float PropertiesFrame::get_outdoor_temp() const { return i16tof(this->pbuf_[22]); }
float PropertiesFrame::get_humidity_setpoint() const { return static_cast<float>(this->pbuf_[29] & 0x7F); }
climate::ClimateMode PropertiesFrame::get_mode() const {
if (!this->get_power_())
return climate::CLIMATE_MODE_OFF;
switch (this->pbuf_[12] >> 5) {
case MIDEA_MODE_AUTO:
return climate::CLIMATE_MODE_AUTO;
case MIDEA_MODE_COOL:
return climate::CLIMATE_MODE_COOL;
case MIDEA_MODE_DRY:
return climate::CLIMATE_MODE_DRY;
case MIDEA_MODE_HEAT:
return climate::CLIMATE_MODE_HEAT;
case MIDEA_MODE_FAN_ONLY:
return climate::CLIMATE_MODE_FAN_ONLY;
default:
return climate::CLIMATE_MODE_OFF;
}
}
void PropertiesFrame::set_mode(climate::ClimateMode mode) {
uint8_t m;
switch (mode) {
case climate::CLIMATE_MODE_AUTO:
m = MIDEA_MODE_AUTO;
break;
case climate::CLIMATE_MODE_COOL:
m = MIDEA_MODE_COOL;
break;
case climate::CLIMATE_MODE_DRY:
m = MIDEA_MODE_DRY;
break;
case climate::CLIMATE_MODE_HEAT:
m = MIDEA_MODE_HEAT;
break;
case climate::CLIMATE_MODE_FAN_ONLY:
m = MIDEA_MODE_FAN_ONLY;
break;
default:
this->set_power_(false);
return;
}
this->set_power_(true);
this->pbuf_[12] &= ~0xE0;
this->pbuf_[12] |= m << 5;
}
climate::ClimateFanMode PropertiesFrame::get_fan_mode() const {
switch (this->pbuf_[13]) {
case MIDEA_FAN_LOW:
return climate::CLIMATE_FAN_LOW;
case MIDEA_FAN_MEDIUM:
return climate::CLIMATE_FAN_MEDIUM;
case MIDEA_FAN_HIGH:
return climate::CLIMATE_FAN_HIGH;
default:
return climate::CLIMATE_FAN_AUTO;
}
}
void PropertiesFrame::set_fan_mode(climate::ClimateFanMode mode) {
uint8_t m;
switch (mode) {
case climate::CLIMATE_FAN_LOW:
m = MIDEA_FAN_LOW;
break;
case climate::CLIMATE_FAN_MEDIUM:
m = MIDEA_FAN_MEDIUM;
break;
case climate::CLIMATE_FAN_HIGH:
m = MIDEA_FAN_HIGH;
break;
default:
m = MIDEA_FAN_AUTO;
break;
}
this->pbuf_[13] = m;
}
climate::ClimateSwingMode PropertiesFrame::get_swing_mode() const {
switch (this->pbuf_[17] & 0x0F) {
case MIDEA_SWING_VERTICAL:
return climate::CLIMATE_SWING_VERTICAL;
case MIDEA_SWING_HORIZONTAL:
return climate::CLIMATE_SWING_HORIZONTAL;
case MIDEA_SWING_BOTH:
return climate::CLIMATE_SWING_BOTH;
default:
return climate::CLIMATE_SWING_OFF;
}
}
void PropertiesFrame::set_swing_mode(climate::ClimateSwingMode mode) {
uint8_t m;
switch (mode) {
case climate::CLIMATE_SWING_VERTICAL:
m = MIDEA_SWING_VERTICAL;
break;
case climate::CLIMATE_SWING_HORIZONTAL:
m = MIDEA_SWING_HORIZONTAL;
break;
case climate::CLIMATE_SWING_BOTH:
m = MIDEA_SWING_BOTH;
break;
default:
m = MIDEA_SWING_OFF;
break;
}
this->pbuf_[17] = 0x30 | m;
}
float PropertiesFrame::get_power_usage() const {
uint32_t power = 0;
const uint8_t *ptr = this->pbuf_ + 28;
for (uint32_t weight = 1;; weight *= 10, ptr--) {
power += (*ptr % 16) * weight;
weight *= 10;
power += (*ptr / 16) * weight;
if (weight == 100000)
return static_cast<float>(power) * 0.1;
}
}
} // namespace midea_ac
} // namespace esphome

View File

@@ -0,0 +1,137 @@
#pragma once
#include "esphome/components/climate/climate.h"
#include "esphome/components/midea_dongle/midea_frame.h"
namespace esphome {
namespace midea_ac {
/// Enum for all modes a Midea device can be in.
enum MideaMode : uint8_t {
/// The Midea device is set to automatically change the heating/cooling cycle
MIDEA_MODE_AUTO = 1,
/// The Midea device is manually set to cool mode (not in auto mode!)
MIDEA_MODE_COOL = 2,
/// The Midea device is manually set to dry mode
MIDEA_MODE_DRY = 3,
/// The Midea device is manually set to heat mode (not in auto mode!)
MIDEA_MODE_HEAT = 4,
/// The Midea device is manually set to fan only mode
MIDEA_MODE_FAN_ONLY = 5,
};
/// Enum for all modes a Midea fan can be in
enum MideaFanMode : uint8_t {
/// The fan mode is set to Auto
MIDEA_FAN_AUTO = 102,
/// The fan mode is set to Low
MIDEA_FAN_LOW = 40,
/// The fan mode is set to Medium
MIDEA_FAN_MEDIUM = 60,
/// The fan mode is set to High
MIDEA_FAN_HIGH = 80,
};
/// Enum for all modes a Midea swing can be in
enum MideaSwingMode : uint8_t {
/// The sing mode is set to Off
MIDEA_SWING_OFF = 0b0000,
/// The fan mode is set to Both
MIDEA_SWING_BOTH = 0b1111,
/// The fan mode is set to Vertical
MIDEA_SWING_VERTICAL = 0b1100,
/// The fan mode is set to Horizontal
MIDEA_SWING_HORIZONTAL = 0b0011,
};
class PropertiesFrame : public midea_dongle::BaseFrame {
public:
PropertiesFrame() = delete;
PropertiesFrame(uint8_t *data) : BaseFrame(data) {}
PropertiesFrame(const Frame &frame) : BaseFrame(frame) {}
bool has_properties() const {
return this->has_response_type(0xC0) && (this->has_type(0x03) || this->has_type(0x02));
}
bool has_power_info() const { return this->has_response_type(0xC1); }
/* TARGET TEMPERATURE */
float get_target_temp() const;
void set_target_temp(float temp);
/* MODE */
climate::ClimateMode get_mode() const;
void set_mode(climate::ClimateMode mode);
/* FAN SPEED */
climate::ClimateFanMode get_fan_mode() const;
void set_fan_mode(climate::ClimateFanMode mode);
/* SWING MODE */
climate::ClimateSwingMode get_swing_mode() const;
void set_swing_mode(climate::ClimateSwingMode mode);
/* INDOOR TEMPERATURE */
float get_indoor_temp() const;
/* OUTDOOR TEMPERATURE */
float get_outdoor_temp() const;
/* HUMIDITY SETPOINT */
float get_humidity_setpoint() const;
/* ECO MODE */
bool get_eco_mode() const { return this->pbuf_[19]; }
void set_eco_mode(bool state) { this->set_bytemask_(19, 0xFF, state); }
/* SLEEP MODE */
bool get_sleep_mode() const { return this->pbuf_[20] & 0x01; }
void set_sleep_mode(bool state) { this->set_bytemask_(20, 0x01, state); }
/* TURBO MODE */
bool get_turbo_mode() const { return this->pbuf_[20] & 0x02; }
void set_turbo_mode(bool state) { this->set_bytemask_(20, 0x02, state); }
/* POWER USAGE */
float get_power_usage() const;
/// Set properties from another frame
void set_properties(const PropertiesFrame &p) { memcpy(this->pbuf_ + 11, p.data() + 11, 10); }
protected:
/* POWER */
bool get_power_() const { return this->pbuf_[11] & 0x01; }
void set_power_(bool state) { this->set_bytemask_(11, 0x01, state); }
};
// Query state frame (read-only)
class QueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> {
public:
QueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
private:
static const uint8_t PROGMEM INIT[];
};
// Power query state frame (read-only)
class PowerQueryFrame : public midea_dongle::StaticFrame<midea_dongle::Frame> {
public:
PowerQueryFrame() : StaticFrame(FPSTR(this->INIT)) {}
private:
static const uint8_t PROGMEM INIT[];
};
// Command frame
class CommandFrame : public midea_dongle::StaticFrame<PropertiesFrame> {
public:
CommandFrame() : StaticFrame(FPSTR(this->INIT)) {}
void set_beeper_feedback(bool state) { this->set_bytemask_(11, 0x40, state); }
private:
static const uint8_t PROGMEM INIT[];
};
} // namespace midea_ac
} // namespace esphome