1
0
mirror of https://github.com/esphome/esphome.git synced 2025-04-15 07:10:33 +01:00

Add diesel heater BLE component

This commit is contained in:
Mateusz Wójcik 2025-01-03 16:21:25 +01:00
parent 56e305f986
commit 45766a1a32
12 changed files with 1115 additions and 0 deletions

View File

@ -0,0 +1,29 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import ble_client
from esphome.components import sensor
from esphome.const import CONF_ID
CODEOWNERS = ["@warehog"]
DEPENDENCIES = ["ble_client"]
CONF_HEATER_ID = "diesel_heater_ble"
diesel_heater_ble_ns = cg.esphome_ns.namespace("diesel_heater_ble")
DieselHeaterBLE = diesel_heater_ble_ns.class_("DieselHeaterBLE", sensor.Sensor, cg.Component, ble_client.BLEClientNode)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DieselHeaterBLE)
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(ble_client.BLE_CLIENT_SCHEMA)
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await ble_client.register_ble_node(var, config)

View File

@ -0,0 +1,12 @@
#pragma once
#include "esphome/core/component.h"
#include "heater.h"
#include "messages.h"
namespace esphome {
namespace diesel_heater_ble {
} // namespace diesel_heater_ble
} // namespace esphome

View File

@ -0,0 +1,24 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
CONF_POWER,
ICON_POWER,
DEVICE_CLASS_BUTTON
)
from . import DieselHeaterBLE, CONF_HEATER_ID, diesel_heater_ble_ns
CODEOWNERS = ["@warehog"]
DEPENDENCIES = ["diesel_heater_ble"]
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(CONF_HEATER_ID): cv.use_id(DieselHeaterBLE),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_HEATER_ID])

View File

