mirror of
https://github.com/esphome/esphome.git
synced 2024-11-21 22:48:10 +01:00
Anova ble component (#1752)
Co-authored-by: Ben Buxton <bb@cactii.net> Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
d31040f5d8
commit
ab31117bf3
8 changed files with 434 additions and 0 deletions
|
@ -15,6 +15,7 @@ esphome/components/ac_dimmer/* @glmnet
|
||||||
esphome/components/adc/* @esphome/core
|
esphome/components/adc/* @esphome/core
|
||||||
esphome/components/addressable_light/* @justfalter
|
esphome/components/addressable_light/* @justfalter
|
||||||
esphome/components/animation/* @syndlex
|
esphome/components/animation/* @syndlex
|
||||||
|
esphome/components/anova/* @buxtronix
|
||||||
esphome/components/api/* @OttoWinter
|
esphome/components/api/* @OttoWinter
|
||||||
esphome/components/async_tcp/* @OttoWinter
|
esphome/components/async_tcp/* @OttoWinter
|
||||||
esphome/components/atc_mithermometer/* @ahpohl
|
esphome/components/atc_mithermometer/* @ahpohl
|
||||||
|
|
0
esphome/components/anova/__init__.py
Normal file
0
esphome/components/anova/__init__.py
Normal file
140
esphome/components/anova/anova.cpp
Normal file
140
esphome/components/anova/anova.cpp
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
#include "anova.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace anova {
|
||||||
|
|
||||||
|
static const char *TAG = "anova";
|
||||||
|
|
||||||
|
using namespace esphome::climate;
|
||||||
|
|
||||||
|
void Anova::dump_config() { LOG_CLIMATE("", "Anova BLE Cooker", this); }
|
||||||
|
|
||||||
|
void Anova::setup() {
|
||||||
|
this->codec_ = new AnovaCodec();
|
||||||
|
this->current_request_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Anova::loop() {}
|
||||||
|
|
||||||
|
void Anova::control(const ClimateCall &call) {
|
||||||
|
if (call.get_mode().has_value()) {
|
||||||
|
ClimateMode mode = *call.get_mode();
|
||||||
|
AnovaPacket *pkt;
|
||||||
|
switch (mode) {
|
||||||
|
case climate::CLIMATE_MODE_OFF:
|
||||||
|
pkt = this->codec_->get_stop_request();
|
||||||
|
break;
|
||||||
|
case climate::CLIMATE_MODE_HEAT:
|
||||||
|
pkt = this->codec_->get_start_request();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ESP_LOGW(TAG, "Unsupported mode: %d", mode);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_,
|
||||||
|
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||||
|
if (status)
|
||||||
|
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||||
|
}
|
||||||
|
if (call.get_target_temperature().has_value()) {
|
||||||
|
auto pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature());
|
||||||
|
auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_,
|
||||||
|
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||||
|
if (status)
|
||||||
|
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Anova::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->current_temperature = NAN;
|
||||||
|
this->target_temperature = NAN;
|
||||||
|
this->publish_state();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||||
|
auto chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID);
|
||||||
|
if (chr == nullptr) {
|
||||||
|
ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this->char_handle_ = chr->handle;
|
||||||
|
|
||||||
|
auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle);
|
||||||
|
if (status) {
|
||||||
|
ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||||
|
this->node_state = espbt::ClientState::Established;
|
||||||
|
this->current_request_ = 0;
|
||||||
|
this->update();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ESP_GATTC_NOTIFY_EVT: {
|
||||||
|
if (param->notify.handle != this->char_handle_)
|
||||||
|
break;
|
||||||
|
this->codec_->decode(param->notify.value, param->notify.value_len);
|
||||||
|
if (this->codec_->has_target_temp()) {
|
||||||
|
this->target_temperature = this->codec_->target_temp_;
|
||||||
|
}
|
||||||
|
if (this->codec_->has_current_temp()) {
|
||||||
|
this->current_temperature = this->codec_->current_temp_;
|
||||||
|
}
|
||||||
|
if (this->codec_->has_running()) {
|
||||||
|
this->mode = this->codec_->running_ ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_OFF;
|
||||||
|
}
|
||||||
|
this->publish_state();
|
||||||
|
|
||||||
|
if (this->current_request_ > 0) {
|
||||||
|
AnovaPacket *pkt = nullptr;
|
||||||
|
switch (this->current_request_++) {
|
||||||
|
case 1:
|
||||||
|
pkt = this->codec_->get_read_target_temp_request();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
pkt = this->codec_->get_read_current_temp_request();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this->current_request_ = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (pkt != nullptr) {
|
||||||
|
auto status =
|
||||||
|
esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, pkt->length,
|
||||||
|
pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||||
|
if (status)
|
||||||
|
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
|
||||||
|
status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Anova::update() {
|
||||||
|
if (this->node_state != espbt::ClientState::Established)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (this->current_request_ == 0) {
|
||||||
|
auto pkt = this->codec_->get_read_device_status_request();
|
||||||
|
auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_,
|
||||||
|
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||||
|
if (status)
|
||||||
|
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||||
|
this->current_request_++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace anova
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
50
esphome/components/anova/anova.h
Normal file
50
esphome/components/anova/anova.h
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#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"
|
||||||
|
#include "anova_base.h"
|
||||||
|
|
||||||
|
#ifdef ARDUINO_ARCH_ESP32
|
||||||
|
|
||||||
|
#include <esp_gattc_api.h>
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace anova {
|
||||||
|
|
||||||
|
namespace espbt = esphome::esp32_ble_tracker;
|
||||||
|
|
||||||
|
static const uint16_t ANOVA_SERVICE_UUID = 0xFFE0;
|
||||||
|
static const uint16_t ANOVA_CHARACTERISTIC_UUID = 0xFFE1;
|
||||||
|
|
||||||
|
class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent {
|
||||||
|
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 dump_config() override;
|
||||||
|
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||||
|
climate::ClimateTraits traits() {
|
||||||
|
auto traits = climate::ClimateTraits();
|
||||||
|
traits.set_supports_current_temperature(true);
|
||||||
|
traits.set_supports_heat_mode(true);
|
||||||
|
traits.set_visual_min_temperature(25.0);
|
||||||
|
traits.set_visual_max_temperature(100.0);
|
||||||
|
traits.set_visual_temperature_step(0.1);
|
||||||
|
return traits;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AnovaCodec *codec_;
|
||||||
|
void control(const climate::ClimateCall &call) override;
|
||||||
|
uint16_t char_handle_;
|
||||||
|
uint8_t current_request_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace anova
|
||||||
|
} // namespace esphome
|
||||||
|
|
||||||
|
#endif
|
119
esphome/components/anova/anova_base.cpp
Normal file
119
esphome/components/anova/anova_base.cpp
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
#include "anova_base.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace anova {
|
||||||
|
|
||||||
|
AnovaPacket *AnovaCodec::clean_packet_() {
|
||||||
|
this->packet_.length = strlen((char *) this->packet_.data);
|
||||||
|
this->packet_.data[this->packet_.length] = '\0';
|
||||||
|
ESP_LOGV("anova", "SendPkt: %s\n", this->packet_.data);
|
||||||
|
return &this->packet_;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnovaPacket *AnovaCodec::get_read_device_status_request() {
|
||||||
|
this->current_query_ = READ_DEVICE_STATUS;
|
||||||
|
sprintf((char *) this->packet_.data, "%s", CMD_READ_DEVICE_STATUS);
|
||||||
|
return this->clean_packet_();
|
||||||
|
}
|
||||||
|
|
||||||
|
AnovaPacket *AnovaCodec::get_read_target_temp_request() {
|
||||||
|
this->current_query_ = READ_TARGET_TEMPERATURE;
|
||||||
|
sprintf((char *) this->packet_.data, "%s", CMD_READ_TARGET_TEMP);
|
||||||
|
return this->clean_packet_();
|
||||||
|
}
|
||||||
|
|
||||||
|
AnovaPacket *AnovaCodec::get_read_current_temp_request() {
|
||||||
|
this->current_query_ = READ_CURRENT_TEMPERATURE;
|
||||||
|
sprintf((char *) this->packet_.data, "%s", CMD_READ_CURRENT_TEMP);
|
||||||
|
return this->clean_packet_();
|
||||||
|
}
|
||||||
|
|
||||||
|
AnovaPacket *AnovaCodec::get_read_unit_request() {
|
||||||
|
this->current_query_ = READ_UNIT;
|
||||||
|
sprintf((char *) this->packet_.data, "%s", CMD_READ_UNIT);
|
||||||
|
return this->clean_packet_();
|
||||||
|
}
|
||||||
|
|
||||||
|
AnovaPacket *AnovaCodec::get_read_data_request() {
|
||||||
|
this->current_query_ = READ_DATA;
|
||||||
|
sprintf((char *) this->packet_.data, "%s", CMD_READ_DATA);
|
||||||
|
return this->clean_packet_();
|
||||||
|
}
|
||||||
|
|
||||||
|
AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) {
|
||||||
|
this->current_query_ = SET_TARGET_TEMPERATURE;
|
||||||
|
sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature);
|
||||||
|
return this->clean_packet_();
|
||||||
|
}
|
||||||
|
|
||||||
|
AnovaPacket *AnovaCodec::get_set_unit_request(char unit) {
|
||||||
|
this->current_query_ = SET_UNIT;
|
||||||
|
sprintf((char *) this->packet_.data, CMD_SET_TEMP_UNIT, unit);
|
||||||
|
return this->clean_packet_();
|
||||||
|
}
|
||||||
|
|
||||||
|
AnovaPacket *AnovaCodec::get_start_request() {
|
||||||
|
this->current_query_ = START;
|
||||||
|
sprintf((char *) this->packet_.data, CMD_START);
|
||||||
|
return this->clean_packet_();
|
||||||
|
}
|
||||||
|
|
||||||
|
AnovaPacket *AnovaCodec::get_stop_request() {
|
||||||
|
this->current_query_ = STOP;
|
||||||
|
sprintf((char *) this->packet_.data, CMD_STOP);
|
||||||
|
return this->clean_packet_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnovaCodec::decode(const uint8_t *data, uint16_t length) {
|
||||||
|
memset(this->buf_, 0, 32);
|
||||||
|
strncpy(this->buf_, (char *) data, length);
|
||||||
|
ESP_LOGV("anova", "Received: %s\n", this->buf_);
|
||||||
|
this->has_target_temp_ = this->has_current_temp_ = this->has_unit_ = this->has_running_ = false;
|
||||||
|
switch (this->current_query_) {
|
||||||
|
case READ_DEVICE_STATUS: {
|
||||||
|
if (!strncmp(this->buf_, "stopped", 7)) {
|
||||||
|
this->has_running_ = true;
|
||||||
|
this->running_ = false;
|
||||||
|
}
|
||||||
|
if (!strncmp(this->buf_, "running", 7)) {
|
||||||
|
this->has_running_ = true;
|
||||||
|
this->running_ = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case START: {
|
||||||
|
if (!strncmp(this->buf_, "start", 5)) {
|
||||||
|
this->has_running_ = true;
|
||||||
|
this->running_ = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case STOP: {
|
||||||
|
if (!strncmp(this->buf_, "stop", 4)) {
|
||||||
|
this->has_running_ = true;
|
||||||
|
this->running_ = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case READ_TARGET_TEMPERATURE: {
|
||||||
|
this->target_temp_ = strtof(this->buf_, nullptr);
|
||||||
|
this->has_target_temp_ = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SET_TARGET_TEMPERATURE: {
|
||||||
|
this->target_temp_ = strtof(this->buf_, nullptr);
|
||||||
|
this->has_target_temp_ = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case READ_CURRENT_TEMPERATURE: {
|
||||||
|
this->current_temp_ = strtof(this->buf_, nullptr);
|
||||||
|
this->has_current_temp_ = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace anova
|
||||||
|
} // namespace esphome
|
79
esphome/components/anova/anova_base.h
Normal file
79
esphome/components/anova/anova_base.h
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace anova {
|
||||||
|
|
||||||
|
enum CurrentQuery {
|
||||||
|
NONE,
|
||||||
|
READ_DEVICE_STATUS,
|
||||||
|
READ_TARGET_TEMPERATURE,
|
||||||
|
READ_CURRENT_TEMPERATURE,
|
||||||
|
READ_DATA,
|
||||||
|
READ_UNIT,
|
||||||
|
SET_TARGET_TEMPERATURE,
|
||||||
|
SET_UNIT,
|
||||||
|
START,
|
||||||
|
STOP,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AnovaPacket {
|
||||||
|
uint16_t length;
|
||||||
|
uint8_t data[24];
|
||||||
|
};
|
||||||
|
|
||||||
|
#define CMD_READ_DEVICE_STATUS "status\r"
|
||||||
|
#define CMD_READ_TARGET_TEMP "read set temp\r"
|
||||||
|
#define CMD_READ_CURRENT_TEMP "read temp\r"
|
||||||
|
#define CMD_READ_UNIT "read unit\r"
|
||||||
|
#define CMD_READ_DATA "read data\r"
|
||||||
|
#define CMD_SET_TARGET_TEMP "set temp %.1f\r"
|
||||||
|
#define CMD_SET_TEMP_UNIT "set unit %c\r"
|
||||||
|
|
||||||
|
#define CMD_START "start\r"
|
||||||
|
#define CMD_STOP "stop\r"
|
||||||
|
|
||||||
|
class AnovaCodec {
|
||||||
|
public:
|
||||||
|
AnovaPacket *get_read_device_status_request();
|
||||||
|
AnovaPacket *get_read_target_temp_request();
|
||||||
|
AnovaPacket *get_read_current_temp_request();
|
||||||
|
AnovaPacket *get_read_data_request();
|
||||||
|
AnovaPacket *get_read_unit_request();
|
||||||
|
|
||||||
|
AnovaPacket *get_set_target_temp_request(float temperature);
|
||||||
|
AnovaPacket *get_set_unit_request(char unit);
|
||||||
|
|
||||||
|
AnovaPacket *get_start_request();
|
||||||
|
AnovaPacket *get_stop_request();
|
||||||
|
|
||||||
|
void decode(const uint8_t *data, uint16_t length);
|
||||||
|
bool has_target_temp() { return this->has_target_temp_; }
|
||||||
|
bool has_current_temp() { return this->has_current_temp_; }
|
||||||
|
bool has_unit() { return this->has_unit_; }
|
||||||
|
bool has_running() { return this->has_running_; }
|
||||||
|
|
||||||
|
union {
|
||||||
|
float target_temp_;
|
||||||
|
float current_temp_;
|
||||||
|
char unit_;
|
||||||
|
bool running_;
|
||||||
|
};
|
||||||
|
|
||||||
|
protected:
|
||||||
|
AnovaPacket *clean_packet_();
|
||||||
|
AnovaPacket packet_;
|
||||||
|
|
||||||
|
bool has_target_temp_;
|
||||||
|
bool has_current_temp_;
|
||||||
|
bool has_unit_;
|
||||||
|
bool has_running_;
|
||||||
|
char buf_[32];
|
||||||
|
|
||||||
|
CurrentQuery current_query_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace anova
|
||||||
|
} // namespace esphome
|
25
esphome/components/anova/climate.py
Normal file
25
esphome/components/anova/climate.py
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
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 = ["@buxtronix"]
|
||||||
|
DEPENDENCIES = ["ble_client"]
|
||||||
|
|
||||||
|
anova_ns = cg.esphome_ns.namespace("anova")
|
||||||
|
Anova = anova_ns.class_(
|
||||||
|
"Anova", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
climate.CLIMATE_SCHEMA.extend({cv.GenerateID(): cv.declare_id(Anova)})
|
||||||
|
.extend(ble_client.BLE_CLIENT_SCHEMA)
|
||||||
|
.extend(cv.polling_component_schema("60s"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
yield cg.register_component(var, config)
|
||||||
|
yield climate.register_climate(var, config)
|
||||||
|
yield ble_client.register_ble_node(var, config)
|
|
@ -1526,6 +1526,26 @@ climate:
|
||||||
name: Toshiba Climate
|
name: Toshiba Climate
|
||||||
- platform: hitachi_ac344
|
- platform: hitachi_ac344
|
||||||
name: Hitachi Climate
|
name: Hitachi Climate
|
||||||
|
- platform: midea_ac
|
||||||
|
visual:
|
||||||
|
min_temperature: 18 °C
|
||||||
|
max_temperature: 25 °C
|
||||||
|
temperature_step: 0.1 °C
|
||||||
|
name: 'Electrolux EACS'
|
||||||
|
beeper: true
|
||||||
|
outdoor_temperature:
|
||||||
|
name: 'Temp'
|
||||||
|
power_usage:
|
||||||
|
name: 'Power'
|
||||||
|
humidity_setpoint:
|
||||||
|
name: 'Hum'
|
||||||
|
- platform: anova
|
||||||
|
name: Anova cooker
|
||||||
|
ble_client_id: ble_blah
|
||||||
|
|
||||||
|
midea_dongle:
|
||||||
|
uart_id: uart0
|
||||||
|
strength_icon: true
|
||||||
|
|
||||||
switch:
|
switch:
|
||||||
- platform: gpio
|
- platform: gpio
|
||||||
|
|
Loading…
Reference in a new issue