This commit is contained in:
Petapton 2024-05-02 15:46:55 +12:00 committed by GitHub
commit e3c60a760e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 558 additions and 0 deletions

View File

@ -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

View File

@ -0,0 +1 @@
CODEOWNERS = ["@Petapton"]

View File

@ -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)

View File

@ -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

View File

@ -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