@ -0,0 +1,219 @@
#include "heater.h"
#ifdef USE_ESP32
namespace esphome {
namespace diesel_heater_ble {
const char *TAG = "diesel_heater_ble";
void DieselHeaterBLE::loop() {
if (this->node_state == esp32_ble_tracker::ClientState::ESTABLISHED) {
if (this->last_request_ + 1000 < millis()) {
this->last_request_ = millis();
uint8_t data[8] = {0xaa, 0x55, 0x0c, 0x22, 0x01, 0x00, 0x00, 0x2f};
this->ble_write_chr(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), this->handle_, data, sizeof(data));
}
this->update_sensors(this->state_);
}
}
void DieselHeaterBLE::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_OPEN_EVT:
ESP_LOGD(TAG, "GATT client opened.");
break;
case ESP_GATTC_DISCONNECT_EVT:
ESP_LOGD(TAG, "GATT client disconnected.");
this->node_state = esp32_ble_tracker::ClientState::DISCONNECTING;
this->handle_ = 0;
break;
case ESP_GATTC_SEARCH_CMPL_EVT:
if (param->search_cmpl.status != ESP_GATT_OK) {
ESP_LOGD(TAG, "Service search failed, status: %d", param->search_cmpl.status);
break;
}
this->ble_register_for_notify(this->parent()->get_gattc_if(), this->parent()->get_remote_bda());
break;
case ESP_GATTC_REG_FOR_NOTIFY_EVT:
if (param->reg_for_notify.status != ESP_GATT_OK) {
ESP_LOGD(TAG, "Register for notify failed, status: %d", param->reg_for_notify.status);
break;
}
this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED;
break;
case ESP_GATTC_NOTIFY_EVT:
if (param->notify.conn_id != this->parent()->get_conn_id()) break;
if (param->notify.handle == this->handle_) {
std::vector<uint8_t> data(param->notify.value, param->notify.value + param->notify.value_len);
this->on_notification_received(data);
}
break;
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_WRITE_CHAR_EVT:
break;
default:
ESP_LOGD(TAG, "Unhandled GATT event: %d", event);
break;
}
}
bool DieselHeaterBLE::ble_write_chr(esp_gatt_if_t gattc_if, esp_bd_addr_t remote_bda, uint16_t handle, uint8_t *data, uint16_t len) {
esp_err_t ret = esp_ble_gattc_write_char(gattc_if, 0, handle, len, data, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
if (ret != ESP_OK) {
ESP_LOGD(TAG, "Write characteristic failed, status: %d", ret);
return false;
} else {
ESP_LOGD(TAG, "Write characteristic success: %s", format_hex_pretty(std::vector<uint8_t>(data, data + len)).c_str());
}
return true;
}
bool DieselHeaterBLE::ble_read_chr(esp_gatt_if_t gattc_if, esp_bd_addr_t remote_bda, uint16_t handle) {
esp_err_t ret = esp_ble_gattc_read_char(gattc_if, 0, handle, ESP_GATT_AUTH_REQ_NONE);
if (ret != ESP_OK) {
ESP_LOGD(TAG, "Read characteristic failed, status: %d", ret);
return false;
}
return true;
}
bool DieselHeaterBLE::ble_register_for_notify(esp_gatt_if_t gattc_if, esp_bd_addr_t remote_bda) {
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->characteristic_uuid_);
if (!chr) {
ESP_LOGD(TAG, "Characteristic not found.");
return false;
}
this->handle_ = chr->handle;
esp_err_t ret = esp_ble_gattc_register_for_notify(gattc_if, remote_bda, this->handle_);
if (ret != ESP_OK) {
ESP_LOGD(TAG, "Register for notify failed, status: %d", ret);
return false;
}
return true;
}
void DieselHeaterBLE::on_notification_received(const std::vector<uint8_t> &data) {
// ESP_LOGD(TAG, "Notification received: %s", format_hex_pretty(data).c_str());
bool ret = ResponseParser::parse(data, this->state_);
if (!ret) {
ESP_LOGD(TAG, "Failed to parse response.");
return;
}
}
void DieselHeaterBLE::update_sensors(const HeaterState &new_state) {
if (running_state_ != nullptr && running_state_->state != new_state.runningstate) {
if (this->power_switch_ != nullptr) {
this->power_switch_->publish_state(new_state.runningstate);
}
running_state_->publish_state(new_state.runningstate);
return;
}
if (error_code_ != nullptr && error_code_->state != new_state.errcode) {
error_code_->publish_state(new_state.errcode);
return;
}
if (running_step_ != nullptr && running_step_->state != new_state.runningstep) {
running_step_->publish_state(new_state.runningstep);
return;
}
if (altitude_ != nullptr && altitude_->state != new_state.altitude) {
altitude_->publish_state(new_state.altitude);
return;
}
if (running_mode_ != nullptr && running_mode_->state != new_state.runningmode) {
running_mode_->publish_state(new_state.runningmode);
return;
}
if (set_level_ != nullptr && set_level_->state != new_state.setlevel) {
if (this->power_level_number_ != nullptr) {
this->power_level_number_->publish_state(new_state.setlevel);
}
set_level_->publish_state(new_state.setlevel);
return;
}
if (set_temp_ != nullptr && set_temp_->state != new_state.settemp) {
if (this->set_temp_number_ != nullptr) {
this->set_temp_number_->publish_state(new_state.settemp);
}
set_temp_->publish_state(new_state.settemp);
return;
}
if (set_temp_number_ != nullptr && set_temp_number_->state != new_state.settemp) {
set_temp_number_->publish_state(new_state.settemp);
return;
}
if (supply_voltage_ != nullptr && supply_voltage_->state != new_state.supplyvoltage) {
supply_voltage_->publish_state(new_state.supplyvoltage);
return;
}
if (case_temp_ != nullptr && case_temp_->state != new_state.casetemp) {
case_temp_->publish_state(new_state.casetemp);
return;
}
if (cab_temp_ != nullptr && cab_temp_->state != new_state.cabtemp) {
cab_temp_->publish_state(new_state.cabtemp);
return;
}
if (start_time_ != nullptr && start_time_->state != new_state.sttime) {
start_time_->publish_state(new_state.sttime);
return;
}
if (auto_time_ != nullptr && auto_time_->state != new_state.autotime) {
auto_time_->publish_state(new_state.autotime);
return;
}
if (run_time_ != nullptr && run_time_->state != new_state.runtime) {
run_time_->publish_state(new_state.runtime);
return;
}
if (is_auto_ != nullptr && is_auto_->state != new_state.isauto) {
is_auto_->publish_state(new_state.isauto);
return;
}
// if (language_ != nullptr && language_->state != new_state.language) {
// language_->publish_state(new_state.language);
// return;
// }
if (temp_offset_ != nullptr && temp_offset_->state != new_state.tempoffset) {
temp_offset_->publish_state(new_state.tempoffset);
return;
}
if (tank_volume_ != nullptr && tank_volume_->state != new_state.tankvolume) {
tank_volume_->publish_state(new_state.tankvolume);
return;
}
if (oil_pump_type_ != nullptr && oil_pump_type_->state != new_state.oilpumptype) {
oil_pump_type_->publish_state(new_state.oilpumptype);
return;
}
if (rf433_on_off_ != nullptr && rf433_on_off_->state != static_cast<float>(new_state.rf433onoff)) {
rf433_on_off_->publish_state(new_state.rf433onoff);
return;
}
if (temp_unit_ != nullptr && temp_unit_->state != new_state.tempunit) {
temp_unit_->publish_state(new_state.tempunit);
return;
}
if (altitude_unit_ != nullptr && altitude_unit_->state != new_state.altiunit) {
altitude_unit_->publish_state(new_state.altiunit);
return;
}
if (automatic_heating_ != nullptr && automatic_heating_->state != new_state.automaticheating) {
automatic_heating_->publish_state(new_state.automaticheating);
return;
}
}
} // namespace diesel_heater_ble
} // namespace esphome
#endif

