mirror of https://github.com/esphome/esphome.git
Merge 1f450214c5
into c7c0d97a5e
This commit is contained in:
commit
e3c60a760e
|
@ -197,6 +197,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
|
|||
esphome/components/lock/* @esphome/core
|
||||
esphome/components/logger/* @esphome/core
|
||||
esphome/components/ltr390/* @sjtrny
|
||||
esphome/components/madoka/* @Petapton
|
||||
esphome/components/matrix_keypad/* @ssieb
|
||||
esphome/components/max31865/* @DAVe3283
|
||||
esphome/components/max44009/* @berfenger
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
CODEOWNERS = ["@Petapton"]
|
|
@ -0,0 +1,29 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import climate, ble_client
|
||||
from esphome.const import CONF_ID
|
||||
|
||||
CODEOWNERS = ["@Petapton"]
|
||||
DEPENDENCIES = ["ble_client"]
|
||||
|
||||
madoka_ns = cg.esphome_ns.namespace("madoka")
|
||||
Madoka = madoka_ns.class_(
|
||||
"Madoka", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
climate.CLIMATE_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(Madoka),
|
||||
}
|
||||
)
|
||||
.extend(ble_client.BLE_CLIENT_SCHEMA)
|
||||
.extend(cv.polling_component_schema("10s"))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await climate.register_climate(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
|
@ -0,0 +1,413 @@
|
|||
#include "madoka.h"
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include <utility>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace madoka {
|
||||
|
||||
using namespace esphome::climate;
|
||||
|
||||
void Madoka::dump_config() { LOG_CLIMATE(TAG, "Daikin Madoka Climate Controller", this); }
|
||||
|
||||
void Madoka::setup() { this->receive_semaphore_ = xSemaphoreCreateMutex(); }
|
||||
|
||||
void Madoka::loop() {
|
||||
chunk chk = {};
|
||||
if (xSemaphoreTake(this->receive_semaphore_, 0L)) {
|
||||
if (!this->received_chunks_.empty()) {
|
||||
chk = this->received_chunks_.front();
|
||||
this->received_chunks_.pop();
|
||||
}
|
||||
xSemaphoreGive(this->receive_semaphore_);
|
||||
if (!chk.empty()) {
|
||||
this->process_incoming_chunk_(chk);
|
||||
}
|
||||
}
|
||||
if (this->should_update_) {
|
||||
this->should_update_ = false;
|
||||
this->update();
|
||||
}
|
||||
}
|
||||
|
||||
void Madoka::control(const ClimateCall &call) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED)
|
||||
return;
|
||||
if (call.get_mode().has_value()) {
|
||||
ClimateMode mode = *call.get_mode();
|
||||
std::vector<chunk> pkt;
|
||||
uint8_t mode_out = 255, status_out = 0;
|
||||
switch (mode) {
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
status_out = 0;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
status_out = 1;
|
||||
mode_out = 2;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
status_out = 1;
|
||||
mode_out = 3;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
status_out = 1;
|
||||
mode_out = 4;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_FAN_ONLY:
|
||||
status_out = 1;
|
||||
mode_out = 0;
|
||||
break;
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
status_out = 1;
|
||||
mode_out = 1;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unsupported mode: %d", mode);
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "status: %d, mode: %d", status_out, mode_out);
|
||||
if (mode_out != 255) {
|
||||
this->query_(0x4030, message({0x20, 0x01, (uint8_t) mode_out}), 600);
|
||||
}
|
||||
this->query_(0x4020, message({0x20, 0x01, (uint8_t) status_out}), 200);
|
||||
}
|
||||
if (call.get_target_temperature_low().has_value() && call.get_target_temperature_high().has_value()) {
|
||||
uint16_t target_low = *call.get_target_temperature_low() * 128;
|
||||
uint16_t target_high = *call.get_target_temperature_high() * 128;
|
||||
this->query_(0x4040,
|
||||
message({0x20, 0x02, (uint8_t) ((target_high >> 8) & 0xFF), (uint8_t) (target_high & 0xFF), 0x21, 0x02,
|
||||
(uint8_t) ((target_low >> 8) & 0xFF), (uint8_t) (target_low & 0xFF)}),
|
||||
400);
|
||||
}
|
||||
if (call.get_fan_mode().has_value()) {
|
||||
uint8_t fan_mode = call.get_fan_mode().value();
|
||||
uint8_t fan_mode_out = 255;
|
||||
switch (fan_mode) {
|
||||
case climate::CLIMATE_FAN_AUTO:
|
||||
fan_mode_out = 0;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_LOW:
|
||||
fan_mode_out = 1;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_MEDIUM:
|
||||
fan_mode_out = 3;
|
||||
break;
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
fan_mode_out = 5;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unsupported fan mode: %d", fan_mode);
|
||||
break;
|
||||
}
|
||||
if (fan_mode_out != 255) {
|
||||
this->query_(0x4050, message({0x20, 0x01, (uint8_t) fan_mode_out, 0x21, 0x01, (uint8_t) fan_mode_out}), 200);
|
||||
}
|
||||
}
|
||||
this->should_update_ = true;
|
||||
}
|
||||
|
||||
void Madoka::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GAP_BLE_SEC_REQ_EVT:
|
||||
esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
|
||||
break;
|
||||
case ESP_GAP_BLE_NC_REQ_EVT:
|
||||
esp_ble_confirm_reply(param->ble_security.ble_req.bd_addr, true);
|
||||
ESP_LOGI(TAG, "ESP_GAP_BLE_NC_REQ_EVT, the passkey Notify number:%d", param->ble_security.key_notif.passkey);
|
||||
break;
|
||||
case ESP_GAP_BLE_AUTH_CMPL_EVT: {
|
||||
if (!param->ble_security.auth_cmpl.success) {
|
||||
ESP_LOGE(TAG, "Authentication failed, status: 0x%x", param->ble_security.auth_cmpl.fail_reason);
|
||||
break;
|
||||
}
|
||||
auto *nfy = this->parent_->get_characteristic(MADOKA_SERVICE_UUID, NOTIFY_CHARACTERISTIC_UUID);
|
||||
auto *wwr = this->parent_->get_characteristic(MADOKA_SERVICE_UUID, WWR_CHARACTERISTIC_UUID);
|
||||
if (nfy == nullptr || wwr == nullptr) {
|
||||
ESP_LOGW(TAG, "[%s] No control service found at device, not a Daikin Madoka..?", this->get_name().c_str());
|
||||
break;
|
||||
}
|
||||
this->notify_handle_ = nfy->handle;
|
||||
this->wwr_handle_ = wwr->handle;
|
||||
|
||||
auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
|
||||
nfy->handle);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Madoka::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) {
|
||||
switch (event) {
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
this->node_state = espbt::ClientState::IDLE; // ??
|
||||
this->current_temperature = NAN;
|
||||
this->target_temperature = NAN;
|
||||
this->publish_state();
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_DESCR_EVT:
|
||||
if (param->write.status != ESP_GATT_OK) {
|
||||
if (param->write.status == ESP_GATT_INSUF_AUTHENTICATION) {
|
||||
ESP_LOGE(TAG, "Insufficient authentication");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Failed writing characteristic descriptor, status = 0x%x", param->write.status);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
esp_ble_set_encryption(this->parent_->get_remote_bda(), ESP_BLE_SEC_ENCRYPT_MITM);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->node_state = espbt::ClientState::ESTABLISHED; // ??
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.handle != this->notify_handle_) {
|
||||
ESP_LOGW(TAG, "Different notify handle");
|
||||
break;
|
||||
}
|
||||
chunk chk = chunk(param->notify.value, param->notify.value + param->notify.value_len);
|
||||
xSemaphoreTake(this->receive_semaphore_, portMAX_DELAY);
|
||||
this->received_chunks_.push(chk);
|
||||
xSemaphoreGive(this->receive_semaphore_);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void Madoka::update() {
|
||||
ESP_LOGD(TAG, "Got update request...");
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGD(TAG, "...but device is disconnected");
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<uint16_t> all_cmds({0x0020, 0x0030, 0x0040, 0x0050, 0x0110});
|
||||
for (auto cmd : all_cmds) {
|
||||
this->query_(cmd, message({0x00, 0x00}), 50);
|
||||
}
|
||||
}
|
||||
|
||||
bool validate_buffer(message buffer) { return buffer[0] == buffer.size(); }
|
||||
|
||||
void Madoka::process_incoming_chunk_(chunk chk) {
|
||||
if (chk.size() < 2) {
|
||||
ESP_LOGI(TAG, "Chunk discarded: invalid length.");
|
||||
return;
|
||||
}
|
||||
uint8_t chunk_id = chk[0];
|
||||
message stripped(chk.begin() + 1, chk.end());
|
||||
if (chunk_id == 0 && validate_buffer(stripped)) {
|
||||
this->parse_cb_(stripped);
|
||||
return;
|
||||
}
|
||||
if (this->pending_chunks_.count(chunk_id)) {
|
||||
ESP_LOGE(TAG, "Another packet with the same chunk ID is already in the buffer.");
|
||||
ESP_LOGD(TAG, "Chunk ID: %d.", chunk_id);
|
||||
return;
|
||||
}
|
||||
this->pending_chunks_[chunk_id] = chk;
|
||||
|
||||
if (this->pending_chunks_.size() != this->pending_chunks_.rbegin()->first + 1) {
|
||||
ESP_LOGW(TAG, "Buffer is missing packets");
|
||||
return;
|
||||
}
|
||||
|
||||
message msg;
|
||||
int lim = this->pending_chunks_.size();
|
||||
for (int i = 0; i < lim; i++) {
|
||||
msg.insert(msg.end(), this->pending_chunks_[i].begin() + 1, this->pending_chunks_[i].end());
|
||||
}
|
||||
if (validate_buffer(msg)) {
|
||||
this->pending_chunks_.clear();
|
||||
this->parse_cb_(msg);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<chunk> Madoka::split_payload_(message msg) {
|
||||
std::vector<chunk> result;
|
||||
size_t len = msg.size();
|
||||
result.push_back(chunk({0x00, (uint8_t) (len + 1)}));
|
||||
result[0].insert(result[0].end(), msg.begin(), min(msg.begin() + (MAX_CHUNK_SIZE - 2), msg.end()));
|
||||
int i = 0;
|
||||
for (i = 1; i < len / (MAX_CHUNK_SIZE - 1); i++) { // from second to second-last
|
||||
result.emplace_back(msg.begin() + ((MAX_CHUNK_SIZE - 1) * i - 1),
|
||||
msg.begin() + ((MAX_CHUNK_SIZE - 1) * (i + 1) - 1));
|
||||
}
|
||||
if (len > 18) {
|
||||
i++;
|
||||
result.emplace_back(msg.begin() + ((MAX_CHUNK_SIZE - 1) * i), msg.end());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
message Madoka::prepare_message_(uint16_t cmd, message args) {
|
||||
message result({0x00, (uint8_t) ((cmd >> 8) & 0xFF), (uint8_t) (cmd & 0xFF)});
|
||||
result.insert(result.end(), args.begin(), args.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
void Madoka::query_(uint16_t cmd, message args, int t_d) {
|
||||
message payload = this->prepare_message_(cmd, std::move(args));
|
||||
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
return;
|
||||
}
|
||||
std::vector<chunk> chunks = this->split_payload_(payload);
|
||||
|
||||
for (auto chk : chunks) {
|
||||
esp_err_t status;
|
||||
for (int j = 0; j < BLE_SEND_MAX_RETRIES; j++) {
|
||||
status = esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->wwr_handle_,
|
||||
chk.size(), &chk[0], ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (!status) {
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%s] esp_ble_gattc_write_char failed (%d of %d), status=%d", this->parent_->address_str().c_str(),
|
||||
j + 1, BLE_SEND_MAX_RETRIES, status);
|
||||
}
|
||||
if (status) {
|
||||
ESP_LOGE(TAG, "[%s] Command could not be sent, last status=%d", this->parent_->address_str().c_str(), status);
|
||||
return;
|
||||
}
|
||||
}
|
||||
delay(t_d);
|
||||
}
|
||||
|
||||
void Madoka::parse_cb_(message msg) {
|
||||
uint16_t function_id = msg[2] << 8 | msg[3];
|
||||
uint8_t i = 4;
|
||||
uint8_t message_size = msg.size();
|
||||
|
||||
switch (function_id) {
|
||||
case 0x0020:
|
||||
while (i < message_size) {
|
||||
uint8_t argument_id = msg[i++];
|
||||
uint8_t len = msg[i++];
|
||||
if (argument_id == 0x20) {
|
||||
message val(msg.begin() + i, msg.begin() + i + len);
|
||||
this->cur_status_.status = val[0];
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
case 0x0030:
|
||||
while (i < message_size) {
|
||||
uint8_t argument_id = msg[i++];
|
||||
uint8_t len = msg[i++];
|
||||
if (argument_id == 0x20) {
|
||||
message val(msg.begin() + i, msg.begin() + i + len);
|
||||
this->cur_status_.mode = val[0];
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (function_id) {
|
||||
case 0x0020:
|
||||
case 0x0030:
|
||||
// ESP_LOGI(TAG, "status: %d, mode: %d", this->cur_status_.status, this->cur_status_.mode);
|
||||
if (this->cur_status_.status) {
|
||||
switch (this->cur_status_.mode) {
|
||||
case 0:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
case 1:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
case 2:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
case 3:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
case 4:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
}
|
||||
break;
|
||||
case 0x0040:
|
||||
while (i < message_size) {
|
||||
uint8_t argument_id = msg[i++];
|
||||
uint8_t len = msg[i++];
|
||||
switch (argument_id) {
|
||||
case 0x20: {
|
||||
message val(msg.begin() + i, msg.begin() + i + len);
|
||||
this->target_temperature_high = (float) (val[0] << 8 | val[1]) / 128;
|
||||
break;
|
||||
}
|
||||
case 0x21: {
|
||||
message val(msg.begin() + i, msg.begin() + i + len);
|
||||
this->target_temperature_low = (float) (val[0] << 8 | val[1]) / 128;
|
||||
break;
|
||||
}
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
break;
|
||||
case 0x0050: {
|
||||
uint8_t fan_mode = 255;
|
||||
while (i < message_size) {
|
||||
uint8_t argument_id = msg[i++];
|
||||
uint8_t len = msg[i++];
|
||||
if (this->cur_status_.mode == 1) {
|
||||
} else if ((argument_id == 0x21 && len == 1 && this->cur_status_.mode == 4) ||
|
||||
(argument_id == 0x20 && len == 1 && this->cur_status_.mode != 4)) {
|
||||
fan_mode = msg[i];
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
switch (fan_mode) {
|
||||
case 0:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
case 1:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
case 2:
|
||||
case 3:
|
||||
case 4:
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
break;
|
||||
case 5:
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x0110:
|
||||
while (i < message_size) {
|
||||
uint8_t argument_id = msg[i++];
|
||||
uint8_t len = msg[i++];
|
||||
if (argument_id == 0x40) {
|
||||
message val(msg.begin() + i, msg.begin() + i + len);
|
||||
this->current_temperature = val[0];
|
||||
}
|
||||
i += len;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this->publish_state();
|
||||
}
|
||||
|
||||
} // namespace madoka
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
|
@ -0,0 +1,114 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <queue>
|
||||
#include <map>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/components/climate/climate.h"
|
||||
|
||||
// #define USE_ESP32
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
static const uint8_t MAX_CHUNK_SIZE = 20;
|
||||
static const uint8_t BLE_SEND_MAX_RETRIES = 5;
|
||||
|
||||
namespace esphome {
|
||||
namespace madoka {
|
||||
|
||||
static const char *const TAG = "madoka";
|
||||
|
||||
using chunk = std::vector<uint8_t>;
|
||||
using message = std::vector<uint8_t>;
|
||||
|
||||
struct Setpoint {
|
||||
uint16_t cooling;
|
||||
uint16_t heating;
|
||||
};
|
||||
|
||||
struct FanSpeed {
|
||||
uint8_t cooling;
|
||||
uint8_t heating;
|
||||
};
|
||||
|
||||
struct SensorReading {
|
||||
uint8_t indoor;
|
||||
uint8_t outdoor;
|
||||
};
|
||||
|
||||
struct Status {
|
||||
bool status;
|
||||
uint8_t mode;
|
||||
};
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
#define TO_ESPBTUUID(x) espbt::ESPBTUUID::from_raw(std::string(x))
|
||||
|
||||
#define MADOKA_SERVICE_UUID TO_ESPBTUUID("2141e110-213a-11e6-b67b-9e71128cae77")
|
||||
#define NOTIFY_CHARACTERISTIC_UUID TO_ESPBTUUID("2141e111-213a-11e6-b67b-9e71128cae77")
|
||||
#define WWR_CHARACTERISTIC_UUID TO_ESPBTUUID("2141e112-213a-11e6-b67b-9e71128cae77")
|
||||
|
||||
class Madoka : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent {
|
||||
protected:
|
||||
bool should_update_ = false;
|
||||
std::queue<chunk> received_chunks_ = {};
|
||||
std::map<uint8_t, chunk> pending_chunks_ = {};
|
||||
uint16_t notify_handle_;
|
||||
uint16_t wwr_handle_;
|
||||
SemaphoreHandle_t receive_semaphore_ = nullptr;
|
||||
Status cur_status_;
|
||||
|
||||
std::vector<chunk> split_payload_(message msg);
|
||||
message prepare_message_(uint16_t cmd, message args);
|
||||
void query_(uint16_t cmd, message args, int t_d);
|
||||
void parse_cb_(message msg);
|
||||
void process_incoming_chunk_(chunk chk);
|
||||
|
||||
void control(const climate::ClimateCall &call) override;
|
||||
|
||||
public:
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override;
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
climate::ClimateTraits traits() override {
|
||||
auto traits = climate::ClimateTraits();
|
||||
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_LOW,
|
||||
climate::CLIMATE_FAN_MEDIUM,
|
||||
climate::CLIMATE_FAN_HIGH,
|
||||
climate::CLIMATE_FAN_AUTO,
|
||||
});
|
||||
traits.set_visual_min_temperature(16);
|
||||
traits.set_visual_max_temperature(32);
|
||||
traits.set_visual_temperature_step(1);
|
||||
traits.set_supports_two_point_target_temperature(true);
|
||||
traits.set_supports_current_temperature(true);
|
||||
return traits;
|
||||
}
|
||||
void set_unit_of_measurement(const char *);
|
||||
};
|
||||
|
||||
} // namespace madoka
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
Loading…
Reference in New Issue