View File

@ -0,0 +1,120 @@
#pragma once
#ifdef USE_ESP32
#include <esp_gattc_api.h>
#include <algorithm>
#include <iterator>
#include "esphome/components/ble_client/ble_client.h"
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/number/number.h"
#include "esphome/components/switch/switch.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h"
#include "messages.h"
#include "state.h"
namespace esphome {
namespace diesel_heater_ble {
class DieselHeaterBLE : public Component, public ble_client::BLEClientNode {
public:
DieselHeaterBLE() = default;
void loop() 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 sent_request(const std::vector<uint8_t> &data) {
std::vector<uint8_t> data_(data);
this->ble_write_chr(this->parent()->get_gattc_if(), this->parent()->get_remote_bda(), this->handle_, data_.data(), data.size());
}
bool ble_write_chr(esp_gatt_if_t gattc_if, esp_bd_addr_t remote_bda, uint16_t handle, uint8_t *data, uint16_t len);
bool ble_read_chr(esp_gatt_if_t gattc_if, esp_bd_addr_t remote_bda, uint16_t handle);
bool ble_register_for_notify(esp_gatt_if_t gattc_if, esp_bd_addr_t remote_bda);
void on_notification_received(const std::vector<uint8_t> &data);
void update_sensors(const HeaterState &new_state);
// Sensor setters
void set_running_state(sensor::Sensor *sensor) { running_state_ = sensor; }
void set_error_code(sensor::Sensor *sensor) { error_code_ = sensor; }
void set_running_step(sensor::Sensor *sensor) { running_step_ = sensor; }
void set_altitude(sensor::Sensor *sensor) { altitude_ = sensor; }
void set_running_mode(sensor::Sensor *sensor) { running_mode_ = sensor; }
void set_set_level(sensor::Sensor *sensor) { set_level_ = sensor; }
void set_set_temp(sensor::Sensor *sensor) { set_temp_ = sensor; }
void set_supply_voltage(sensor::Sensor *sensor) { supply_voltage_ = sensor; }
void set_case_temp(sensor::Sensor *sensor) { case_temp_ = sensor; }
void set_cab_temp(sensor::Sensor *sensor) { cab_temp_ = sensor; }
void set_start_time(sensor::Sensor *sensor) { start_time_ = sensor; }
void set_auto_time(sensor::Sensor *sensor) { auto_time_ = sensor; }
void set_run_time(sensor::Sensor *sensor) { run_time_ = sensor; }
void set_is_auto(sensor::Sensor *sensor) { is_auto_ = sensor; }
void set_language(sensor::Sensor *sensor) { language_ = sensor; }
void set_temp_offset(sensor::Sensor *sensor) { temp_offset_ = sensor; }
void set_tank_volume(sensor::Sensor *sensor) { tank_volume_ = sensor; }
void set_oil_pump_type(sensor::Sensor *sensor) { oil_pump_type_ = sensor; }
void set_rf433_on_off(sensor::Sensor *sensor) { rf433_on_off_ = sensor; }
void set_temp_unit(sensor::Sensor *sensor) { temp_unit_ = sensor; }
void set_altitude_unit(sensor::Sensor *sensor) { altitude_unit_ = sensor; }
void set_automatic_heating(sensor::Sensor *sensor) { automatic_heating_ = sensor; }
void set_power_level_number(number::Number *number) { power_level_number_ = number; }
void set_set_temp_number(number::Number *number) { set_temp_number_ = number; }
void set_power_switch(switch_::Switch *sw) { power_switch_ = sw; }
HeaterState get_state() {
return this->state_;
}
protected:
uint16_t handle_{0};
esp32_ble_tracker::ESPBTUUID service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw("0000ffe0-0000-1000-8000-00805f9b34fb");
esp32_ble_tracker::ESPBTUUID characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw("0000ffe1-0000-1000-8000-00805f9b34fb");
HeaterState state_;
bool response_received_{false};
uint32_t last_request_{0};
uint32_t last_update_{0};
// Sensor fields
sensor::Sensor *running_state_{};
sensor::Sensor *error_code_{};
sensor::Sensor *running_step_{};
sensor::Sensor *altitude_{};
sensor::Sensor *running_mode_{};
sensor::Sensor *set_level_{};
sensor::Sensor *set_temp_{};
sensor::Sensor *supply_voltage_{};
sensor::Sensor *case_temp_{};
sensor::Sensor *cab_temp_{};
sensor::Sensor *start_time_{};
sensor::Sensor *auto_time_{};
sensor::Sensor *run_time_{};
sensor::Sensor *is_auto_{};
sensor::Sensor *language_{};
sensor::Sensor *temp_offset_{};
sensor::Sensor *tank_volume_{};
sensor::Sensor *oil_pump_type_{};
sensor::Sensor *rf433_on_off_{};
sensor::Sensor *temp_unit_{};
sensor::Sensor *altitude_unit_{};
sensor::Sensor *automatic_heating_{};
number::Number *power_level_number_{};
number::Number *set_temp_number_{};
switch_::Switch *power_switch_{};
};
} // namespace diesel_heater_ble
} // namespace esphome
#endif // USE_ESP32

View File

@ -0,0 +1,230 @@
#pragma once
#include <iostream>
#include <vector>
#include <cstdint>
#include <stdexcept>
#include "state.h"
namespace esphome {
namespace diesel_heater_ble {
class ResponseParser {
public:
static std::vector<uint8_t> decrypt(const std::vector<uint8_t> &raw) {
// decrypt only if raw data starts with [0xDA, 0x07]
if (raw[0] == 0xAA) {
return raw;
}
std::vector<uint8_t> decrypted = raw;
for (size_t i = 0; i < 48; i += 8) {
decrypted[i] ^= 112; // "p" in ASCII
decrypted[i + 1] ^= 97; // "a" in ASCII
decrypted[i + 2] ^= 115; // "s" in ASCII
decrypted[i + 3] ^= 115; // "s" in ASCII
decrypted[i + 4] ^= 119; // "w" in ASCII
decrypted[i + 5] ^= 111; // "o" in ASCII
decrypted[i + 6] ^= 114; // "r" in ASCII
decrypted[i + 7] ^= 100; // "d" in ASCII
}
return decrypted;
}
static HeaterClass detect_heater_class(const std::vector<uint8_t> &raw) {
if(raw[0] == 0xAA) {
return raw[1] == 0x55 ? HeaterClass::HEATER_AA_55 : HeaterClass::HEATER_AA_66;
} else if (raw[0] == 0xDA) {
std::vector<uint8_t> decrypted = ResponseParser::decrypt(raw);
if (decrypted[1] == 0x55) {
return HeaterClass::HEATER_AA_55_ENCRYPTED;
} else if (decrypted[1] == 0x66) {
return HeaterClass::HEATER_AA_66_ENCRYPTED;
} else {
return HeaterClass::HEATER_CLASS_UNKNOWN;
}
} else {
return HeaterClass::HEATER_CLASS_UNKNOWN;
}
}
static bool parse(const std::vector<uint8_t> &raw, HeaterState &state) {
HeaterClass heater_class = detect_heater_class(raw);
if (heater_class == HeaterClass::HEATER_CLASS_UNKNOWN) return false;
std::vector<uint8_t> decrypted = decrypt(raw);
state.heater_class = heater_class;
state.rcv_cmd = decrypted[2];
state.runningstate = decrypted[3];
if (heater_class == HeaterClass::HEATER_AA_55 || heater_class == HeaterClass::HEATER_AA_55_ENCRYPTED) {
state.errcode = decrypted[4];
} else if (heater_class == HeaterClass::HEATER_AA_66) {
state.errcode = decrypted[17];
} else if (heater_class == HeaterClass::HEATER_AA_66_ENCRYPTED) {
state.errcode = decrypted[35];
}
state.runningstep = decrypted[5];
if (heater_class == HeaterClass::HEATER_AA_55 || heater_class == HeaterClass::HEATER_AA_66 ) {
state.altitude = decrypted[6] + (decrypted[7] << 8);
} else if (heater_class == HeaterClass::HEATER_AA_55_ENCRYPTED || heater_class == HeaterClass::HEATER_AA_66_ENCRYPTED) {
state.altitude = (decrypted[7] + (decrypted[6] << 8)) / 10;
}
state.runningmode = decrypted[8];
if (heater_class == HeaterClass::HEATER_AA_55 || heater_class == HeaterClass::HEATER_AA_66 ) {
if (state.runningmode == 0x00) {
state.setlevel = decrypted[10] + 1;
} else if (state.runningmode == 0x01) {
state.setlevel = decrypted[9];
} else if (state.runningmode == 0x02) {
state.settemp = decrypted[9];
state.setlevel = decrypted[10] + 1;
}
} else if (heater_class == HeaterClass::HEATER_AA_55_ENCRYPTED || heater_class == HeaterClass::HEATER_AA_66_ENCRYPTED) {
state.setlevel = decrypted[10];
state.settemp = decrypted[9];
}
if (heater_class == HeaterClass::HEATER_AA_55 || heater_class == HeaterClass::HEATER_AA_66 ) {
state.supplyvoltage = (decrypted[11] + (decrypted[12] << 8)) / 10;
} else if (heater_class == HeaterClass::HEATER_AA_55_ENCRYPTED || heater_class == HeaterClass::HEATER_AA_66_ENCRYPTED) {
state.supplyvoltage = (decrypted[12] + (decrypted[11] << 8)) / 10;
}
if (heater_class == HeaterClass::HEATER_AA_55 || heater_class == HeaterClass::HEATER_AA_66 ) {
state.casetemp = (decrypted[13] + (decrypted[14] << 8));
state.cabtemp = (decrypted[15] + (decrypted[16] << 8));
} else if (heater_class == HeaterClass::HEATER_AA_55_ENCRYPTED || heater_class == HeaterClass::HEATER_AA_66_ENCRYPTED) {
state.casetemp = (decrypted[14] + (decrypted[13] << 8));
state.cabtemp = (decrypted[33] + (decrypted[32] << 8)) / 10;
}
// encrypted types only
if (heater_class == HeaterClass::HEATER_AA_55_ENCRYPTED || heater_class == HeaterClass::HEATER_AA_66_ENCRYPTED) {
state.sttime = decrypted[20] + (decrypted[19] << 8);
state.autotime = decrypted[22] + (decrypted[21] << 8);
state.runtime = decrypted[24] + (decrypted[23] << 8);
state.isauto = decrypted[25];
state.language = decrypted[26];
state.tempoffset = decrypted[34];
state.tankvolume = decrypted[28];
state.oilpumptype = decrypted[29];
if (raw[29] == 20) {
state.rf433onoff = false;
} else if (raw[29] == 21) {
state.rf433onoff = true;
}
state.tempunit = decrypted[27];
state.altiunit = decrypted[30];
state.automaticheating = decrypted[31];
}
return true;
}
};
class Request {
public:
uint8_t header_1 = 0xAA;
uint8_t header_2 = 0x55;
uint8_t password_1 = 0x0C;
uint8_t password_2 = 0x22;
uint8_t command;
uint8_t data_1;
uint8_t data_2;
uint8_t checksum;
Request(uint8_t command, uint8_t data1 = 0x00, uint8_t data2 = 0x00)
: command(command), data_1(data1), data_2(data2) {
calculateChecksum();
}
void calculateChecksum() {
checksum = (password_1 + password_2 + command + data_1 + data_2) % 256;
}
std::vector<uint8_t> toBytes() const {
return {header_1, header_2, password_1, password_2, command, data_1, data_2, checksum};
}
};
// Specific Requests
class StatusRequest : public Request {
public:
StatusRequest() : Request(0x01) {}
};
class SetPowerRequest : public Request {
public:
SetPowerRequest(bool enable)
: Request(0x03, enable ? 0x01: 0x00, 0x00) {}
};
class SetTemperatureRequest : public Request {
public:
SetTemperatureRequest(uint8_t temperature)
: Request(0x04, temperature, 0x00) {}
};
class SetLevelRequest : public Request {
public:
SetLevelRequest(uint8_t level)
: Request(0x04, level - 1, 0x00) {}
};
class SetRunningModeRequest : public Request {
public:
SetRunningModeRequest(uint8_t mode)
: Request(0x02, mode, 0x00) {}
};
class SetAutomaticStartStopRequest : public Request {
public:
SetAutomaticStartStopRequest(bool enable)
: Request(0x13, enable ? 0x01 : 0x00, 0x00) {}
};
class SetLanguageRequest : public Request {
public:
SetLanguageRequest(uint8_t languageCode)
: Request(0x14, languageCode, 0x00) {}
};
class SetTemperatureUnitRequest : public Request {
public:
SetTemperatureUnitRequest(bool isCelsius)
: Request(0x15, isCelsius ? 0x01 : 0x00, 0x00) {}
};
class SetAltitudeUnitRequest : public Request {
public:
SetAltitudeUnitRequest(bool isMeters)
: Request(0x16, isMeters ? 0x01 : 0x00, 0x00) {}
};
class SetTankVolumeRequest : public Request {
public:
SetTankVolumeRequest(uint8_t volume)
: Request(0x17, volume, 0x00) {}
};
class SetOilPumpTypeRequest : public Request {
public:
SetOilPumpTypeRequest(uint8_t type)
: Request(0x18, type, 0x00) {}
};
class SetTemperatureOffsetRequest : public Request {
public:
SetTemperatureOffsetRequest(uint8_t offset)
: Request(0x20, offset, 0x00) {}
};
} // namespace diesel_heater_ble
} // namespace esphome

View File

@ -0,0 +1,39 @@
#pragma once
#include "esphome/components/number/number.h"
#include "esphome/core/component.h"
#include "heater.h"
#include "messages.h"
namespace esphome {
namespace diesel_heater_ble {
class PowerLevelNumber : public number::Number, public Parented<DieselHeaterBLE> {
public:
PowerLevelNumber() = default;
protected:
void control(float value) override {
if (this->parent_->get_state().runningmode == 2) {
this->parent_->sent_request(SetRunningModeRequest(1).toBytes());
}
this->parent_->sent_request(SetLevelRequest(value + 1).toBytes());
}
};
class SetTempNumber : public number::Number, public Parented<DieselHeaterBLE> {
public:
SetTempNumber() = default;
protected:
void control(float value) override {
if (this->parent_->get_state().runningmode == 1) {
this->parent_->sent_request(SetRunningModeRequest(2).toBytes());
}
this->parent_->sent_request(SetTemperatureRequest(value).toBytes());
}
};
} // namespace diesel_heater_ble
} // namespace esphome

View File

@ -0,0 +1,42 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import number
from esphome.const import (
CONF_TEMPERATURE
)
from . import DieselHeaterBLE, CONF_HEATER_ID, diesel_heater_ble_ns
CODEOWNERS = ["@warehog"]
DEPENDENCIES = ["diesel_heater_ble"]
AUTO_LOAD = ["number"]
CONF_POWER_LEVEL = "power_level"
PowerLevelNumber = diesel_heater_ble_ns.class_("PowerLevelNumber", number.Number)
CONF_SET_TEMP = "set_temp"
SetTempNumber = diesel_heater_ble_ns.class_("SetTempNumber", number.Number)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(CONF_HEATER_ID): cv.use_id(DieselHeaterBLE),
cv.Optional(CONF_POWER_LEVEL): number.number_schema(PowerLevelNumber),
cv.Optional(CONF_SET_TEMP): number.number_schema(SetTempNumber),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_HEATER_ID])
# CONF_POWER_LEVEL
if conf := config.get(CONF_POWER_LEVEL):
b = await number.new_number(conf, min_value=1, max_value=10, step=1)
await cg.register_parented(b, parent)
cg.add(getattr(parent, f"set_{CONF_POWER_LEVEL}_number")(b))
if conf := config.get(CONF_SET_TEMP):
b = await number.new_number(conf, min_value=8, max_value=36, step=1)
await cg.register_parented(b, parent)
cg.add(getattr(parent, f"set_{CONF_SET_TEMP}_number")(b))

View File

@ -0,0 +1,217 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import ble_client
from esphome.components import sensor
from esphome.const import (
CONF_ID,
UNIT_VOLT,
UNIT_CELSIUS,
UNIT_PERCENT,
UNIT_SECOND,
UNIT_METER,
UNIT_EMPTY,
DEVICE_CLASS_TEMPERATURE,
STATE_CLASS_MEASUREMENT,
DEVICE_CLASS_EMPTY
)
from . import DieselHeaterBLE, CONF_HEATER_ID
CODEOWNERS = ["@warehog"]
DEPENDENCIES = ["diesel_heater_ble"]
AUTO_LOAD = ["sensor"]
# Sensor configurations
CONF_RUNNING_STATE = "running_state"
CONF_ERROR_CODE = "error_code"
CONF_RUNNING_STEP = "running_step"
CONF_ALTITUDE = "altitude"
CONF_RUNNING_MODE = "running_mode"
CONF_SET_LEVEL = "set_level"
CONF_SET_TEMP = "set_temp"
CONF_SUPPLY_VOLTAGE = "supply_voltage"
CONF_CASE_TEMP = "case_temp"
CONF_CAB_TEMP = "cab_temp"
CONF_START_TIME = "start_time"
CONF_AUTO_TIME = "auto_time"
CONF_RUN_TIME = "run_time"
CONF_IS_AUTO = "is_auto"
CONF_LANGUAGE = "language"
CONF_TEMP_OFFSET = "temp_offset"
CONF_TANK_VOLUME = "tank_volume"
CONF_OIL_PUMP_TYPE = "oil_pump_type"
CONF_RF433_ON_OFF = "rf433_on_off"
CONF_TEMP_UNIT = "temp_unit"
CONF_ALTITUDE_UNIT = "altitude_unit"
CONF_AUTOMATIC_HEATING = "automatic_heating"
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(CONF_HEATER_ID): cv.use_id(DieselHeaterBLE),
cv.Optional(CONF_RUNNING_STATE): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=1,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ERROR_CODE): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_RUNNING_STEP): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ALTITUDE): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_RUNNING_MODE): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SET_LEVEL): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SET_TEMP): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT
),
cv.Optional(CONF_SUPPLY_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CASE_TEMP): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT
),
cv.Optional(CONF_CAB_TEMP): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_START_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_SECOND,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTO_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_SECOND,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_RUN_TIME): sensor.sensor_schema(
unit_of_measurement=UNIT_SECOND,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_IS_AUTO): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_LANGUAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMP_OFFSET): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT
),
cv.Optional(CONF_TANK_VOLUME): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_OIL_PUMP_TYPE): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_RF433_ON_OFF): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_TEMP_UNIT): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ALTITUDE_UNIT): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_AUTOMATIC_HEATING): sensor.sensor_schema(
unit_of_measurement=UNIT_EMPTY,
accuracy_decimals=0,
device_class=DEVICE_CLASS_EMPTY,
state_class=STATE_CLASS_MEASUREMENT,
)
}
)
.extend(ble_client.BLE_CLIENT_SCHEMA)
)
async def to_code(config):
var = await cg.get_variable(config[CONF_HEATER_ID])
for sensor_name in [
CONF_RUNNING_STATE,
CONF_ERROR_CODE,
CONF_RUNNING_STEP,
CONF_ALTITUDE,
CONF_RUNNING_MODE,
CONF_SET_LEVEL,
CONF_SET_TEMP,
CONF_SUPPLY_VOLTAGE,
CONF_CASE_TEMP,
CONF_CAB_TEMP,
CONF_START_TIME,
CONF_AUTO_TIME,
CONF_RUN_TIME,
CONF_IS_AUTO,
CONF_LANGUAGE,
CONF_TEMP_OFFSET,
CONF_TANK_VOLUME,
CONF_OIL_PUMP_TYPE,
CONF_RF433_ON_OFF,
CONF_TEMP_UNIT,
CONF_ALTITUDE_UNIT,
CONF_AUTOMATIC_HEATING]:
if sensor_config := config.get(sensor_name):
sens = await sensor.new_sensor(sensor_config)
cg.add(getattr(var, f"set_{sensor_name}")(sens))

View File

@ -0,0 +1,124 @@
#pragma once
#include <cinttypes>
#include <string>
namespace esphome {
namespace diesel_heater_ble {
enum class HeaterClass {
HEATER_AA_55, // The one that sends responses with 0xAA 0x55 header
HEATER_AA_66, // The one that sends responses with 0xAA 0x66 header
HEATER_AA_55_ENCRYPTED, // The one that sends responses with 0xAA 0x55 header and encrypted data
HEATER_AA_66_ENCRYPTED, // The one that sends responses with 0xAA 0x66 header and encrypted data
HEATER_CLASS_UNKNOWN
};
class HeaterState {
public:
HeaterClass heater_class;
uint8_t rcv_cmd;
uint8_t runningstate;
uint8_t errcode;
uint8_t runningstep;
uint16_t altitude;
uint8_t runningmode;
uint8_t setlevel;
uint8_t settemp;
uint16_t supplyvoltage;
uint16_t casetemp;
uint16_t cabtemp;
// encoded types only
uint16_t sttime;
uint16_t autotime;
uint16_t runtime;
uint8_t isauto;
uint8_t language;
uint8_t tempoffset;
uint8_t tankvolume;
uint8_t oilpumptype;
bool rf433onoff;
uint8_t tempunit;
uint8_t altiunit;
uint8_t automaticheating;
// return table format of data with columnt names
std::string to_string() {
return "HeaterState: \n"
" heater_class: " + std::to_string(static_cast<int>(heater_class)) + "\n"
" rcv_cmd: " + std::to_string(rcv_cmd) + "\n"
" runningstate: " + std::to_string(runningstate) + "\n"
" errcode: " + std::to_string(errcode) + "\n"
" runningstep: " + std::to_string(runningstep) + "\n"
" altitude: " + std::to_string(altitude) + "\n"
" runningmode: " + std::to_string(runningmode) + "\n"
" setlevel: " + std::to_string(setlevel) + "\n"
" settemp: " + std::to_string(settemp) + "\n"
" supplyvoltage: " + std::to_string(supplyvoltage) + "\n"
" casetemp: " + std::to_string(casetemp) + "\n"
" cabtemp: " + std::to_string(cabtemp) + "\n"
" sttime: " + std::to_string(sttime) + "\n"
" autotime: " + std::to_string(autotime) + "\n"
" runtime: " + std::to_string(runtime) + "\n"
" isauto: " + std::to_string(isauto) + "\n";
}
};
} // namespace diesel_heater_ble
} // namespace esphome
// // Represents the current system state
// class HeaterState {
// public:
// // Decoded fields from the response
// uint16_t deviceStatusFlags; // Device status flags (on/off, error states)
// uint16_t tempSetpoint; // Temperature setpoint
// uint16_t currentTemperature; // Current temperature
// uint16_t altitude; // Altitude in meters or feet
// uint8_t batteryVoltage; // Battery voltage (scaled)
// uint8_t fuelLevel; // Remaining fuel level
// uint8_t operatingMode; // Current operating mode
// uint8_t errorCode; // Error code
// uint32_t runtimeCounter; // Total runtime (in minutes or seconds)
// uint8_t additionalStatus[10]; // Placeholder for additional statuses
// // Decode raw response into meaningful fields
// static std::vector<uint8_t> decode(const std::vector<uint8_t> &raw) {
// if (raw.size() != 48 || raw[0] != 0xAA || raw[1] != 0x55) {
// throw std::invalid_argument("Invalid packet format");
// }
// std::vector<uint8_t> decoded;
// decoded.push_back(raw[3] | (raw[4] << 8));
// decoded.push_back(raw[5] | (raw[6] << 8));
// decoded.push_back(raw[7] | (raw[8] << 8));
// decoded.push_back(raw[9] | (raw[10] << 8));
// decoded.push_back(raw[11]);
// decoded.push_back(raw[12]);
// decoded.push_back(raw[13]);
// decoded.push_back(raw[14]);
// decoded.push_back(raw[15] | (raw[16] << 8) | (raw[17] << 16) | (raw[18] << 24));
// std::memcpy(&decoded[9], &raw[19], 10);
// return decoded;
// }
// static HeaterState decompose(const std::vector<uint8_t> &raw) {
// if (raw.size() != 48 || raw[0] != 0xAA || raw[1] != 0x55) {
// throw std::invalid_argument("Invalid packet format");
// }
// HeaterState state;
// state.deviceStatusFlags = raw[3] | (raw[4] << 8);
// state.tempSetpoint = raw[5] | (raw[6] << 8);
// state.currentTemperature = raw[7] | (raw[8] << 8);
// state.altitude = raw[9] | (raw[10] << 8);
// state.batteryVoltage = raw[11];
// state.fuelLevel = raw[12];
// state.operatingMode = raw[13];
// state.errorCode = raw[14];
// state.runtimeCounter = raw[15] | (raw[16] << 8) | (raw[17] << 16) | (raw[18] << 24);
// std::memcpy(state.additionalStatus, &raw[19], 10);
// return state;
// }
// };

View File

@ -0,0 +1,23 @@
#pragma once
#include "esphome/components/switch/switch.h"
#include "esphome/core/log.h"
#include "heater.h"
#include "messages.h"
namespace esphome {
namespace diesel_heater_ble {
class PowerSwitch : public switch_::Switch, public Parented<DieselHeaterBLE> {
public:
PowerSwitch() = default;
protected:
void write_state(bool state) override {
ESP_LOGD("diesel_heater_ble", "Setting power state to: %d", state);
this->parent_->sent_request(SetPowerRequest(state).toBytes());
}
};
} // namespace diesel_heater
} // namespace esphome

View File

@ -0,0 +1,36 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import switch
from . import DieselHeaterBLE, CONF_HEATER_ID, diesel_heater_ble_ns
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(CONF_HEATER_ID): cv.use_id(DieselHeaterBLE),
cv.Optional("power"): switch.switch_schema(
diesel_heater_ble_ns.class_("PowerSwitch", switch.Switch),
icon="mdi:power",
),
# cv.Optional("mode"): switch.switch_schema(
# heater_ns.class_("ModeSwitch", switch.Switch),
# icon="mdi:power",
# ),
# cv.Optional("alpine"): switch.switch_schema(
# heater_ns.class_("AlpineSwitch", switch.Switch),
# icon="mdi:power",
# ),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_HEATER_ID])
for switch_type in ["power"]:
if conf := config.get(switch_type):
sw_var = await switch.new_switch(conf)
await cg.register_parented(sw_var, parent)
cg.add(getattr(parent, f"set_{switch_type}_switch")(sw_var))