initial commit.

This commit is contained in:
Your Name
2023-02-11 21:10:30 +01:00
parent cbb9131ec8
commit ced79a4af9
63 changed files with 6470 additions and 2 deletions

View File

@@ -0,0 +1,340 @@
#include "LinBusListener.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "helpers.h"
#ifdef USE_ESP32
#include "driver/uart.h"
#include "soc/uart_struct.h"
#include "soc/uart_reg.h"
#endif
#ifdef USE_ESP8266
#include "esphome/components/uart/uart_component_esp8266.h"
#endif
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include "esphome/components/uart/truma_uart_component_esp32_arduino.h"
#include "esphome/components/uart/uart_component_esp32_arduino.h"
#endif
#ifdef USE_ESP32_FRAMEWORK_ESP_IDF
#include "esphome/components/uart/truma_uart_component_esp_idf.h"
#include "esphome/components/uart/uart_component_esp_idf.h"
#endif
#ifdef USE_RP2040
#include "esphome/components/uart/truma_uart_component_rp2040.h"
#include "esphome/components/uart/uart_component_rp2040.h"
#endif
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
// For method `xTaskCreateUniversal`
#include <esp32-hal.h>
#endif
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.LinBusListener";
#define LIN_BREAK 0x00
#define LIN_SYNC 0x55
#define DIAGNOSTIC_FRAME_MASTER 0x3c
#define DIAGNOSTIC_FRAME_SLAVE 0x3d
void LinBusListener::dump_config() {
ESP_LOGCONFIG(TAG, "TODO");
this->check_uart_settings(9600, 2, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
}
void LinBusListener::setup() {
ESP_LOGCONFIG(TAG, "Setting up LIN BUS...");
this->time_per_baud_ = (1000.0f * 1000.0f / this->parent_->get_baud_rate());
this->time_per_lin_break_ = this->time_per_baud_ * this->lin_break_length * 1.1;
this->time_per_pid_ = this->time_per_baud_ * this->frame_length_ * 1.1;
this->time_per_first_byte_ = this->time_per_baud_ * this->frame_length_ * 3.0;
this->time_per_byte_ = this->time_per_baud_ * this->frame_length_ * 1.1;
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
auto uartComp = static_cast<esphome::uart::truma_ESP32ArduinoUARTComponent *>(this->parent_);
auto uart_num = uartComp->get_hw_serial_number();
// Tweak the fifo settings so data is available as soon as the first byte is recieved.
// If not it will wait either until fifo is filled or a certain time has passed.
uart_intr_config_t uart_intr;
uart_intr.intr_enable_mask =
UART_RXFIFO_FULL_INT_ENA_M | UART_RXFIFO_TOUT_INT_ENA_M; // only these IRQs - no BREAK, PARITY or OVERFLOW
// UART_RXFIFO_FULL_INT_ENA_M | UART_RXFIFO_TOUT_INT_ENA_M | UART_FRM_ERR_INT_ENA_M |
// UART_RXFIFO_OVF_INT_ENA_M | UART_BRK_DET_INT_ENA_M | UART_PARITY_ERR_INT_ENA_M;
uart_intr.rxfifo_full_thresh =
1; // UART_FULL_THRESH_DEFAULT, //120 default!! aghh! need receive 120 chars before we see them
uart_intr.rx_timeout_thresh =
10; // UART_TOUT_THRESH_DEFAULT, //10 works well for my short messages I need send/receive
uart_intr.txfifo_empty_intr_thresh = 10; // UART_EMPTY_THRESH_DEFAULT
uart_intr_config(uart_num, &uart_intr);
#elif USE_ESP32_FRAMEWORK_ESP_IDF
// uartSetFastReading
auto uartComp = ((*uart::truma_IDFUARTComponent) this->parent_);
// Tweak the fifo settings so data is available as soon as the first byte is recieved.
// If not it will wait either until fifo is filled or a certain time has passed.
uart_intr_config_t uart_intr;
uart_intr.intr_enable_mask = 0; // UART_RXFIFO_FULL_INT_ENA_M | UART_RXFIFO_TOUT_INT_ENA_M | UART_FRM_ERR_INT_ENA_M |
// UART_RXFIFO_OVF_INT_ENA_M | UART_BRK_DET_INT_ENA_M | UART_PARITY_ERR_INT_ENA_M;
uart_intr.rxfifo_full_thresh =
1; // UART_FULL_THRESH_DEFAULT, //120 default!! aghh! need receive 120 chars before we see them
uart_intr.rx_timeout_thresh =
1; // UART_TOUT_THRESH_DEFAULT, //10 works well for my short messages I need send/receive
uart_intr.txfifo_empty_intr_thresh = 10; // UART_EMPTY_THRESH_DEFAULT
uart_intr_config(uartComp->get_hw_serial_number(), &uart_intr);
#else
// truma_RP2040UartComponent
#error Only ESP32 Arduino is supported.
#endif
xTaskCreateUniversal(LinBusListener::read_task,
"read_task", // name
4096, // stack size (in words)
this, // input params
1, // priority
&this->read_task_handle, // handle
0 // core
);
if (this->cs_pin_ != nullptr) {
this->cs_pin_->digital_write(true);
}
}
void LinBusListener::write_lin_answer_(const u_int8_t *data, size_t len) {
if (!this->can_write_lin_answer_) {
ESP_LOGE(TAG, "Cannot answer LIN because there is no open order from master.");
return;
}
this->can_write_lin_answer_ = false;
if (len > 8) {
ESP_LOGE(TAG, "LIN answer cannot be longer than 8 bytes.");
return;
}
int64_t wait_time = 0;
// Was data read from FIFO and the master is awaiting an answer.
if (this->total_wait_ > 1000) {
// I am up to date and should not answer too quickly.
auto current = esp_timer_get_time();
auto wait_time_in_us = (int64_t) this->time_per_baud_ - (current - this->last_data_recieved_);
wait_time = wait_time_in_us;
if (wait_time_in_us > 1000 || wait_time_in_us < 0) {
wait_time_in_us = 0;
}
delayMicroseconds(wait_time_in_us);
}
u_int8_t data_CRC = 0;
if (this->lin_checksum_ == LIN_CHECKSUM::LIN_CHECKSUM_VERSION_1 || this->current_PID_ == DIAGNOSTIC_FRAME_SLAVE) {
// LIN checksum V1
data_CRC = data_checksum(data, len, 0);
} else {
// LIN checksum V2
data_CRC = data_checksum(data, len, this->current_PID_with_parity_);
}
if (!this->observer_mode_) {
this->write_array(data, len);
this->write(data_CRC);
this->flush();
}
ESP_LOGV(TAG, "RESPONSE %02x %s %02x T %lli", this->current_PID_, format_hex_pretty(data, len).c_str(), data_CRC,
wait_time);
}
void LinBusListener::read_task(void *params) {
LinBusListener *instance = (LinBusListener *) params;
while (true) {
// Check if Lin Bus is faulty.
if (instance->fault_pin_ != nullptr) {
if (!instance->fault_pin_->digital_read()) {
if (!instance->fault_on_lin_bus_reported_) {
instance->fault_on_lin_bus_reported_ = true;
ESP_LOGE(TAG, "Fault on LIN BUS detected.");
}
// Ignore any data present in buffer
instance->clear_uart_buffer_();
} else if (instance->fault_on_lin_bus_reported_) {
instance->fault_on_lin_bus_reported_ = false;
ESP_LOGI(TAG, "Fault on LIN BUS fixed.");
}
}
if (!instance->fault_on_lin_bus_reported_) {
while (instance->available()) {
instance->read_lin_frame_();
}
}
// Check if CP Plus is inactive mode. In inactive mode it checks the bus every ~15 seconds for ~5 seconds. At the
// start it send a Break to notify devices to wake up.
auto time_since_last_activity = esp_timer_get_time() - instance->last_data_recieved_;
if (time_since_last_activity > 100 * 1000 /* 100 ms*/) {
// CP Plus is inactive.
delay(500); // NOLINT
} else {
// CP Plus is active.
// 1'000'000 ns / 9600 baud = 104 ns/baud * (8 bit + start bit + 2 stop bit) = 1144 ns/byte * 3 (BREAK,SYNC,PID) =
// ~3.5ms per preamble till I should answer. It is still working with 50ms. But thats the upper limit. CP Plus
// waits 50ms when ordering for an answer. With higher polling the number of CRC errors increases and I cannot
// answer lin orders.
delay(10); // NOLINT
}
}
}
void LinBusListener::read_lin_frame_() {
u_int8_t buf;
bool dataRecieved;
u_int8_t data_length, data_CRC, data_CRC_master, data_CRC_slave;
bool message_source_know, message_from_master;
// Reset current state
{
this->current_PID_with_parity_ = 0x00;
this->current_PID_ = 0x00;
this->current_data_valid = true;
this->current_data_count_ = 0;
memset(this->current_data_, 0, sizeof(this->current_data_));
this->total_wait_ = 0;
}
// First is Break expected
if (!this->read_byte(&buf) || buf != LIN_BREAK) {
// Update I recieved garbage
this->last_data_recieved_ = esp_timer_get_time();
ESP_LOGVV(TAG, "Expected BREAK not received.");
return;
}
// Update I recieved a break
this->last_data_recieved_ = esp_timer_get_time();
if (!this->wait_for_data_available_with_timeout_(this->time_per_lin_break_)) {
ESP_LOGV(TAG, "Timeout waiting for Sync");
return;
}
// Second is Sync expected
if (!this->read_byte(&buf) || buf != LIN_SYNC) {
// No data present on UART
ESP_LOGVV(TAG, "Expected SYNC not found.");
return;
}
if (!this->wait_for_data_available_with_timeout_(this->time_per_pid_)) {
ESP_LOGVV(TAG, "Timeout waiting for PID.");
return;
}
this->read_byte(&(this->current_PID_with_parity_));
this->current_PID_ = this->current_PID_with_parity_ & 0x3F;
if (this->lin_checksum_ == LIN_CHECKSUM::LIN_CHECKSUM_VERSION_2) {
if (this->current_PID_with_parity_ != (this->current_PID_ | (addr_parity(this->current_PID_) << 6))) {
ESP_LOGW(TAG, "LIN CRC error");
this->current_data_valid = false;
}
}
this->can_write_lin_answer_ = true;
// Should I response to this PID order? Ask the handling class.
this->answer_lin_order_(this->current_PID_);
this->can_write_lin_answer_ = false;
dataRecieved = wait_for_data_available_with_timeout_(this->time_per_first_byte_);
while (dataRecieved) {
this->read_byte(&buf);
if (this->current_data_count_ < sizeof(this->current_data_)) {
this->current_data_[this->current_data_count_] = buf;
this->current_data_count_++;
dataRecieved = wait_for_data_available_with_timeout_(this->time_per_byte_);
} else {
// end of data reached. There cannot be more than 9 bytes in a LIN frame.
dataRecieved = false;
}
}
if (this->current_data_count_ > 1) {
data_length = this->current_data_count_ - 1;
data_CRC = this->current_data_[this->current_data_count_ - 1];
message_source_know = false;
message_from_master = true;
if (this->lin_checksum_ == LIN_CHECKSUM::LIN_CHECKSUM_VERSION_1 ||
(this->current_PID_ == DIAGNOSTIC_FRAME_MASTER || this->current_PID_ == DIAGNOSTIC_FRAME_SLAVE)) {
if (data_CRC != data_checksum(this->current_data_, data_length, 0)) {
ESP_LOGW(TAG, "LIN v1 CRC error");
this->current_data_valid = false;
}
if (this->current_PID_ == DIAGNOSTIC_FRAME_MASTER) {
message_source_know = true;
message_from_master = true;
} else if (this->current_PID_ == DIAGNOSTIC_FRAME_SLAVE) {
message_source_know = true;
message_from_master = false;
}
} else {
data_CRC_master = data_checksum(this->current_data_, data_length, this->current_PID_);
data_CRC_slave = data_checksum(this->current_data_, data_length, this->current_PID_with_parity_);
if (data_CRC != data_CRC_master && data_CRC != data_CRC_slave) {
ESP_LOGW(TAG, "LIN v2 CRC error");
this->current_data_valid = false;
}
message_source_know = true;
if (data_CRC == data_CRC_slave) {
message_from_master = false;
}
}
// Mark the PID of the TRUMA Combi heater as very verbose message.
if (this->current_PID_ == 0x20 || this->current_PID_ == 0x21 || this->current_PID_ == 0x22) {
ESP_LOGVV(TAG, "PID %02x (%02x) %s %s %s", this->current_PID_, this->current_PID_with_parity_,
format_hex_pretty(this->current_data_, this->current_data_count_).c_str(),
message_source_know ? (message_from_master ? " - MASTER" : " - SLAVE") : "",
this->current_data_valid ? "" : "INVALID");
} else {
ESP_LOGV(TAG, "PID %02x (%02x) %s %s %S", this->current_PID_, this->current_PID_with_parity_,
format_hex_pretty(this->current_data_, this->current_data_count_).c_str(),
message_source_know ? (message_from_master ? " - MASTER" : " - SLAVE") : "",
this->current_data_valid ? "" : "INVALID");
}
if (this->current_data_valid && message_from_master) {
this->lin_message_recieved_(this->current_PID_, this->current_data_, data_length);
}
} else {
ESP_LOGV(TAG, "PID %02x (%02x) order no answer", this->current_PID_, this->current_PID_with_parity_);
}
}
void LinBusListener::clear_uart_buffer_() {
u_int8_t buffer;
while (this->available() && this->read_byte(&buffer)) {
}
}
bool LinBusListener::wait_for_data_available_with_timeout_(u_int32_t timeout) {
int64_t start = esp_timer_get_time();
int64_t current = esp_timer_get_time();
int64_t latest_end = start + timeout;
while (current < latest_end) {
current = esp_timer_get_time();
if (this->available()) {
this->total_wait_ += current - start;
this->last_data_recieved_ = current;
return true;
}
NOP();
}
return false;
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,79 @@
#pragma once
#include <vector>
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#ifdef USE_ESP32
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#endif
namespace esphome {
namespace truma_inetbox {
enum class LIN_CHECKSUM { LIN_CHECKSUM_VERSION_1, LIN_CHECKSUM_VERSION_2 };
class LinBusListener : public PollingComponent, public uart::UARTDevice {
public:
float get_setup_priority() const override { return setup_priority::DATA; }
void dump_config() override;
void setup() override;
void set_lin_checksum(LIN_CHECKSUM val) { this->lin_checksum_ = val; }
void set_cs_pin(GPIOPin *pin) { this->cs_pin_ = pin; }
void set_fault_pin(GPIOPin *pin) { this->fault_pin_ = pin; }
void set_observer_mode(bool val) { this->observer_mode_ = val; }
bool get_lin_bus_fault() { return fault_on_lin_bus_reported_; }
protected:
LIN_CHECKSUM lin_checksum_;
GPIOPin *cs_pin_;
GPIOPin *fault_pin_;
bool observer_mode_ = false;
void write_lin_answer_(const u_int8_t *data, size_t len);
virtual bool answer_lin_order_(const u_int8_t pid) = 0;
virtual void lin_message_recieved_(const u_int8_t pid, const u_int8_t *message, u_int8_t length) = 0;
private:
// Microseconds per UART Baud
u_int32_t time_per_baud_;
// 9.. 15
u_int8_t lin_break_length = 13;
// Microseconds per LIN Break
u_int32_t time_per_lin_break_;
u_int8_t frame_length_ = (8 /* bits */ + 1 /* Start bit */ + 2 /* Stop bits */);
// Microseconds per UART Byte (UART Frame)
u_int32_t time_per_pid_;
// Microseconds per UART Byte (UART Frame)
u_int32_t time_per_first_byte_;
// Microseconds per UART Byte (UART Frame)
u_int32_t time_per_byte_;
bool fault_on_lin_bus_reported_ = false;
bool can_write_lin_answer_ = false;
u_int8_t current_PID_with_parity_ = 0x00;
u_int8_t current_PID_ = 0x00;
bool current_data_valid = true;
u_int8_t current_data_count_ = 0;
// up to 8 byte data frame + CRC
u_int8_t current_data_[9] = {};
// Total wait time for this LIN Frame (Break, SYNC, Data, CRC)
u_int32_t total_wait_;
// Time when the last LIN data was available.
int64_t last_data_recieved_;
TaskHandle_t read_task_handle = NULL;
static void read_task(void *params);
void read_lin_frame_();
void clear_uart_buffer_();
bool wait_for_data_available_with_timeout_(u_int32_t timeout);
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,254 @@
#include "LinBusProtocol.h"
#include <array>
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.LinBusProtocol";
#define DIAGNOSTIC_FRAME_MASTER 0x3c
#define DIAGNOSTIC_FRAME_SLAVE 0x3d
#define LIN_NAD_BROADCAST 0x7F
#define LIN_SID_RESPONSE 0x40
#define LIN_SID_ASSIGN_NAD 0xB0
#define LIN_SID_ASSIGN_NAD_RESPONSE (LIN_SID_ASSIGN_NAD | LIN_SID_RESPONSE)
#define LIN_SID_READ_BY_IDENTIFIER 0xB2
#define LIN_SID_READ_BY_IDENTIFIER_RESPONSE (LIN_SID_READ_BY_IDENTIFIER | LIN_SID_RESPONSE)
#define LIN_SID_HEARTBEAT 0xB9
#define LIN_SID_HEARTBEAT_RESPONSE (LIN_SID_HEARTBEAT | LIN_SID_RESPONSE)
#define LIN_SID_READ_STATE_BUFFER 0xBA
#define LIN_SID_READ_STATE_BUFFER_RESPONSE (LIN_SID_READ_STATE_BUFFER | LIN_SID_RESPONSE)
#define LIN_SID_FIll_STATE_BUFFFER 0xBB
#define LIN_SID_FIll_STATE_BUFFFER_BRESPONSE (LIN_SID_FIll_STATE_BUFFFER | LIN_SID_RESPONSE)
bool LinBusProtocol::answer_lin_order_(const u_int8_t pid) {
// Send requested answer
if (pid == DIAGNOSTIC_FRAME_SLAVE) {
if (!this->updates_to_send_.empty()) {
auto update_to_send_ = this->updates_to_send_.front();
this->updates_to_send_.pop();
this->write_lin_answer_(update_to_send_.data(), update_to_send_.size());
return true;
}
}
return false;
}
void LinBusProtocol::lin_message_recieved_(const u_int8_t pid, const u_int8_t *message, u_int8_t length) {
if (pid == DIAGNOSTIC_FRAME_MASTER) {
// The original Inet Box is answering this message. Works fine without.
// std::array<u_int8_t, 8> message_array = {};
// std::copy(message, message + length, message_array.begin());
// if (message_array == this->lin_empty_response_) {
// std::array<u_int8_t, 8> response = this->lin_empty_response_;
// response[0] = 0x00;
// response[1] = 0x55;
// response[2] = 0x03; // this->lin_node_address_;
// response[3] = 0x66;
// response[4] = 0x5B;
// response[5] = 0xA7;
// response[6] = 0x0E;
// response[7] = 0x49;
// this->prepare_update_msg_(response);
// }
this->lin_message_recieved_diagnostic_(message, length);
} else if (pid == this->lin_node_address_) {
ESP_LOGW(TAG, "Unhandled message for me.");
}
}
void LinBusProtocol::prepare_update_msg_(const std::array<u_int8_t, 8> message) {
this->updates_to_send_.push(message);
}
bool LinBusProtocol::is_matching_identifier_(const u_int8_t *message) {
auto lin_identifier = this->lin_identifier();
return message[0] == lin_identifier[0] && message[1] == lin_identifier[1] && message[2] == lin_identifier[2] &&
message[3] == lin_identifier[3];
}
void LinBusProtocol::lin_message_recieved_diagnostic_(const u_int8_t *message, u_int8_t length) {
u_int8_t node_address = message[0];
bool my_node_address = node_address == this->lin_node_address_;
bool broadcast_address = node_address == LIN_NAD_BROADCAST;
if (!my_node_address && !broadcast_address) {
return;
}
u_int8_t protocol_control_information = message[1];
u_int16_t message_length = 0;
u_int8_t service_identifier = 0;
if ((protocol_control_information & 0xF0) == 0x00) {
// Single Frame mode
{
// End any open Multi frame mode message
this->multi_pdu_message_expected_size_ = 0;
this->multi_pdu_message_len_ = 0;
}
message_length = protocol_control_information;
service_identifier = message[2];
if (message_length > 6) {
ESP_LOGE(TAG, "LIN Protocol issue: Single frame message too long.");
// ignore invalid message
return;
}
} else if ((protocol_control_information & 0xF0) == 0x10) {
// First Frame of multi PDU message
message_length = (protocol_control_information & 0x0F << 8) + message[2];
service_identifier = message[3];
if (message_length < 7) {
ESP_LOGE(TAG, "LIN Protocol issue: Multi frame message too short.");
// ignore invalid message
return;
}
if (message_length > sizeof(this->multi_pdu_message_)) {
ESP_LOGE(TAG, "LIN Protocol issue: Multi frame message too long.");
// ignore invalid message
return;
}
this->multi_pdu_message_expected_size_ = message_length;
this->multi_pdu_message_len_ = 0;
for (size_t i = 3; i < 8; i++) {
this->multi_pdu_message_[this->multi_pdu_message_len_++] = message[i];
}
// Message is handeld
return;
} else if ((protocol_control_information & 0xF0) == 0x20) {
// Consecutive Frames
if (this->multi_pdu_message_len_ >= this->multi_pdu_message_expected_size_) {
// ignore, because i don't await a consecutive frame
return;
}
this->lin_message_recieved_diagnostic_multi_(message, length, protocol_control_information);
// Message is handeld
return;
}
if (service_identifier == LIN_SID_READ_BY_IDENTIFIER && message_length == 6) {
if (this->is_matching_identifier_(&message[4])) {
// I have observed the following identifiers:
// broadcast_address:
// - 0x00 - response lin_identifier[0:4] + 0x00 /* Hardware revision*/
// my_node_address:
// - 0x20 - displayed version
// - 0x22 - unkown
auto identifier = message[3];
std::array<u_int8_t, 8> response = this->lin_empty_response_;
response[0] = this->lin_node_address_;
std::array<u_int8_t, 5> identifier_response = {};
if (this->lin_read_field_by_identifier_(identifier, &identifier_response)) {
response[1] = 6; /* bytes length - ignored by CP Plus?*/
response[2] = LIN_SID_READ_BY_IDENTIFIER_RESPONSE;
auto iterator = response.begin();
std::advance(iterator, 3);
std::copy(identifier_response.data(), identifier_response.data() + identifier_response.size(), iterator);
} else {
// Not supported - Negative response (see 4.2.6.1 Read by identifier)
response[1] = 3; /* bytes length*/
response[2] = 0x7F;
response[3] = LIN_SID_READ_BY_IDENTIFIER;
response[4] = 0x12;
}
this->prepare_update_msg_(response);
}
} else if (my_node_address && service_identifier == LIN_SID_HEARTBEAT && message_length >= 5) {
// if (message[3] == 0x00 && message[4] == 0x1F && message[5] == 0x00 && message[6] == 0x00) {
std::array<u_int8_t, 8> response = this->lin_empty_response_;
response[0] = this->lin_node_address_;
response[1] = 2; /* bytes length*/
response[2] = LIN_SID_HEARTBEAT_RESPONSE;
response[3] = 0x00;
this->prepare_update_msg_(response);
//}
} else if (broadcast_address && service_identifier == LIN_SID_ASSIGN_NAD && message_length == 6) {
if (this->is_matching_identifier_(&message[3])) {
ESP_LOGI(TAG, "Assigned new SID %02x and reset device", message[7]);
// send response with old node address.
std::array<u_int8_t, 8> response = this->lin_empty_response_;
response[0] = this->lin_node_address_;
response[1] = 1; /* bytes length*/
response[2] = LIN_SID_ASSIGN_NAD_RESPONSE;
this->prepare_update_msg_(response);
this->lin_node_address_ = message[7];
// assumption an assign new SID occurs as part of init process.
this->lin_reset_device();
}
} else {
if (my_node_address) {
ESP_LOGD(TAG, "SID %02x MY - %s - Unhandled", service_identifier, format_hex_pretty(message, length).c_str());
} else if (broadcast_address) {
ESP_LOGD(TAG, "SID %02x BC - %s - Unhandled", service_identifier, format_hex_pretty(message, length).c_str());
}
}
}
void LinBusProtocol::lin_message_recieved_diagnostic_multi_(const u_int8_t *message, u_int8_t length,
u_int8_t protocol_control_information) {
u_int8_t frame_counter = protocol_control_information - 0x21;
for (size_t i = 2; i < 8; i++) {
if (this->multi_pdu_message_len_ < this->multi_pdu_message_expected_size_) {
this->multi_pdu_message_[this->multi_pdu_message_len_++] = message[i];
}
}
if (this->multi_pdu_message_len_ == this->multi_pdu_message_expected_size_) {
ESP_LOGD(TAG, "Multi package request %s",
format_hex_pretty(this->multi_pdu_message_, this->multi_pdu_message_len_).c_str());
u_int8_t answer_len = 0;
auto answer = this->lin_multiframe_recieved(this->multi_pdu_message_, this->multi_pdu_message_len_, &answer_len);
if (answer_len > 0) {
ESP_LOGD(TAG, "Multi package response %s", format_hex_pretty(answer, answer_len).c_str());
if (answer_len <= 6) {
// Single Frame response
std::array<u_int8_t, 8> response = this->lin_empty_response_;
response[0] = this->lin_node_address_;
response[1] = answer_len; /* bytes length */
response[2] = answer[0] | LIN_SID_RESPONSE;
for (size_t i = 1; i < answer_len; i++) {
response[i + 2] = answer[i];
}
this->prepare_update_msg_(response);
} else {
// Multi Frame response
std::array<u_int8_t, 8> response = this->lin_empty_response_;
response[0] = this->lin_node_address_;
response[1] = 0x10 | ((answer_len >> 8) & 0x0F);
response[2] = answer_len & 0xFF;
response[3] = answer[0] | LIN_SID_RESPONSE;
for (size_t i = 1; i < 5; i++) {
response[i + 3] = answer[i];
}
this->prepare_update_msg_(response);
u_int16_t answer_position = 5; // The first 5 bytes are sent in First frame of multi frame response.
u_int8_t answer_frame_counter = 0; // Each answer frame can contain 6 bytes
while (answer_position < answer_len) {
response = this->lin_empty_response_;
response[0] = this->lin_node_address_;
response[1] = ((answer_frame_counter + 1) & 0x0F) | 0x20;
for (size_t i = 0; i < 6; i++) {
if (answer_position < answer_len) {
response[i + 2] = answer[answer_position++];
}
}
this->prepare_update_msg_(response);
answer_frame_counter++;
}
}
}
}
}
void LinBusProtocol::lin_message_recieved_diagnostic_single_(const u_int8_t *message, u_int8_t length) {
// TODO: Split up `lin_message_recieved_diagnostic_` method.
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,42 @@
#pragma once
#include <vector>
#include <queue>
#include "LinBusListener.h"
namespace esphome {
namespace truma_inetbox {
class LinBusProtocol : public LinBusListener {
public:
virtual const std::array<u_int8_t, 4> lin_identifier() = 0;
virtual void lin_reset_device() = 0;
protected:
const std::array<u_int8_t, 8> lin_empty_response_ = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
bool answer_lin_order_(const u_int8_t pid) override;
void lin_message_recieved_(const u_int8_t pid, const u_int8_t *message, u_int8_t length) override;
virtual bool lin_read_field_by_identifier_(u_int8_t identifier, std::array<u_int8_t, 5> *response) = 0;
virtual const u_int8_t *lin_multiframe_recieved(const u_int8_t *message, const u_int8_t message_len,
u_int8_t *return_len) = 0;
std::queue<std::array<u_int8_t, 8>> updates_to_send_ = {};
private:
u_int8_t lin_node_address_ = /*LIN initial node address*/ 0x03;
void prepare_update_msg_(const std::array<u_int8_t, 8> message);
bool is_matching_identifier_(const u_int8_t *message);
u_int16_t multi_pdu_message_expected_size_ = 0;
u_int8_t multi_pdu_message_len_ = 0;
u_int8_t multi_pdu_message_[64];
void lin_message_recieved_diagnostic_(const u_int8_t *message, u_int8_t length);
void lin_message_recieved_diagnostic_multi_(const u_int8_t *message, u_int8_t length,
u_int8_t protocol_control_information);
void lin_message_recieved_diagnostic_single_(const u_int8_t *message, u_int8_t length);
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,86 @@
#include "TrumaStatusFrame.h"
#include "esphome/core/helpers.h"
#include "TrumaiNetBoxApp.h"
#include "helpers.h"
namespace esphome {
namespace truma_inetbox {
void status_frame_create_empty(StatusFrame *response, u_int8_t message_type, u_int8_t message_length,
u_int8_t command_counter) {
response->inner.genericHeader.service_identifier = LIN_SID_READ_STATE_BUFFER | LIN_SID_RESPONSE;
// Copy header over for this message.
for (size_t i = 1; i < truma_message_header.size(); i++) {
response->raw[i] = truma_message_header[i];
}
response->inner.genericHeader.header_2 = 'T';
response->inner.genericHeader.header_3 = 0x01;
response->inner.genericHeader.message_type = message_type;
response->inner.genericHeader.message_length = message_length;
response->inner.genericHeader.command_counter = command_counter;
}
void status_frame_calculate_checksum(StatusFrame *response) {
response->inner.genericHeader.checksum = 0x0;
response->inner.genericHeader.checksum = data_checksum(&response->raw[10], sizeof(StatusFrame) - 10, 0);
}
void status_frame_create_init(StatusFrame *response, u_int8_t command_counter) {
status_frame_create_empty(response, STATUS_FRAME_RESPONSE_INIT_REQUEST, 0, command_counter);
status_frame_calculate_checksum(response);
}
void status_frame_create_update_clock(StatusFrame *response, u_int8_t command_counter, u_int8_t hour, u_int8_t minute,
u_int8_t second, ClockMode clockMode) {
status_frame_create_empty(response, STATUS_FRAME_CLOCK_RESPONSE, sizeof(StatusFrameClock), command_counter);
response->inner.clock.clock_hour = hour;
response->inner.clock.clock_minute = minute;
response->inner.clock.clock_second = second;
response->inner.clock.display_1 = 0x1;
response->inner.clock.display_2 = 0x1;
response->inner.clock.clock_mode = clockMode;
status_frame_calculate_checksum(response);
}
void status_frame_create_update_timer(StatusFrame *response, u_int8_t command_counter, TimerActive active,
u_int8_t start_hour, u_int8_t start_minute, u_int8_t stop_hour,
u_int8_t stop_minute, TargetTemp room, TargetTemp water, HeatingMode mode,
EnergyMix energy, ElectricPowerLevel elPower) {
status_frame_create_empty(response, STATUS_FRAME_TIMER_RESPONSE, sizeof(StatusFrameTimerResponse), command_counter);
response->inner.timerResponse.timer_target_temp_room = room;
response->inner.timerResponse.timer_heating_mode = mode;
response->inner.timerResponse.timer_target_temp_water = water;
response->inner.timerResponse.timer_energy_mix_a = energy;
response->inner.timerResponse.timer_energy_mix_b = energy;
response->inner.timerResponse.timer_el_power_level_a = elPower;
response->inner.timerResponse.timer_el_power_level_b = elPower;
response->inner.timerResponse.timer_resp_active = active;
response->inner.timerResponse.timer_resp_start_hours = start_hour;
response->inner.timerResponse.timer_resp_start_minutes = start_minute;
response->inner.timerResponse.timer_resp_stop_hours = stop_hour;
response->inner.timerResponse.timer_resp_stop_minutes = stop_minute;
status_frame_calculate_checksum(response);
}
void status_frame_create_update_heater(StatusFrame *response, u_int8_t command_counter, TargetTemp room,
TargetTemp water, HeatingMode mode, EnergyMix energy,
ElectricPowerLevel elPower) {
status_frame_create_empty(response, STATUS_FRAME_HEATER_RESPONSE, sizeof(StatusFrameHeaterResponse), command_counter);
response->inner.heaterResponse.target_temp_room = room;
response->inner.heaterResponse.heating_mode = mode;
response->inner.heaterResponse.target_temp_water = water;
response->inner.heaterResponse.energy_mix_a = energy;
response->inner.heaterResponse.energy_mix_b = energy;
response->inner.heaterResponse.el_power_level_a = elPower;
response->inner.heaterResponse.el_power_level_b = elPower;
status_frame_calculate_checksum(response);
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,28 @@
#pragma once
#include "TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
void status_frame_create_empty(StatusFrame *response, u_int8_t message_type, u_int8_t message_length,
u_int8_t command_counter);
void status_frame_calculate_checksum(StatusFrame *response);
void status_frame_create_init(StatusFrame *response, u_int8_t command_counter);
void status_frame_create_update_clock(StatusFrame *response, u_int8_t command_counter, u_int8_t hour, u_int8_t minute,
u_int8_t second, ClockMode clockMode);
void status_frame_create_update_timer(StatusFrame *response, u_int8_t command_counter, TimerActive active,
u_int8_t start_hour, u_int8_t start_minute, u_int8_t stop_hour,
u_int8_t stop_minute, TargetTemp room, TargetTemp water, HeatingMode mode,
EnergyMix energy, ElectricPowerLevel elPower);
void status_frame_create_update_heater(StatusFrame *response, u_int8_t command_counter, TargetTemp room,
TargetTemp water, HeatingMode mode, EnergyMix energy,
ElectricPowerLevel elPower);
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,439 @@
#include "TrumaiNetBoxApp.h"
#include "TrumaStatusFrame.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.TrumaiNetBoxApp";
TrumaiNetBoxApp::TrumaiNetBoxApp(u_int8_t expected_listener_count) {
this->listeners_heater_.reserve(expected_listener_count);
}
void TrumaiNetBoxApp::update() {
// Call listeners in after method 'lin_multiframe_recieved' call.
// Because 'lin_multiframe_recieved' is time critical an all these sensors can take some time.
if (this->status_heater_updated_ || this->status_timer_updated_ || this->status_clock_updated_ ||
this->status_config_updated_) {
// Run through listeners
for (auto &listener : this->listeners_heater_) {
if (this->status_heater_updated_ && listener.on_heater_change != nullptr) {
listener.on_heater_change(&this->status_heater_);
}
if (this->status_timer_updated_ && listener.on_timer_change != nullptr) {
listener.on_timer_change(&this->status_timer_);
}
if (this->status_clock_updated_ && listener.on_clock_change != nullptr) {
listener.on_clock_change(&this->status_clock_);
}
if (this->status_config_updated_ && listener.on_config_change != nullptr) {
listener.on_config_change(&this->status_config_);
}
}
// Run through callbacks
if (this->status_heater_updated_) {
this->state_heater_callback_.call(&this->status_heater_);
}
// update is handeld
this->status_heater_updated_ = false;
this->status_timer_updated_ = false;
this->status_clock_updated_ = false;
this->status_config_updated_ = false;
}
}
const std::array<uint8_t, 4> TrumaiNetBoxApp::lin_identifier() {
// Unkown:
// 17.46.01.03 - Unkown more comms required for init.
// 17.46.10.03 - Unkown more comms required for init.
// Heater:
// 17.46.40.03 - H2.00.01 - 0340.xx Combi 4/6
// Aircon:
// 17.46.00.0C - A23.70.0 - 0C00.xx (with light option: OFF/1..5)
// 17.46.01.0C - A23.70.0 - 0C01.xx
// 17.46.04.0C - A23.70.0 - 0C04.xx (with light option: OFF/1..5)
// 17.46.05.0C - A23.70.0 - 0C05.xx
// 17.46.06.0C - A23.70.0 - 0C06.xx (with light option: OFF/1..5)
// 17.46.07.0C - A23.70.0 - 0C07.xx (with light option: OFF/1..5)
// iNet Box:
// 17.46.00.1F - T23.70.0 - 1F00.xx iNet Box
return {0x17 /*Supplied Id*/, 0x46 /*Supplied Id*/, 0x00 /*Function Id*/, 0x1F /*Function Id*/};
}
void TrumaiNetBoxApp::lin_reset_device() {
this->device_registered_ = esp_timer_get_time();
this->init_recieved_ = 0;
this->status_heater_valid_ = false;
this->status_heater_updated_ = false;
this->status_timer_valid_ = false;
this->status_timer_updated_ = false;
this->status_clock_valid_ = false;
this->status_clock_updated_ = false;
this->status_config_valid_ = false;
this->status_config_updated_ = false;
this->update_time_ = 0;
this->update_status_heater_prepared_ = false;
this->update_status_heater_unsubmitted_ = false;
this->update_status_heater_stale_ = false;
this->update_status_timer_prepared_ = false;
this->update_status_timer_unsubmitted_ = false;
this->update_status_timer_stale_ = false;
}
void TrumaiNetBoxApp::register_listener(const std::function<void(const StatusFrameHeater *)> &func) {
auto listener = StatusFrameListener{
.on_heater_change = func,
};
this->listeners_heater_.push_back(std::move(listener));
if (this->status_heater_valid_) {
func(&this->status_heater_);
}
}
void TrumaiNetBoxApp::register_listener(const std::function<void(const StatusFrameTimer *)> &func) {
auto listener = StatusFrameListener{
.on_timer_change = func,
};
this->listeners_heater_.push_back(std::move(listener));
if (this->status_timer_valid_) {
func(&this->status_timer_);
}
}
void TrumaiNetBoxApp::register_listener(const std::function<void(const StatusFrameClock *)> &func) {
auto listener = StatusFrameListener{
.on_clock_change = func,
};
this->listeners_heater_.push_back(std::move(listener));
if (this->status_clock_valid_) {
func(&this->status_clock_);
}
}
void TrumaiNetBoxApp::register_listener(const std::function<void(const StatusFrameConfig *)> &func) {
auto listener = StatusFrameListener{
.on_config_change = func,
};
this->listeners_heater_.push_back(std::move(listener));
if (this->status_config_valid_) {
func(&this->status_config_);
}
}
StatusFrameHeaterResponse *TrumaiNetBoxApp::update_heater_prepare() {
// An update is currently going on.
if (this->update_status_heater_prepared_ || this->update_status_heater_stale_) {
return &this->update_status_heater_;
}
// prepare status heater response
this->update_status_heater_ = {};
this->update_status_heater_.target_temp_room = this->status_heater_.target_temp_room;
this->update_status_heater_.heating_mode = this->status_heater_.heating_mode;
this->update_status_heater_.el_power_level_a = this->status_heater_.el_power_level_a;
this->update_status_heater_.target_temp_water = this->status_heater_.target_temp_water;
this->update_status_heater_.el_power_level_b = this->status_heater_.el_power_level_b;
this->update_status_heater_.energy_mix_a = this->status_heater_.energy_mix_a;
this->update_status_heater_.energy_mix_b = this->status_heater_.energy_mix_b;
this->update_status_heater_prepared_ = true;
return &this->update_status_heater_;
}
StatusFrameTimerResponse *TrumaiNetBoxApp::update_timer_prepare() {
// An update is currently going on.
if (this->update_status_timer_prepared_ || this->update_status_timer_stale_) {
return &this->update_status_timer_;
}
// prepare status heater response
this->update_status_timer_ = {};
this->update_status_timer_.timer_target_temp_room = this->status_timer_.timer_target_temp_room;
this->update_status_timer_.timer_heating_mode = this->status_timer_.timer_heating_mode;
this->update_status_timer_.timer_el_power_level_a = this->status_timer_.timer_el_power_level_a;
this->update_status_timer_.timer_target_temp_water = this->status_timer_.timer_target_temp_water;
this->update_status_timer_.timer_el_power_level_b = this->status_timer_.timer_el_power_level_b;
this->update_status_timer_.timer_energy_mix_a = this->status_timer_.timer_energy_mix_a;
this->update_status_timer_.timer_energy_mix_b = this->status_timer_.timer_energy_mix_b;
this->update_status_timer_.timer_resp_active = this->status_timer_.timer_active;
this->update_status_timer_.timer_resp_start_minutes = this->status_timer_.timer_start_minutes;
this->update_status_timer_.timer_resp_start_hours = this->status_timer_.timer_start_hours;
this->update_status_timer_.timer_resp_stop_minutes = this->status_timer_.timer_stop_minutes;
this->update_status_timer_.timer_resp_stop_hours = this->status_timer_.timer_stop_hours;
this->update_status_timer_prepared_ = true;
return &this->update_status_timer_;
}
bool TrumaiNetBoxApp::answer_lin_order_(const u_int8_t pid) {
// Alive message
if (pid == LIN_PID_TRUMA_INET_BOX) {
std::array<u_int8_t, 8> response = this->lin_empty_response_;
if (this->updates_to_send_.empty() && !this->has_update_to_submit_()) {
response[0] = 0xFE;
}
this->write_lin_answer_(response.data(), sizeof(response));
return true;
}
return LinBusProtocol::answer_lin_order_(pid);
}
bool TrumaiNetBoxApp::lin_read_field_by_identifier_(u_int8_t identifier, std::array<u_int8_t, 5> *response) {
this->device_registered_ = esp_timer_get_time();
if (identifier == 0x00 /* LIN Product Identification */) {
auto lin_identifier = this->lin_identifier();
(*response)[0] = lin_identifier[0];
(*response)[1] = lin_identifier[1];
(*response)[2] = lin_identifier[2];
(*response)[3] = lin_identifier[3];
(*response)[4] = 0x01; // Hardware revision
return true;
} else if (identifier == 0x20 /* Product details to display in CP plus */) {
auto lin_identifier = this->lin_identifier();
// Only the first three parts are used.
(*response)[0] = lin_identifier[0];
(*response)[1] = lin_identifier[1];
(*response)[2] = lin_identifier[2];
// (*response)[3] = // unkown
// (*response)[4] = // unkown
return true;
} else if (identifier == 0x22 /* unkown usage */) {
// Init is failing if missing
// Data can be anything?
return true;
}
return false;
}
const u_int8_t *TrumaiNetBoxApp::lin_multiframe_recieved(const u_int8_t *message, const u_int8_t message_len,
u_int8_t *return_len) {
static u_int8_t response[48] = {};
// Validate message prefix.
if (message_len < truma_message_header.size()) {
return nullptr;
}
for (u_int8_t i = 1; i < truma_message_header.size(); i++) {
if (message[i] != truma_message_header[i]) {
return nullptr;
}
}
if (message[0] == LIN_SID_READ_STATE_BUFFER) {
// Example: BA.00.1F.00.1E.00.00.22.FF.FF.FF (11)
memset(response, 0, sizeof(response));
auto response_frame = reinterpret_cast<StatusFrame *>(response);
// The order must match with the method 'has_update_to_submit_'.
if (this->init_recieved_ == 0) {
status_frame_create_init(response_frame, this->message_counter++);
// TODO: can I create a shorter (quicker) messsage here?
(*return_len) = sizeof(StatusFrame);
return response;
}
if (this->update_status_heater_unsubmitted_) {
status_frame_create_update_heater(
response_frame, this->message_counter++, this->update_status_heater_.target_temp_room,
this->update_status_heater_.target_temp_water, this->update_status_heater_.heating_mode,
this->update_status_heater_.energy_mix_a, this->update_status_heater_.el_power_level_a);
this->update_time_ = 0;
this->update_status_heater_prepared_ = false;
this->update_status_heater_unsubmitted_ = false;
this->update_status_heater_stale_ = true;
// Remove last 12 bytes (2 Frames), because they are always 0.
// This cannot be done on the first message, but later messages it is fine.
(*return_len) = sizeof(StatusFrame);
return response;
}
if (this->update_status_timer_unsubmitted_) {
status_frame_create_update_timer(
response_frame, this->message_counter++, this->update_status_timer_.timer_resp_active,
this->update_status_timer_.timer_resp_start_hours, this->update_status_timer_.timer_resp_start_minutes,
this->update_status_timer_.timer_resp_stop_hours, this->update_status_timer_.timer_resp_stop_minutes,
this->update_status_timer_.timer_target_temp_room, this->update_status_timer_.timer_target_temp_water,
this->update_status_timer_.timer_heating_mode, this->update_status_timer_.timer_energy_mix_a,
this->update_status_timer_.timer_el_power_level_a);
this->update_time_ = 0;
this->update_status_timer_prepared_ = false;
this->update_status_timer_unsubmitted_ = false;
this->update_status_timer_stale_ = true;
(*return_len) = sizeof(StatusFrame);
return response;
}
if (this->update_status_clock_unsubmitted_) {
// read time live
auto now = this->time_->now();
status_frame_create_update_clock(response_frame, this->message_counter++, now.hour, now.minute, now.second,
this->status_clock_.clock_mode);
this->update_status_clock_unsubmitted_ = false;
(*return_len) = sizeof(StatusFrame);
return response;
}
}
if (message_len < sizeof(StatusFrame) && message[0] == LIN_SID_FIll_STATE_BUFFFER) {
return nullptr;
}
auto statusFrame = reinterpret_cast<const StatusFrame *>(message);
auto header = &statusFrame->inner.genericHeader;
// Validate Truma frame checksum
if (header->checksum != data_checksum(&statusFrame->raw[10], sizeof(StatusFrame) - 10, (0xFF - header->checksum)) ||
header->header_2 != 'T' || header->header_3 != 0x01) {
ESP_LOGE(TAG, "Truma checksum fail.");
return nullptr;
}
// create acknowledge response.
response[0] = (header->service_identifier | LIN_SID_RESPONSE);
(*return_len) = 1;
if (header->message_type == STATUS_FRAME_HEATER && header->message_length == sizeof(StatusFrameHeater)) {
ESP_LOGI(TAG, "StatusFrameHeater");
// Example:
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|tRoom|mo| |elecA|tWate|elecB|mi|mi|cWate|cRoom|st|err | |
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.14.33.00.12.00.00.00.00.00.00.00.00.00.00.01.01.CC.0B.6C.0B.00.00.00.00
this->status_heater_ = statusFrame->inner.heater;
this->status_heater_valid_ = true;
this->status_heater_updated_ = true;
this->update_status_heater_stale_ = false;
return response;
} else if (header->message_type == STATUS_FRAME_TIMER && header->message_length == sizeof(StatusFrameTimer)) {
ESP_LOGI(TAG, "StatusFrameTimer");
// EXAMPLE:
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.18.3D.00.1D.18.0B.01.00.00.00.00.00.00.00.01.01.00.00.00.00.00.00.00.01.00.08.00.09
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.18.3D.00.13.18.0B.0B.00.00.00.00.00.00.00.01.01.00.00.00.00.00.00.00.01.00.08.00.09
this->status_timer_ = statusFrame->inner.timer;
this->status_timer_valid_ = true;
this->status_timer_updated_ = true;
this->update_status_timer_stale_ = false;
ESP_LOGV(TAG, "StatusFrameTimer target_temp_room: %f target_temp_water: %f %u:%u -> %u:%u %s",
temp_code_to_decimal(this->status_timer_.timer_target_temp_room),
temp_code_to_decimal(this->status_timer_.timer_target_temp_water), this->status_timer_.timer_start_hours,
this->status_timer_.timer_start_minutes, this->status_timer_.timer_stop_hours,
this->status_timer_.timer_stop_minutes, ((u_int8_t) this->status_timer_.timer_active ? " ON" : " OFF"));
return response;
} else if (header->message_type == STATUS_FRAME_RESPONSE_ACK &&
header->message_length == sizeof(StatusFrameResponseAck)) {
// Example:
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.02.0D.01.98.02.00
auto data = statusFrame->inner.responseAck;
if (data.error_code != ResponseAckResult::RESPONSE_ACK_RESULT_OKAY) {
ESP_LOGW(TAG, "StatusFrameResponseAck");
} else {
ESP_LOGI(TAG, "StatusFrameResponseAck");
}
ESP_LOGV(TAG, "StatusFrameResponseAck %02x %s %02x", statusFrame->inner.genericHeader.command_counter,
data.error_code == ResponseAckResult::RESPONSE_ACK_RESULT_OKAY ? " OKAY " : " FAILED ",
(u_int8_t) data.error_code);
if (data.error_code != ResponseAckResult::RESPONSE_ACK_RESULT_OKAY) {
// I tried to update something and it failed. Read current state again to validate and hold any updates for now.
this->lin_reset_device();
}
return response;
} else if (header->message_type == STATUS_FRAME_CLOCK && header->message_length == sizeof(StatusFrameClock)) {
ESP_LOGI(TAG, "StatusFrameClock");
// Example:
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.15.00.5B.0D.20.00.01.01.00.00.01.00.00
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.15.00.71.16.00.00.01.01.00.00.02.00.00
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.15.00.2B.16.1F.28.01.01.00.00.01.00.00
this->status_clock_ = statusFrame->inner.clock;
this->status_clock_valid_ = true;
this->status_clock_updated_ = true;
ESP_LOGV(TAG, "StatusFrameClock %02d:%02d:%02d", this->status_clock_.clock_hour, this->status_clock_.clock_minute,
this->status_clock_.clock_second);
return response;
} else if (header->message_type == STAUTS_FRAME_CONFIG && header->message_length == sizeof(StatusFrameConfig)) {
ESP_LOGI(TAG, "StatusFrameConfig");
// Example:
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.17.00.0F.06.01.B4.0A.AA.0A.00.00.00.00
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.17.00.41.06.01.B4.0A.78.0A.00.00.00.00
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0A.17.00.0F.06.01.B4.0A.AA.0A.00.00.00.00
this->status_config_ = statusFrame->inner.config;
this->status_config_valid_ = true;
this->status_config_updated_ = true;
ESP_LOGV(TAG, "StatusFrameConfig Offset: %d", offset_code_to_decimal(this->status_config_.temp_offset));
return response;
} else if (header->message_type == STATUS_FRAME_DEVICES && header->message_length == sizeof(StatusFrameDevice)) {
ESP_LOGI(TAG, "StatusFrameDevice");
// This message is special. I recieve one response per registered (at CP plus) device.
// Example:
// SID<---------PREAMBLE --------->|<---MSG HEAD --->|
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0C.0B.00.79.02.00.01.00.50.00.00.04.03.02.AD.10 - C4.03.02 0050.00
// BB.00.1F.00.1E.00.00.22.FF.FF.FF.54.01.0C.0B.00.27.02.01.01.00.40.03.22.02.00.01.00.00 - H2.00.01 0340.22
auto device = statusFrame->inner.device;
this->init_recieved_ = esp_timer_get_time();
ESP_LOGV(TAG, "StatusFrameDevice %d/%d - %d.%02d.%02d %04x.%02x", device.device_id + 1, device.device_count,
device.software_revision[0], device.software_revision[1], device.software_revision[2],
device.hardware_revision_major, device.hardware_revision_minor);
return response;
} else {
ESP_LOGW(TAG, "Unkown message type %02x", header->message_type);
}
(*return_len) = 1;
return nullptr;
}
bool TrumaiNetBoxApp::has_update_to_submit_() {
if (this->init_requested_ == 0) {
this->init_requested_ = esp_timer_get_time();
ESP_LOGV(TAG, "Requesting initial data.");
return true;
} else if (this->init_recieved_ == 0) {
auto init_wait_time = esp_timer_get_time() - this->init_requested_;
// it has been 5 seconds and i am still awaiting the init data.
if (init_wait_time > 1000 * 1000 * 5) {
ESP_LOGV(TAG, "Requesting initial data again.");
this->init_requested_ = esp_timer_get_time();
return true;
}
} else if (this->update_status_heater_unsubmitted_ || this->update_status_timer_unsubmitted_ ||
this->update_status_clock_unsubmitted_) {
if (this->update_time_ == 0) {
ESP_LOGV(TAG, "Notify CP Plus I got updates.");
this->update_time_ = esp_timer_get_time();
return true;
}
auto update_wait_time = esp_timer_get_time() - this->update_time_;
if (update_wait_time > 1000 * 1000 * 5) {
ESP_LOGV(TAG, "Notify CP Plus again I still got updates.");
this->update_time_ = esp_timer_get_time();
return true;
}
}
return false;
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,461 @@
#pragma once
#include <vector>
#include "LinBusProtocol.h"
#include "esphome/core/automation.h"
#include "esphome/components/time/real_time_clock.h"
namespace esphome {
namespace truma_inetbox {
#define LIN_PID_TRUMA_INET_BOX 0x18
#define LIN_SID_RESPONSE 0x40
#define LIN_SID_READ_STATE_BUFFER 0xBA
#define LIN_SID_FIll_STATE_BUFFFER 0xBB
// Response to init are the following frames:
// - 2 * STATUS_FRAME_DEVICES
// - STATUS_FRAME_HEATER
// - STATUS_FRAME_TIMER
// - STAUTS_FRAME_CONFIG
// - STATUS_FRAME_CLOCK
#define STATUS_FRAME_RESPONSE_INIT_REQUEST 0x0A
#define STATUS_FRAME_DEVICES 0x0B
#define STATUS_FRAME_RESPONSE_ACK 0x0D
#define STATUS_FRAME_CLOCK 0x15
#define STATUS_FRAME_CLOCK_RESPONSE (STATUS_FRAME_CLOCK - 1)
#define STAUTS_FRAME_CONFIG 0x17
#define STAUTS_FRAME_CONFIG_RESPONSE (STAUTS_FRAME_CONFIG - 1)
// Why can I send 0x33 as response?
#define STATUS_FRAME_HEATER 0x33
#define STATUS_FRAME_HEATER_RESPONSE (STATUS_FRAME_HEATER - 1)
// Error response
#define STATUS_FRAME_UNKOWN_34 0x34
// Error response
#define STATUS_FRAME_UNKOWN_36 0x36
#define STATUS_FRAME_TIMER 0x3D
#define STATUS_FRAME_TIMER_RESPONSE (STATUS_FRAME_TIMER - 1)
// Error response
#define STATUS_FRAME_UNKOWN_3E 0x3E
enum class HeatingMode : u_int8_t {
HEATING_MODE_OFF = 0x0,
HEATING_MODE_ECO = 0x1,
HEATING_MODE_HIGH = 0xA,
HEATING_MODE_BOOST = 0xB,
};
enum class ElectricPowerLevel : u_int16_t {
ELECTRIC_POWER_LEVEL_0 = 0,
ELECTRIC_POWER_LEVEL_900 = 900,
ELECTRIC_POWER_LEVEL_1800 = 1800,
};
enum class TargetTemp : u_int16_t {
TARGET_TEMP_OFF = 0x0,
// 40C
TARGET_TEMP_WATER_ECO = (40 + 273) * 10,
// 60C
TARGET_TEMP_WATER_HIGH = (60 + 273) * 10,
// 200C
TARGET_TEMP_WATER_BOOST = (200 + 273) * 10,
TARGET_TEMP_ROOM_MIN = (5 + 273) * 10,
TARGET_TEMP_ROOM_05C = (5 + 273) * 10,
TARGET_TEMP_ROOM_06C = (6 + 273) * 10,
TARGET_TEMP_ROOM_07C = (7 + 273) * 10,
TARGET_TEMP_ROOM_08C = (8 + 273) * 10,
TARGET_TEMP_ROOM_09C = (9 + 273) * 10,
TARGET_TEMP_ROOM_10C = (10 + 273) * 10,
TARGET_TEMP_ROOM_11C = (11 + 273) * 10,
TARGET_TEMP_ROOM_12C = (12 + 273) * 10,
TARGET_TEMP_ROOM_13C = (13 + 273) * 10,
TARGET_TEMP_ROOM_14C = (14 + 273) * 10,
TARGET_TEMP_ROOM_15C = (15 + 273) * 10,
TARGET_TEMP_ROOM_16C = (16 + 273) * 10,
TARGET_TEMP_ROOM_17C = (17 + 273) * 10,
TARGET_TEMP_ROOM_18C = (18 + 273) * 10,
TARGET_TEMP_ROOM_19C = (19 + 273) * 10,
TARGET_TEMP_ROOM_20C = (20 + 273) * 10,
TARGET_TEMP_ROOM_21C = (21 + 273) * 10,
TARGET_TEMP_ROOM_22C = (22 + 273) * 10,
TARGET_TEMP_ROOM_23C = (23 + 273) * 10,
TARGET_TEMP_ROOM_24C = (24 + 273) * 10,
TARGET_TEMP_ROOM_25C = (25 + 273) * 10,
TARGET_TEMP_ROOM_26C = (26 + 273) * 10,
TARGET_TEMP_ROOM_27C = (27 + 273) * 10,
TARGET_TEMP_ROOM_28C = (28 + 273) * 10,
TARGET_TEMP_ROOM_29C = (29 + 273) * 10,
TARGET_TEMP_ROOM_30C = (30 + 273) * 10,
TARGET_TEMP_ROOM_MAX = (30 + 273) * 10,
};
enum class EnergyMix : u_int8_t {
ENERGY_MIX_NONE = 0b00,
ENERGY_MIX_GAS = 0b01,
ENERGY_MIX_ELECTRICITY = 0b10,
ENERGY_MIX_MIX = 0b11,
};
enum class OperatingStatus : u_int8_t {
OPERATING_STATUS_UNSET = 0x0,
OPERATING_STATUS_OFF = 0x0,
OPERATING_STATUS_WARNING = 0x1,
OPERATING_STATUS_START_OR_COOL_DOWN = 0x4,
// ON - Heater off
OPERATING_STATUS_ON_5 = 0x5,
OPERATING_STATUS_ON_6 = 0x6,
OPERATING_STATUS_ON_7 = 0x7,
OPERATING_STATUS_ON_8 = 0x8,
OPERATING_STATUS_ON_9 = 0x9,
};
enum class OperatingUnits : u_int8_t {
OPERATING_UNITS_CELSIUS = 0x0,
OPERATING_UNITS_FAHRENHEIT = 0x1,
};
enum class Language : u_int8_t {
LANGUAGE_GERMAN = 0x0,
LANGUAGE_ENGLISH = 0x1,
LANGUAGE_FRENCH = 0x2,
LANGUAGE_ITALY = 0x3,
};
enum class ResponseAckResult : u_int8_t {
RESPONSE_ACK_RESULT_OKAY = 0x0,
RESPONSE_ACK_RESULT_ERROR_INVALID_MSG = 0x2,
// The response status frame `message_type` is unkown.
RESPONSE_ACK_RESULT_ERROR_INVALID_ID = 0x3,
};
enum class TempOffset : u_int8_t {
TEMP_OFFSET_0_0C = (u_int8_t) ((-0.0f + 17) * 10),
TEMP_OFFSET_0_5C = (u_int8_t) ((-0.5f + 17) * 10),
TEMP_OFFSET_1_0C = (u_int8_t) ((-1.0f + 17) * 10),
TEMP_OFFSET_1_5C = (u_int8_t) ((-1.5f + 17) * 10),
TEMP_OFFSET_2_0C = (u_int8_t) ((-2.0f + 17) * 10),
TEMP_OFFSET_2_5C = (u_int8_t) ((-2.5f + 17) * 10),
TEMP_OFFSET_3_0C = (u_int8_t) ((-3.0f + 17) * 10),
TEMP_OFFSET_3_5C = (u_int8_t) ((-3.5f + 17) * 10),
TEMP_OFFSET_4_0C = (u_int8_t) ((-4.0f + 17) * 10),
TEMP_OFFSET_4_5C = (u_int8_t) ((-4.5f + 17) * 10),
TEMP_OFFSET_5_0C = (u_int8_t) ((-5.0f + 17) * 10),
};
enum class ClockMode : u_int8_t {
CLOCK_MODE_24H = 0x0,
CLOCK_MODE_12H = 0x1,
};
enum class TimerActive : u_int8_t {
TIMER_ACTIVE_ON = 0x1,
TIMER_ACTIVE_OFF = 0x0,
};
enum class ClockSource : u_int8_t {
// Set by user
CLOCK_SOURCE_MANUAL = 0x1,
// Set by message
CLOCK_SOURCE_PROG = 0x2,
};
struct StatusFrameHeader { // NOLINT(altera-struct-pack-align)
// sid
u_int8_t service_identifier;
u_int8_t header[10];
u_int8_t header_2;
u_int8_t header_3;
// after checksum
u_int8_t message_length;
u_int8_t message_type;
u_int8_t command_counter;
u_int8_t checksum;
} __attribute__((packed));
// Length 20 (0x14)
struct StatusFrameHeater { // NOLINT(altera-struct-pack-align)
TargetTemp target_temp_room;
// Room
HeatingMode heating_mode;
u_int8_t heater_unkown_1;
ElectricPowerLevel el_power_level_a;
TargetTemp target_temp_water;
ElectricPowerLevel el_power_level_b;
EnergyMix energy_mix_a;
// Ignored by response
EnergyMix energy_mix_b;
u_int16_t current_temp_water;
u_int16_t current_temp_room;
OperatingStatus operating_status;
u_int16_t error_code;
u_int8_t heater_unkown_2;
} __attribute__((packed));
// Length 12 (0x0C)
struct StatusFrameHeaterResponse { // NOLINT(altera-struct-pack-align)
TargetTemp target_temp_room;
// Room
HeatingMode heating_mode;
u_int8_t recv_status_u3;
ElectricPowerLevel el_power_level_a;
TargetTemp target_temp_water;
ElectricPowerLevel el_power_level_b;
EnergyMix energy_mix_a;
// Ignored?
EnergyMix energy_mix_b;
} __attribute__((packed));
// Length 24 (0x18)
struct StatusFrameTimer { // NOLINT(altera-struct-pack-align)
TargetTemp timer_target_temp_room;
HeatingMode timer_heating_mode;
u_int8_t timer_unkown_1;
ElectricPowerLevel timer_el_power_level_a;
TargetTemp timer_target_temp_water;
ElectricPowerLevel timer_el_power_level_b;
EnergyMix timer_energy_mix_a;
EnergyMix timer_energy_mix_b;
// used by timer response message
u_int8_t unused[5];
u_int8_t timer_unknown_3;
u_int8_t timer_unknown_4;
TimerActive timer_active;
u_int8_t timer_start_minutes;
u_int8_t timer_start_hours;
u_int8_t timer_stop_minutes;
u_int8_t timer_stop_hours;
} __attribute__((packed));
// Length 13 (0x0D)
struct StatusFrameTimerResponse { // NOLINT(altera-struct-pack-align)
TargetTemp timer_target_temp_room;
HeatingMode timer_heating_mode;
u_int8_t timer_unkown_1;
ElectricPowerLevel timer_el_power_level_a;
TargetTemp timer_target_temp_water;
ElectricPowerLevel timer_el_power_level_b;
EnergyMix timer_energy_mix_a;
EnergyMix timer_energy_mix_b;
// set by response message to active timer
TimerActive timer_resp_active;
// set by response message to active timer
u_int8_t timer_resp_start_minutes;
// set by response message to active timer
u_int8_t timer_resp_start_hours;
// set by response message to active timer
u_int8_t timer_resp_stop_minutes;
// set by response message to active timer
u_int8_t timer_resp_stop_hours;
} __attribute__((packed));
// Length 2 (0x02)
struct StatusFrameResponseAck { // NOLINT(altera-struct-pack-align)
ResponseAckResult error_code;
u_int8_t unkown;
} __attribute__((packed));
// Length 10 (0x0A)
struct StatusFrameClock { // NOLINT(altera-struct-pack-align)
u_int8_t clock_hour;
u_int8_t clock_minute;
u_int8_t clock_second;
// MUST be 0x1, 0x2, 0x3..? (lower than 0x9)
u_int8_t display_1;
// MUST be 0x1
u_int8_t display_2;
u_int8_t display_3;
ClockMode clock_mode;
ClockSource clock_source;
u_int8_t display_4;
u_int8_t display_5;
} __attribute__((packed));
// Length 10 (0x0A)
struct StatusFrameConfig { // NOLINT(altera-struct-pack-align)
// 0x01 .. 0x0A
u_int8_t display_brightness;
Language language;
u_int8_t unkown_2; // 0xB4
u_int8_t unkown_3; // 0x0A
TempOffset temp_offset;
u_int8_t unkown_5; // 0x0A
OperatingUnits temp_units;
u_int8_t unkown_6;
u_int8_t unkown_7;
u_int8_t unkown_8;
} __attribute__((packed));
// Length 12 (0x0C)
struct StatusFrameDevice { // NOLINT(altera-struct-pack-align)
u_int8_t device_count;
u_int8_t device_id;
// 0x01 - Maybe active or found
u_int8_t unkown_0;
// 0x00
u_int8_t unkown_1;
u_int16_t hardware_revision_major;
u_int8_t hardware_revision_minor;
// `software_revision[0].software_revision[1].software_revision[2]`
u_int8_t software_revision[3];
// 0xAD on CPplus
// 0x00 on Combi4
u_int8_t unkown_2;
// 0x10 on CPplus
// 0x00 on Combi4
u_int8_t unkown_3;
} __attribute__((packed));
union StatusFrame { // NOLINT(altera-struct-pack-align)
u_int8_t raw[41];
struct inner { // NOLINT(altera-struct-pack-align)
StatusFrameHeader genericHeader;
union { // NOLINT(altera-struct-pack-align)
StatusFrameHeater heater;
StatusFrameHeaterResponse heaterResponse;
StatusFrameTimer timer;
StatusFrameTimerResponse timerResponse;
StatusFrameResponseAck responseAck;
StatusFrameClock clock;
StatusFrameConfig config;
StatusFrameDevice device;
} __attribute__((packed));
} inner;
} __attribute__((packed));
struct StatusFrameListener {
std::function<void(const StatusFrameHeater *)> on_heater_change;
std::function<void(const StatusFrameTimer *)> on_timer_change;
std::function<void(const StatusFrameClock *)> on_clock_change;
std::function<void(const StatusFrameConfig *)> on_config_change;
};
class TrumaiNetBoxApp : public LinBusProtocol {
public:
TrumaiNetBoxApp(u_int8_t expected_listener_count);
void update() override;
const std::array<u_int8_t, 4> lin_identifier() override;
void lin_reset_device() override;
void set_time(time::RealTimeClock *time) { time_ = time; }
time::RealTimeClock *get_time() const { return time_; }
bool get_status_heater_valid() { return this->status_heater_valid_; }
const StatusFrameHeater *get_status_heater() { return &this->status_heater_; }
void register_listener(const std::function<void(const StatusFrameHeater *)> &func);
bool get_status_timer_valid() { return this->status_timer_valid_; }
const StatusFrameTimer *get_status_timer() { return &this->status_timer_; }
void register_listener(const std::function<void(const StatusFrameTimer *)> &func);
bool get_status_clock_valid() { return this->status_clock_valid_; }
const StatusFrameClock *get_status_clock() { return &this->status_clock_; }
void register_listener(const std::function<void(const StatusFrameClock *)> &func);
bool get_status_config_valid() { return this->status_config_valid_; }
const StatusFrameConfig *get_status_config() { return &this->status_config_; }
void register_listener(const std::function<void(const StatusFrameConfig *)> &func);
bool truma_heater_can_update() { return this->status_heater_valid_; }
StatusFrameHeaterResponse *update_heater_prepare();
void update_heater_submit() { this->update_status_heater_unsubmitted_ = true; }
bool truma_timer_can_update() { return this->status_timer_valid_; }
StatusFrameTimerResponse *update_timer_prepare();
void update_timer_submit() { this->update_status_timer_unsubmitted_ = true; }
bool truma_clock_can_update() { return this->status_clock_valid_; }
void update_clock_submit() { this->update_status_clock_unsubmitted_ = true; }
int64_t get_last_cp_plus_request() { return this->device_registered_; }
// Automation
void add_on_heater_message_callback(std::function<void(const StatusFrameHeater *)> callback) {
this->state_heater_callback_.add(std::move(callback));
}
bool action_heater_room(u_int8_t temperature, HeatingMode mode = HeatingMode::HEATING_MODE_OFF);
bool action_heater_water(u_int8_t temperature);
bool action_heater_water(TargetTemp temperature);
bool action_heater_electric_power_level(u_int16_t value);
bool action_heater_energy_mix(EnergyMix energy_mix,
ElectricPowerLevel el_power_level = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0);
bool action_timer_disable();
bool action_timer_activate(u_int16_t start, u_int16_t stop, u_int8_t room_temperature,
HeatingMode mode = HeatingMode::HEATING_MODE_OFF, u_int8_t water_temperature = 0,
EnergyMix energy_mix = EnergyMix::ENERGY_MIX_NONE,
ElectricPowerLevel el_power_level = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0);
bool action_read_time();
bool action_write_time();
protected:
time::RealTimeClock *time_;
// Truma CP Plus needs init (reset). This device is not registered.
int64_t device_registered_ = 0;
int64_t init_requested_ = 0;
int64_t init_recieved_ = 0;
u_int8_t message_counter = 1;
std::vector<StatusFrameListener> listeners_heater_;
CallbackManager<void(const StatusFrameHeater *)> state_heater_callback_{};
bool status_heater_valid_ = false;
// Value has changed notify listeners.
bool status_heater_updated_ = false;
StatusFrameHeater status_heater_;
bool status_timer_valid_ = false;
// Value has changed notify listeners.
bool status_timer_updated_ = false;
StatusFrameTimer status_timer_;
bool status_clock_valid_ = false;
// Value has changed notify listeners.
bool status_clock_updated_ = false;
StatusFrameClock status_clock_;
bool status_config_valid_ = false;
// Value has changed notify listeners.
bool status_config_updated_ = false;
StatusFrameConfig status_config_;
// last time CP plus was informed I got an update msg.
int64_t update_time_ = 0;
// Prepared means `update_status_heater_` was copied from `status_heater_`.
bool update_status_heater_prepared_ = false;
// Prepared means an update is already awating fetch from CP plus.
bool update_status_heater_unsubmitted_ = false;
// I have submitted my update request to CP plus, but I have not recieved an update with new heater values from CP
// plus.
bool update_status_heater_stale_ = false;
StatusFrameHeaterResponse update_status_heater_;
// Prepared means `update_status_timer_` was copied from `status_timer_`.
bool update_status_timer_prepared_ = false;
// Prepared means an update is already awating fetch from CP plus.
bool update_status_timer_unsubmitted_ = false;
// I have submitted my update request to CP plus, but I have not recieved an update with new timer values from CP
// plus.
bool update_status_timer_stale_ = false;
StatusFrameTimerResponse update_status_timer_;
// The behaviour of `update_status_clock_unsubmitted_` is special.
// Just an update is marked. The actual package is prepared when CP Plus asks for the data in the
// `lin_multiframe_recieved` method.
bool update_status_clock_unsubmitted_ = false;
bool answer_lin_order_(const u_int8_t pid) override;
bool lin_read_field_by_identifier_(u_int8_t identifier, std::array<u_int8_t, 5> *response) override;
const u_int8_t *lin_multiframe_recieved(const u_int8_t *message, const u_int8_t message_len,
u_int8_t *return_len) override;
bool has_update_to_submit_();
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,250 @@
#include "TrumaiNetBoxApp.h"
#include "TrumaStatusFrame.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#include "helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.TrumaiNetBoxApp";
bool TrumaiNetBoxApp::action_heater_room(u_int8_t temperature, HeatingMode mode) {
if (!this->truma_heater_can_update()) {
ESP_LOGW(TAG, "Cannot update Truma.");
return false;
}
auto heater = this->update_heater_prepare();
heater->target_temp_room = decimal_to_room_temp(temperature);
// Ensure `heating_mode` and `energy_mix_a` is set.
if (heater->target_temp_room == TargetTemp::TARGET_TEMP_OFF) {
heater->heating_mode = HeatingMode::HEATING_MODE_OFF;
} else {
// If parameter `mode` contains a valid Heating mode use it or else use `ECO`.
if (mode == HeatingMode::HEATING_MODE_ECO || mode == HeatingMode::HEATING_MODE_HIGH ||
mode == HeatingMode::HEATING_MODE_BOOST) {
heater->heating_mode = mode;
} else if (heater->heating_mode == HeatingMode::HEATING_MODE_OFF) {
heater->heating_mode = HeatingMode::HEATING_MODE_ECO;
}
}
if (heater->energy_mix_a == EnergyMix::ENERGY_MIX_NONE) {
heater->energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
}
this->update_heater_submit();
return true;
}
bool TrumaiNetBoxApp::action_heater_water(u_int8_t temperature) {
if (!this->truma_heater_can_update()) {
ESP_LOGW(TAG, "Cannot update Truma.");
return false;
}
auto heater = this->update_heater_prepare();
heater->target_temp_water = deciaml_to_water_temp(temperature);
// Ensure `energy_mix_a` is set.
if (heater->target_temp_water != TargetTemp::TARGET_TEMP_OFF && heater->energy_mix_a == EnergyMix::ENERGY_MIX_NONE) {
heater->energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
}
this->update_heater_submit();
return true;
}
bool TrumaiNetBoxApp::action_heater_water(TargetTemp temperature) {
if (!this->truma_heater_can_update()) {
ESP_LOGW(TAG, "Cannot update Truma.");
return false;
}
auto heater = this->update_heater_prepare();
// If parameter `temperature` contains a valid mode use it or else use `OFF`.
if (temperature == TargetTemp::TARGET_TEMP_WATER_ECO || temperature == TargetTemp::TARGET_TEMP_WATER_HIGH ||
temperature == TargetTemp::TARGET_TEMP_WATER_BOOST) {
heater->target_temp_water = temperature;
} else {
heater->target_temp_water = TargetTemp::TARGET_TEMP_OFF;
}
// Ensure `energy_mix_a` is set.
if (heater->target_temp_water != TargetTemp::TARGET_TEMP_OFF && heater->energy_mix_a == EnergyMix::ENERGY_MIX_NONE) {
heater->energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
}
this->update_heater_submit();
return true;
}
bool TrumaiNetBoxApp::action_heater_electric_power_level(u_int16_t value) {
if (!this->truma_heater_can_update()) {
ESP_LOGW(TAG, "Cannot update Truma.");
return false;
}
auto heater = this->update_heater_prepare();
heater->el_power_level_a = decimal_to_el_power_level(value);
if (heater->el_power_level_a != ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0) {
if (heater->energy_mix_a != EnergyMix::ENERGY_MIX_MIX &&
heater->energy_mix_a != EnergyMix::ENERGY_MIX_ELECTRICITY) {
heater->energy_mix_a = EnergyMix::ENERGY_MIX_MIX;
}
} else {
heater->energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
}
this->update_heater_submit();
return true;
}
bool TrumaiNetBoxApp::action_heater_energy_mix(EnergyMix energy_mix, ElectricPowerLevel el_power_level) {
if (!this->truma_heater_can_update()) {
ESP_LOGW(TAG, "Cannot update Truma.");
return false;
}
auto heater = this->update_heater_prepare();
// If parameter `el_power_level` contains a valid mode use it.
if (el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0 ||
el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900 ||
el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_1800) {
heater->el_power_level_a = el_power_level;
}
if (energy_mix == EnergyMix::ENERGY_MIX_GAS) {
heater->energy_mix_a = energy_mix;
heater->el_power_level_a = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0;
} else if (energy_mix == EnergyMix::ENERGY_MIX_MIX || energy_mix == EnergyMix::ENERGY_MIX_ELECTRICITY) {
heater->energy_mix_a = energy_mix;
// Electric energy is requested by user without a power level. Set it to minimum.
if (heater->el_power_level_a == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0) {
heater->el_power_level_a = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900;
}
}
// This last check is reached if invalid `energy_mix` parameter was submitted.
if (heater->el_power_level_a != ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0) {
if (heater->energy_mix_a != EnergyMix::ENERGY_MIX_MIX &&
heater->energy_mix_a != EnergyMix::ENERGY_MIX_ELECTRICITY) {
heater->energy_mix_a = EnergyMix::ENERGY_MIX_MIX;
}
} else {
heater->energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
}
this->update_heater_submit();
return true;
}
bool TrumaiNetBoxApp::action_timer_disable() {
if (!this->truma_timer_can_update()) {
ESP_LOGW(TAG, "Cannot update Truma.");
return false;
}
auto timer = this->update_timer_prepare();
timer->timer_resp_active = TimerActive::TIMER_ACTIVE_OFF;
this->update_timer_submit();
return true;
}
bool TrumaiNetBoxApp::action_timer_activate(u_int16_t start, u_int16_t stop, u_int8_t room_temperature,
HeatingMode mode, u_int8_t water_temperature, EnergyMix energy_mix,
ElectricPowerLevel el_power_level) {
if (!this->truma_timer_can_update()) {
ESP_LOGW(TAG, "Cannot update Truma.");
return false;
}
if (start > 1440 || stop > 1440) {
ESP_LOGW(TAG, "Invalid values start/stop submitted.");
return false;
}
auto timer = this->update_timer_prepare();
timer->timer_resp_active = TimerActive::TIMER_ACTIVE_ON;
timer->timer_resp_start_hours = start / 60;
timer->timer_resp_start_minutes = start % 60;
timer->timer_resp_stop_hours = stop / 60;
timer->timer_resp_stop_minutes = stop % 60;
timer->timer_target_temp_room = decimal_to_room_temp(room_temperature);
// Ensure `timer_heating_mode` and `timer_energy_mix_a` is set.
if (timer->timer_target_temp_room == TargetTemp::TARGET_TEMP_OFF) {
timer->timer_heating_mode = HeatingMode::HEATING_MODE_OFF;
} else {
// If parameter `mode` contains a valid Heating mode use it or else use `ECO`.
if (mode == HeatingMode::HEATING_MODE_ECO || mode == HeatingMode::HEATING_MODE_HIGH ||
mode == HeatingMode::HEATING_MODE_BOOST) {
timer->timer_heating_mode = mode;
} else if (timer->timer_heating_mode == HeatingMode::HEATING_MODE_OFF) {
timer->timer_heating_mode = HeatingMode::HEATING_MODE_ECO;
}
}
timer->timer_target_temp_water = deciaml_to_water_temp(water_temperature);
// If parameter `el_power_level` contains a valid mode use it.
if (el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0 ||
el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900 ||
el_power_level == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_1800) {
timer->timer_el_power_level_a = el_power_level;
}
// Ensure `timer_energy_mix_a` is set
if (timer->timer_energy_mix_a == EnergyMix::ENERGY_MIX_NONE) {
timer->timer_energy_mix_a = EnergyMix::ENERGY_MIX_GAS;
}
// User has supplied a `energy_mix`
if (energy_mix == EnergyMix::ENERGY_MIX_GAS) {
timer->timer_energy_mix_a = energy_mix;
timer->timer_el_power_level_a = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0;
} else if (energy_mix == EnergyMix::ENERGY_MIX_MIX || energy_mix == EnergyMix::ENERGY_MIX_ELECTRICITY) {
timer->timer_energy_mix_a = energy_mix;
// Electric energy is requested by user without a power level. Set it to minimum.
if (timer->timer_el_power_level_a == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0) {
timer->timer_el_power_level_a = ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900;
}
}
this->update_timer_submit();
return true;
}
bool TrumaiNetBoxApp::action_read_time() {
// int ret = settimeofday(&timev, &tz);
// if (ret == EINVAL) {
// // Some ESP8266 frameworks abort when timezone parameter is not NULL
// // while ESP32 expects it not to be NULL
// ret = settimeofday(&timev, nullptr);
// }
return false;
}
bool TrumaiNetBoxApp::action_write_time() {
if (!this->truma_clock_can_update()) {
ESP_LOGW(TAG, "Cannot update Truma.");
return false;
}
auto now = this->time_->now();
if (!now.is_valid()) {
ESP_LOGW(TAG, "Invalid system time, not syncing to CP Plus.");
return false;
}
// The behaviour of this method is special.
// Just an update is marked. The actual package is prepared when CP Plus asks for the data in the
// `lin_multiframe_recieved` method.
this->update_clock_submit();
return true;
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,366 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import pins, automation
from esphome.components import sensor, uart, time
from esphome.const import (
CONF_ID,
CONF_CS_PIN,
CONF_TEMPERATURE,
CONF_ON_MESSAGE,
CONF_TRIGGER_ID,
CONF_STOP,
CONF_TIME_ID,
)
from .entity_helpers import count_id_usage
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@Fabian-Schmidt"]
CONF_TRUMA_INETBOX_ID = "truma_inetbox_id"
CONF_LIN_CHECKSUM = "lin_checksum"
CONF_FAULT_PIN = "fault_pin"
CONF_OBSERVER_MODE = "observer_mode"
CONF_NUMBER_OF_CHILDREN = "number_of_children"
CONF_ON_HEATER_MESSAGE = "on_heater_message"
truma_inetbox_ns = cg.esphome_ns.namespace("truma_inetbox")
StatusFrameHeater = truma_inetbox_ns.struct("StatusFrameHeater")
StatusFrameHeaterConstPtr = StatusFrameHeater.operator("ptr").operator("const")
TrumaINetBoxApp = truma_inetbox_ns.class_(
"TrumaiNetBoxApp", cg.PollingComponent, uart.UARTDevice
)
TrumaiNetBoxAppHeaterMessageTrigger = truma_inetbox_ns.class_(
"TrumaiNetBoxAppHeaterMessageTrigger",
automation.Trigger.template(cg.int_, cg.const_char_ptr, cg.const_char_ptr),
)
# `LIN_CHECKSUM` is a enum class and not a namespace but it works.
LIN_CHECKSUM_dummy_ns = truma_inetbox_ns.namespace("LIN_CHECKSUM")
CONF_SUPPORTED_LIN_CHECKSUM = {
"VERSION_1": LIN_CHECKSUM_dummy_ns.LIN_CHECKSUM_VERSION_1,
"VERSION_2": LIN_CHECKSUM_dummy_ns.LIN_CHECKSUM_VERSION_2,
}
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(TrumaINetBoxApp),
cv.GenerateID(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Optional(CONF_LIN_CHECKSUM, "VERSION_2"): cv.enum(CONF_SUPPORTED_LIN_CHECKSUM, upper=True),
cv.Optional(CONF_CS_PIN): pins.gpio_output_pin_schema,
cv.Optional(CONF_FAULT_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_OBSERVER_MODE): cv.boolean,
cv.Optional(CONF_ON_HEATER_MESSAGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TrumaiNetBoxAppHeaterMessageTrigger),
}
),
}
)
# Polling is for presenting data to sensors.
# Reading and communication is done in a seperate thread/core.
.extend(cv.polling_component_schema("500ms"))
.extend(uart.UART_DEVICE_SCHEMA),
cv.only_with_arduino,
cv.only_on(["esp32"]),
)
FINAL_VALIDATE_SCHEMA = cv.All(
uart.final_validate_device_schema(
# TODO: Validate 2 Stop bits are configured.
"truma_inetbox", baud_rate=9600, require_tx=True, require_rx=True
),
count_id_usage(CONF_NUMBER_OF_CHILDREN, [
CONF_TRUMA_INETBOX_ID, CONF_ID], TrumaINetBoxApp),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUMBER_OF_CHILDREN])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
time_ = await cg.get_variable(config[CONF_TIME_ID])
cg.add(var.set_time(time_))
if CONF_LIN_CHECKSUM in config:
cg.add(var.set_lin_checksum(
CONF_SUPPORTED_LIN_CHECKSUM[config[CONF_LIN_CHECKSUM]]))
if CONF_CS_PIN in config:
pin = await cg.gpio_pin_expression(config[CONF_CS_PIN])
cg.add(var.set_cs_pin(pin))
if CONF_FAULT_PIN in config:
pin = await cg.gpio_pin_expression(config[CONF_FAULT_PIN])
cg.add(var.set_fault_pin(pin))
if CONF_OBSERVER_MODE in config:
cg.add(var.set_observer_mode(config[CONF_OBSERVER_MODE]))
for conf in config.get(CONF_ON_HEATER_MESSAGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(StatusFrameHeaterConstPtr, "message")], conf
)
# AUTOMATION
CONF_ENERGY_MIX = "energy_mix"
CONF_ELECTRIC_POWER_LEVEL = "electric_power_level"
CONF_HEATING_MODE = "heating_mode"
CONF_WATT = "watt"
CONF_START = "start"
CONF_ROOM_TEMPERATURE = "room_temperature"
CONF_WATER_TEMPERATURE = "water_temperature"
HeaterRoomTempAction = truma_inetbox_ns.class_(
"HeaterRoomTempAction", automation.Action)
HeaterWaterTempAction = truma_inetbox_ns.class_(
"HeaterWaterTempAction", automation.Action)
HeaterWaterTempEnumAction = truma_inetbox_ns.class_(
"HeaterWaterTempEnumAction", automation.Action)
HeaterElecPowerLevelAction = truma_inetbox_ns.class_(
"HeaterElecPowerLevelAction", automation.Action)
HeaterEnergyMixAction = truma_inetbox_ns.class_(
"HeaterEnergyMixAction", automation.Action)
TimerDisableAction = truma_inetbox_ns.class_(
"TimerDisableAction", automation.Action)
TimerActivateAction = truma_inetbox_ns.class_(
"TimerActivateAction", automation.Action)
WriteTimeAction = truma_inetbox_ns.class_("WriteTimeAction", automation.Action)
# `EnergyMix` is a enum class and not a namespace but it works.
EnergyMix_dummy_ns = truma_inetbox_ns.namespace("EnergyMix")
CONF_SUPPORTED_ENERGY_MIX = {
"NONE": EnergyMix_dummy_ns.ENERGY_MIX_NONE,
"GAS": EnergyMix_dummy_ns.ENERGY_MIX_GAS,
"ELECTRICITY": EnergyMix_dummy_ns.ENERGY_MIX_ELECTRICITY,
"MIX": EnergyMix_dummy_ns.ENERGY_MIX_MIX,
}
# `ElectricPowerLevel` is a enum class and not a namespace but it works.
ElectricPowerLevel_dummy_ns = truma_inetbox_ns.namespace("ElectricPowerLevel")
CONF_SUPPORTED_ELECTRIC_POWER_LEVEL = {
"0": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_0,
"0W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_0,
"0 W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_0,
"900": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_900,
"900W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_900,
"900 W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_900,
"1800": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
"1800W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
"1800 W": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
"1.8kW": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
"1,8kW": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
"1.8 kW": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
"1,8 kW": ElectricPowerLevel_dummy_ns.ELECTRIC_POWER_LEVEL_1800,
}
# `HeatingMode` is a enum class and not a namespace but it works.
HeatingMode_dummy_ns = truma_inetbox_ns.namespace("HeatingMode")
CONF_SUPPORTED_HEATING_MODE = {
"OFF": HeatingMode_dummy_ns.HEATING_MODE_OFF,
"ECO": HeatingMode_dummy_ns.HEATING_MODE_ECO,
"HIGH": HeatingMode_dummy_ns.HEATING_MODE_HIGH,
"BOOST": HeatingMode_dummy_ns.HEATING_MODE_BOOST,
}
# `TargetTemp` is a enum class and not a namespace but it works.
TargetTemp_dummy_ns = truma_inetbox_ns.namespace("TargetTemp")
CONF_SUPPORTED_WATER_TEMPERATURE = {
"OFF": TargetTemp_dummy_ns.TARGET_TEMP_OFF,
"ECO": TargetTemp_dummy_ns.TARGET_TEMP_WATER_ECO,
"HIGH": TargetTemp_dummy_ns.TARGET_TEMP_WATER_HIGH,
"BOOST": TargetTemp_dummy_ns.TARGET_TEMP_WATER_BOOST,
}
@automation.register_action(
"truma_inetbox.heater.set_target_room_temperature",
HeaterRoomTempAction,
automation.maybe_conf(
CONF_TEMPERATURE,
{
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.int_range(min=0, max=30)),
cv.Optional(CONF_HEATING_MODE, "OFF"): cv.templatable(cv.enum(CONF_SUPPORTED_HEATING_MODE, upper=True)),
}
),
)
async def truma_inetbox_heater_set_target_room_temperature_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.uint8)
cg.add(var.set_temperature(template_))
template_ = await cg.templatable(config[CONF_HEATING_MODE], args, cg.uint8)
cg.add(var.set_heating_mode(template_))
return var
@automation.register_action(
"truma_inetbox.heater.set_target_water_temperature",
HeaterWaterTempAction,
automation.maybe_conf(
CONF_TEMPERATURE,
{
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.int_range(min=0, max=80)),
}
),
)
async def truma_inetbox_heater_set_target_water_temperature_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.uint8)
cg.add(var.set_temperature(template_))
return var
@automation.register_action(
"truma_inetbox.heater.set_target_water_temperature_enum",
HeaterWaterTempEnumAction,
automation.maybe_conf(
CONF_TEMPERATURE,
{
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
cv.Required(CONF_TEMPERATURE): cv.templatable(cv.enum(CONF_SUPPORTED_WATER_TEMPERATURE, upper=True))
}
),
)
async def truma_inetbox_heater_set_target_water_temperature_enum_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_TEMPERATURE], args, cg.uint16)
cg.add(var.set_temperature(template_))
return var
@automation.register_action(
"truma_inetbox.heater.set_electric_power_level",
HeaterElecPowerLevelAction,
automation.maybe_conf(
CONF_WATT,
{
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
cv.Required(CONF_WATT): cv.templatable(cv.int_range(min=0, max=1800))
}
),
)
async def truma_inetbox_heater_set_electric_power_level_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_WATT], args, cg.uint16)
cg.add(var.set_watt(template_))
return var
@automation.register_action(
"truma_inetbox.heater.set_energy_mix",
HeaterEnergyMixAction,
cv.Schema(
{
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
cv.Required(CONF_ENERGY_MIX): cv.templatable(cv.enum(CONF_SUPPORTED_ENERGY_MIX, upper=True)),
cv.Optional(CONF_WATT, 0): cv.templatable(cv.enum(CONF_SUPPORTED_ELECTRIC_POWER_LEVEL, upper=True)),
}
),
)
async def truma_inetbox_heater_set_energy_mix_level_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_ENERGY_MIX], args, cg.uint8)
cg.add(var.set_energy_mix(template_))
template_ = await cg.templatable(config[CONF_WATT], args, cg.uint16)
cg.add(var.set_watt(template_))
return var
@automation.register_action(
"truma_inetbox.timer.disable",
TimerDisableAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
}
),
)
async def truma_inetbox_timer_disable_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@automation.register_action(
"truma_inetbox.timer.activate",
TimerActivateAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
cv.Required(CONF_START): cv.templatable(cv.int_range(min=0, max=1440)),
cv.Required(CONF_STOP): cv.templatable(cv.int_range(min=0, max=1440)),
cv.Required(CONF_ROOM_TEMPERATURE): cv.templatable(cv.int_range(min=0, max=30)),
cv.Optional(CONF_HEATING_MODE, "OFF"): cv.templatable(cv.enum(CONF_SUPPORTED_HEATING_MODE, upper=True)),
cv.Optional(CONF_WATER_TEMPERATURE, 0): cv.templatable(cv.int_range(min=0, max=80)),
cv.Optional(CONF_ENERGY_MIX, "NONE"): cv.templatable(cv.enum(CONF_SUPPORTED_ENERGY_MIX, upper=True)),
cv.Optional(CONF_WATT, 0): cv.templatable(cv.enum(CONF_SUPPORTED_ELECTRIC_POWER_LEVEL, upper=True)),
}
),
)
async def truma_inetbox_timer_activate_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_START], args, cg.uint16)
cg.add(var.set_start(template_))
template_ = await cg.templatable(config[CONF_STOP], args, cg.uint16)
cg.add(var.set_stop(template_))
template_ = await cg.templatable(config[CONF_ROOM_TEMPERATURE], args, cg.uint8)
cg.add(var.set_room_temperature(template_))
template_ = await cg.templatable(config[CONF_HEATING_MODE], args, cg.uint8)
cg.add(var.set_heating_mode(template_))
template_ = await cg.templatable(config[CONF_WATER_TEMPERATURE], args, cg.uint8)
cg.add(var.set_water_temperature(template_))
template_ = await cg.templatable(config[CONF_ENERGY_MIX], args, cg.uint8)
cg.add(var.set_energy_mix(template_))
template_ = await cg.templatable(config[CONF_WATT], args, cg.uint16)
cg.add(var.set_watt(template_))
return var
@automation.register_action(
"truma_inetbox.clock.set",
WriteTimeAction,
automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(TrumaINetBoxApp),
}
),
)
async def truma_inetbox_clock_set_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var

View File

@@ -0,0 +1,91 @@
#pragma once
#include "esphome/core/component.h"
#include "TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
template<typename... Ts> class HeaterRoomTempAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
public:
TEMPLATABLE_VALUE(u_int8_t, temperature)
TEMPLATABLE_VALUE(HeatingMode, heating_mode)
void play(Ts... x) override {
this->parent_->action_heater_room(this->temperature_.value_or(x..., 0),
this->heating_mode_.value_or(x..., HeatingMode::HEATING_MODE_OFF));
}
};
template<typename... Ts> class HeaterWaterTempAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
public:
TEMPLATABLE_VALUE(u_int8_t, temperature)
void play(Ts... x) override { this->parent_->action_heater_water(this->temperature_.value_or(x..., 0)); }
};
template<typename... Ts> class HeaterWaterTempEnumAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
public:
TEMPLATABLE_VALUE(TargetTemp, temperature)
void play(Ts... x) override {
this->parent_->action_heater_water(this->temperature_.value_or(x..., TargetTemp::TARGET_TEMP_OFF));
}
};
template<typename... Ts> class HeaterElecPowerLevelAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
public:
TEMPLATABLE_VALUE(u_int16_t, watt)
void play(Ts... x) override { this->parent_->action_heater_electric_power_level(this->watt_.value_or(x..., 0)); }
};
template<typename... Ts> class HeaterEnergyMixAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
public:
TEMPLATABLE_VALUE(EnergyMix, energy_mix)
TEMPLATABLE_VALUE(ElectricPowerLevel, watt)
void play(Ts... x) override {
this->parent_->action_heater_energy_mix(this->energy_mix_.value_or(x..., EnergyMix::ENERGY_MIX_GAS),
this->watt_.value_or(x..., ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0));
}
};
template<typename... Ts> class TimerDisableAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
public:
void play(Ts... x) override { this->parent_->action_timer_disable(); }
};
template<typename... Ts> class TimerActivateAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
public:
TEMPLATABLE_VALUE(u_int16_t, start)
TEMPLATABLE_VALUE(u_int16_t, stop)
TEMPLATABLE_VALUE(u_int8_t, room_temperature)
TEMPLATABLE_VALUE(HeatingMode, heating_mode)
TEMPLATABLE_VALUE(u_int8_t, water_temperature)
TEMPLATABLE_VALUE(EnergyMix, energy_mix)
TEMPLATABLE_VALUE(ElectricPowerLevel, watt)
void play(Ts... x) override {
this->parent_->action_timer_activate(
this->start_.value(x...), this->stop_.value(x...), this->room_temperature_.value(x...),
this->heating_mode_.value_or(x..., HeatingMode::HEATING_MODE_OFF), this->water_temperature_.value_or(x..., 0),
this->energy_mix_.value_or(x..., EnergyMix::ENERGY_MIX_NONE),
this->watt_.value_or(x..., ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0));
}
};
template<typename... Ts> class WriteTimeAction : public Action<Ts...>, public Parented<TrumaiNetBoxApp> {
public:
void play(Ts... x) override { this->parent_->action_write_time(); }
};
class TrumaiNetBoxAppHeaterMessageTrigger : public Trigger<const StatusFrameHeater *> {
public:
explicit TrumaiNetBoxAppHeaterMessageTrigger(TrumaiNetBoxApp *parent) {
parent->add_on_heater_message_callback([this](const StatusFrameHeater *message) { this->trigger(message); });
}
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,21 @@
#include "TrumaCpPlusBinarySensor.h"
#include "esphome/core/log.h"
#include "esphome/components/truma_inetbox/helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.binary_sensor";
void TrumaCpPlusBinarySensor::update() {
if (this->parent_->get_lin_bus_fault() || (this->parent_->get_last_cp_plus_request() == 0)) {
this->publish_state(false);
return;
}
auto timeout = this->parent_->get_last_cp_plus_request() + 30 * 1000 * 1000 /* 30 seconds*/;
this->publish_state(esp_timer_get_time() < timeout);
}
void TrumaCpPlusBinarySensor::dump_config() { ESP_LOGCONFIG("", "Truma CP Plus Binary Sensor"); }
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,20 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
class TrumaCpPlusBinarySensor : public PollingComponent,
public binary_sensor::BinarySensor,
public Parented<TrumaiNetBoxApp> {
public:
void update() override;
void dump_config() override;
protected:
private:
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,42 @@
#include "TrumaHeaterBinarySensor.h"
#include "esphome/core/log.h"
#include "esphome/components/truma_inetbox/helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.binary_sensor";
void TrumaHeaterBinarySensor::setup() {
this->parent_->register_listener([this](const StatusFrameHeater *status_heater) {
switch (this->type_) {
case TRUMA_BINARY_SENSOR_TYPE::HEATER_ROOM:
this->publish_state(status_heater->target_temp_room != TargetTemp::TARGET_TEMP_OFF);
break;
case TRUMA_BINARY_SENSOR_TYPE::HEATER_WATER:
this->publish_state(status_heater->target_temp_water != TargetTemp::TARGET_TEMP_OFF);
break;
case TRUMA_BINARY_SENSOR_TYPE::HEATER_GAS:
this->publish_state(status_heater->energy_mix_a == EnergyMix::ENERGY_MIX_GAS);
break;
case TRUMA_BINARY_SENSOR_TYPE::HEATER_MIX_1:
this->publish_state(status_heater->energy_mix_a == EnergyMix::ENERGY_MIX_MIX &&
status_heater->el_power_level_a == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900);
break;
case TRUMA_BINARY_SENSOR_TYPE::HEATER_MIX_2:
this->publish_state(status_heater->energy_mix_a == EnergyMix::ENERGY_MIX_MIX &&
status_heater->el_power_level_a == ElectricPowerLevel::ELECTRIC_POWER_LEVEL_1800);
break;
case TRUMA_BINARY_SENSOR_TYPE::HEATER_ELECTRICITY:
this->publish_state(status_heater->energy_mix_a == EnergyMix::ENERGY_MIX_ELECTRICITY);
break;
}
});
}
void TrumaHeaterBinarySensor::dump_config() {
ESP_LOGCONFIG("", "Truma Heater Binary Sensor");
ESP_LOGCONFIG(TAG, "Type %u", this->type_);
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,24 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
#include "enum.h"
namespace esphome {
namespace truma_inetbox {
class TrumaHeaterBinarySensor : public Component, public binary_sensor::BinarySensor, public Parented<TrumaiNetBoxApp> {
public:
void setup() override;
void dump_config() override;
void set_type(TRUMA_BINARY_SENSOR_TYPE val) { this->type_ = val; }
protected:
TRUMA_BINARY_SENSOR_TYPE type_;
private:
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,31 @@
#include "TrumaTimerBinarySensor.h"
#include "esphome/core/log.h"
#include "esphome/components/truma_inetbox/helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.binary_sensor";
void TrumaTimerBinarySensor::setup() {
this->parent_->register_listener([this](const StatusFrameTimer *status_timer) {
switch (this->type_) {
case TRUMA_BINARY_SENSOR_TYPE::TIMER_ACTIVE:
this->publish_state(status_timer->timer_active == TimerActive::TIMER_ACTIVE_ON);
break;
case TRUMA_BINARY_SENSOR_TYPE::TIMER_ROOM:
this->publish_state(status_timer->timer_target_temp_room != TargetTemp::TARGET_TEMP_OFF);
break;
case TRUMA_BINARY_SENSOR_TYPE::TIMER_WATER:
this->publish_state(status_timer->timer_target_temp_water != TargetTemp::TARGET_TEMP_OFF);
break;
}
});
}
void TrumaTimerBinarySensor::dump_config() {
ESP_LOGCONFIG("", "Truma Timer Binary Sensor");
ESP_LOGCONFIG(TAG, "Type %u", this->type_);
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,23 @@
#pragma once
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
#include "enum.h"
namespace esphome {
namespace truma_inetbox {
class TrumaTimerBinarySensor : public Component, public binary_sensor::BinarySensor, public Parented<TrumaiNetBoxApp> {
public:
void setup() override;
void dump_config() override;
void set_type(TRUMA_BINARY_SENSOR_TYPE val) { this->type_ = val; }
protected:
TRUMA_BINARY_SENSOR_TYPE type_;
private:
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,70 @@
from esphome.components import binary_sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_ID,
CONF_TYPE,
CONF_UPDATE_INTERVAL
)
from .. import truma_inetbox_ns, CONF_TRUMA_INETBOX_ID, TrumaINetBoxApp
DEPENDENCIES = ["truma_inetbox"]
CODEOWNERS = ["@Fabian-Schmidt"]
TrumaSensor = truma_inetbox_ns.class_(
"TrumaBinarySensor", binary_sensor.BinarySensor, cg.Component)
# `TRUMA_BINARY_SENSOR_TYPE` is a enum class and not a namespace but it works.
TRUMA_BINARY_SENSOR_TYPE_dummy_ns = truma_inetbox_ns.namespace(
"TRUMA_BINARY_SENSOR_TYPE")
# 0 - C++ class
# 1 - C++ enum
CONF_SUPPORTED_TYPE = {
# TrumaCpPlusBinarySensor
"CP_PLUS_CONNECTED": (truma_inetbox_ns.class_("TrumaCpPlusBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent), None),
# TrumaHeaterBinarySensor
"HEATER_ROOM": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_ROOM),
"HEATER_WATER": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_WATER),
"HEATER_GAS": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_GAS),
"HEATER_MIX_1": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_MIX_1),
"HEATER_MIX_2": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_MIX_2),
"HEATER_ELECTRICITY": (truma_inetbox_ns.class_("TrumaHeaterBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.HEATER_ELECTRICITY),
# TrumaTimerBinarySensor
"TIMER_ACTIVE": (truma_inetbox_ns.class_("TrumaTimerBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.TIMER_ACTIVE),
"TIMER_ROOM": (truma_inetbox_ns.class_("TrumaTimerBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.TIMER_ROOM),
"TIMER_WATER": (truma_inetbox_ns.class_("TrumaTimerBinarySensor", binary_sensor.BinarySensor, cg.Component), TRUMA_BINARY_SENSOR_TYPE_dummy_ns.TIMER_WATER),
}
def set_default_based_on_type():
def set_defaults_(config):
# Update type based on configuration
config[CONF_ID].type = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][0]
# set defaults based on sensor type:
if config[CONF_TYPE] == "CP_PLUS_CONNECTED":
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = 500 # 0.5 seconds
return config
return set_defaults_
CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(TrumaSensor)
.extend({
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
cv.Required(CONF_TYPE): cv.enum(CONF_SUPPORTED_TYPE, upper=True),
}).extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = set_default_based_on_type()
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
await cg.register_parented(var, config[CONF_TRUMA_INETBOX_ID])
if CONF_SUPPORTED_TYPE[config[CONF_TYPE]][1]:
cg.add(var.set_type(CONF_SUPPORTED_TYPE[config[CONF_TYPE]][1]))

View File

@@ -0,0 +1,20 @@
#pragma once
namespace esphome {
namespace truma_inetbox {
enum class TRUMA_BINARY_SENSOR_TYPE {
HEATER_ROOM,
HEATER_WATER,
HEATER_GAS,
HEATER_MIX_1,
HEATER_MIX_2,
HEATER_ELECTRICITY,
TIMER_ACTIVE,
TIMER_ROOM,
TIMER_WATER,
};
}
}

View File

@@ -0,0 +1,105 @@
#include "TrumaRoomClimate.h"
#include "esphome/components/truma_inetbox/helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.truma_room_climate";
void TrumaRoomClimate::setup() {
this->parent_->register_listener([this](const StatusFrameHeater *status_heater) {
// Publish updated state
this->target_temperature = temp_code_to_decimal(status_heater->target_temp_room);
this->current_temperature = temp_code_to_decimal(status_heater->current_temp_room);
this->mode = (status_heater->operating_status >= OperatingStatus::OPERATING_STATUS_START_OR_COOL_DOWN)
? climate::CLIMATE_MODE_HEAT
: climate::CLIMATE_MODE_OFF;
switch (status_heater->heating_mode) {
case HeatingMode::HEATING_MODE_ECO:
this->preset = climate::CLIMATE_PRESET_ECO;
break;
case HeatingMode::HEATING_MODE_HIGH:
this->preset = climate::CLIMATE_PRESET_COMFORT;
break;
case HeatingMode::HEATING_MODE_BOOST:
this->preset = climate::CLIMATE_PRESET_BOOST;
break;
default:
this->preset = climate::CLIMATE_PRESET_NONE;
break;
}
this->publish_state();
});
}
void TrumaRoomClimate::dump_config() { ESP_LOGCONFIG(TAG, "Truma Room Climate"); }
void TrumaRoomClimate::control(const climate::ClimateCall &call) {
if (call.get_target_temperature().has_value()) {
float temp = *call.get_target_temperature();
this->parent_->action_heater_room(static_cast<u_int8_t>(temp));
}
if (call.get_mode().has_value()) {
// User requested mode change
climate::ClimateMode mode = *call.get_mode();
auto status_heater = this->parent_->get_status_heater();
switch (mode) {
case climate::CLIMATE_MODE_HEAT:
if (status_heater->target_temp_room == TargetTemp::TARGET_TEMP_OFF) {
this->parent_->action_heater_room(5);
}
break;
default:
this->parent_->action_heater_room(0);
break;
}
}
if (call.get_preset().has_value()) {
climate::ClimatePreset pres = *call.get_preset();
auto status_heater = this->parent_->get_status_heater();
auto current_target_temp = temp_code_to_decimal(status_heater->target_temp_room);
if (call.get_target_temperature().has_value()) {
current_target_temp = *call.get_target_temperature();
}
switch (pres) {
case climate::CLIMATE_PRESET_ECO:
this->parent_->action_heater_room(current_target_temp, HeatingMode::HEATING_MODE_ECO);
break;
case climate::CLIMATE_PRESET_COMFORT:
this->parent_->action_heater_room(current_target_temp, HeatingMode::HEATING_MODE_HIGH);
break;
case climate::CLIMATE_PRESET_BOOST:
this->parent_->action_heater_room(current_target_temp, HeatingMode::HEATING_MODE_BOOST);
break;
default:
this->parent_->action_heater_room(0);
break;
}
}
}
climate::ClimateTraits TrumaRoomClimate::traits() {
// The capabilities of the climate device
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT});
// traits.set_supported_fan_modes({{
// climate::CLIMATE_FAN_LOW,
// climate::CLIMATE_FAN_MEDIUM,
// climate::CLIMATE_FAN_HIGH,
// }});
traits.set_supported_presets({{
climate::CLIMATE_PRESET_NONE,
climate::CLIMATE_PRESET_ECO,
climate::CLIMATE_PRESET_COMFORT,
climate::CLIMATE_PRESET_BOOST,
}});
traits.set_visual_min_temperature(5);
traits.set_visual_max_temperature(30);
traits.set_visual_temperature_step(1);
return traits;
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,22 @@
#pragma once
#include "esphome/components/climate/climate.h"
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
class TrumaRoomClimate : public Component, public climate::Climate, public Parented<TrumaiNetBoxApp> {
public:
void setup() override;
void dump_config() override;
void control(const climate::ClimateCall &call) override;
climate::ClimateTraits traits() override;
protected:
private:
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,54 @@
#include "TrumaWaterClimate.h"
#include "esphome/components/truma_inetbox/helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.truma_water_climate";
void TrumaWaterClimate::setup() {
this->parent_->register_listener([this](const StatusFrameHeater *status_heater) {
// Publish updated state
this->target_temperature = temp_code_to_decimal(status_heater->target_temp_water);
this->current_temperature = temp_code_to_decimal(status_heater->current_temp_water);
this->mode = (status_heater->target_temp_water == TargetTemp::TARGET_TEMP_OFF) ? climate::CLIMATE_MODE_OFF
: climate::CLIMATE_MODE_HEAT;
this->publish_state();
});
}
void TrumaWaterClimate::dump_config() { ESP_LOGCONFIG(TAG, "Truma Climate"); }
void TrumaWaterClimate::control(const climate::ClimateCall &call) {
if (call.get_target_temperature().has_value()) {
float temp = *call.get_target_temperature();
this->parent_->action_heater_water(static_cast<u_int8_t>(temp));
}
if (call.get_mode().has_value()) {
climate::ClimateMode mode = *call.get_mode();
auto status_heater = this->parent_->get_status_heater();
switch (mode) {
case climate::CLIMATE_MODE_HEAT:
if (status_heater->target_temp_water == TargetTemp::TARGET_TEMP_OFF) {
this->parent_->action_heater_water(40);
}
break;
default:
this->parent_->action_heater_water(0);
break;
}
}
}
climate::ClimateTraits TrumaWaterClimate::traits() {
// The capabilities of the climate device
auto traits = climate::ClimateTraits();
traits.set_supports_current_temperature(true);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT});
traits.set_visual_min_temperature(40);
traits.set_visual_max_temperature(80);
traits.set_visual_temperature_step(20);
return traits;
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,22 @@
#pragma once
#include "esphome/components/climate/climate.h"
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
class TrumaWaterClimate : public Component, public climate::Climate, public Parented<TrumaiNetBoxApp> {
public:
void setup() override;
void dump_config() override;
void control(const climate::ClimateCall &call) override;
climate::ClimateTraits traits() override;
protected:
private:
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,45 @@
from esphome.components import climate
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_ID,
CONF_TYPE,
)
from .. import truma_inetbox_ns, CONF_TRUMA_INETBOX_ID, TrumaINetBoxApp
DEPENDENCIES = ["truma_inetbox"]
CODEOWNERS = ["@Fabian-Schmidt"]
TrumaClimate = truma_inetbox_ns.class_(
"TrumaClimate", climate.Climate, cg.Component)
CONF_SUPPORTED_TYPE = {
"ROOM": truma_inetbox_ns.class_("TrumaRoomClimate", climate.Climate, cg.Component),
"WATER": truma_inetbox_ns.class_("TrumaWaterClimate", climate.Climate, cg.Component),
}
def set_default_based_on_type():
def set_defaults_(config):
# update the class
config[CONF_ID].type = CONF_SUPPORTED_TYPE[config[CONF_TYPE]]
return config
return set_defaults_
CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(TrumaClimate),
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
cv.Required(CONF_TYPE): cv.enum(CONF_SUPPORTED_TYPE, upper=True),
}
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = set_default_based_on_type()
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 cg.register_parented(var, config[CONF_TRUMA_INETBOX_ID])

View File

@@ -0,0 +1,65 @@
import esphome.final_validate as fv
from esphome import core
from esphome.const import CONF_ID
def count_id_usage(property_to_update, property_to_count, property_value):
"""Validator that counts a configuration property from another entity, for use with FINAL_VALIDATE_SCHEMA.
If a property is already set, it will not be updated.
"""
def _walk_config(config, path):
walk = [path] if not isinstance(path, list) else path
for item_or_index in walk:
config = config[item_or_index]
return config
def _count_config_value(config, conf_name_list, conf_value):
ret = 0
if isinstance(config, (list, tuple)):
for config_item in config:
ret += _count_config_value(config_item,
conf_name_list, conf_value)
for conf_name in conf_name_list:
if conf_name in config and isinstance(config, (tuple)) and config[0] == conf_name and isinstance(config[1], (core.ID)) and config[1].type is conf_value:
ret += 1
elif isinstance(config, (dict)):
for config_item in config.items():
ret += _count_config_value(config_item,
conf_name_list, conf_value)
return ret
def inherit_property(config):
# Ensure `property_to_count` is a list
property_to_count_list = [property_to_count] if not isinstance(
property_to_count, list) else property_to_count
# Split the property into its path and name
if not isinstance(property_to_update, list):
property_path, property = [], property_to_update
else:
property_path, property = property_to_update[:-
1], property_to_update[-1]
# Check if the property is accessible
try:
config_part = _walk_config(config, property_path)
except KeyError:
return config
# Only update the property if it does not exist yet
if property not in config_part:
fconf = fv.full_config.get()
count = _count_config_value(
fconf, property_to_count_list, property_value)
path = fconf.get_path_for_id(config[CONF_ID])[:-1]
this_config = _walk_config(
fconf.get_config_for_path(path), property_path
)
this_config[property] = count
return config
return inherit_property

View File

@@ -0,0 +1,107 @@
#include "helpers.h"
#include "esphome/core/helpers.h"
#include "TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
u_int8_t addr_parity(const u_int8_t PID) {
u_int8_t P0 = ((PID >> 0) + (PID >> 1) + (PID >> 2) + (PID >> 4)) & 1;
u_int8_t P1 = ~((PID >> 1) + (PID >> 3) + (PID >> 4) + (PID >> 5)) & 1;
return (P0 | (P1 << 1));
}
// sum = 0 LIN 1.X CRC, sum = PID LIN 2.X CRC Enhanced
u_int8_t data_checksum(const u_int8_t *message, u_int8_t length, uint16_t sum) {
for (u_int8_t i = 0; i < length; i++) {
sum += message[i];
if (sum >= 256)
sum -= 255;
}
return (~sum);
}
float temp_code_to_decimal(u_int16_t val, float zero) {
if (val == 0) {
return zero;
}
return ((float) val) / 10.0f - 273.0f;
}
float temp_code_to_decimal(TargetTemp val, float zero) { return temp_code_to_decimal((u_int16_t) val, zero); }
TargetTemp decimal_to_room_temp(u_int8_t val) {
if (val == 0) {
return TargetTemp::TARGET_TEMP_OFF;
}
if (val <= 5) {
return TargetTemp::TARGET_TEMP_ROOM_MIN;
}
if (val >= 30) {
return TargetTemp::TARGET_TEMP_ROOM_MAX;
}
return (TargetTemp) ((((u_int16_t) val) + 273) * 10);
}
TargetTemp decimal_to_room_temp(float val) {
if (val == NAN) {
return TargetTemp::TARGET_TEMP_OFF;
}
if (val <= 5) {
return TargetTemp::TARGET_TEMP_ROOM_MIN;
}
if (val >= 30) {
return TargetTemp::TARGET_TEMP_ROOM_MAX;
}
return (TargetTemp) ((val + 273) * 10);
}
TargetTemp deciaml_to_water_temp(u_int8_t val) {
if (val < 40) {
return TargetTemp::TARGET_TEMP_OFF;
} else if (val >= 40 && val < 60) {
return TargetTemp::TARGET_TEMP_WATER_ECO;
} else if (val >= 60 && val < 80) {
return TargetTemp::TARGET_TEMP_WATER_HIGH;
} else {
return TargetTemp::TARGET_TEMP_WATER_BOOST;
}
}
float offset_code_to_decimal(TempOffset val) { return ((float) val) / 10.0f - 17.0f; }
const std::string operating_status_to_str(OperatingStatus val) {
if (val == OperatingStatus::OPERATING_STATUS_OFF) {
return "OFF";
} else if (val == OperatingStatus::OPERATING_STATUS_WARNING) {
return "WARNING";
} else if (val == OperatingStatus::OPERATING_STATUS_START_OR_COOL_DOWN) {
return "START/COOL DOWN";
} else if (val == OperatingStatus::OPERATING_STATUS_ON_5) {
return "ON (5)";
} else if (val == OperatingStatus::OPERATING_STATUS_ON_6) {
return "ON (6)";
} else if (val == OperatingStatus::OPERATING_STATUS_ON_7) {
return "ON (7)";
} else if (val == OperatingStatus::OPERATING_STATUS_ON_8) {
return "ON (8)";
} else if (val == OperatingStatus::OPERATING_STATUS_ON_9) {
return "ON (9)";
} else {
return str_snprintf("ON %x", (uint8_t) val);
}
}
ElectricPowerLevel decimal_to_el_power_level(u_int16_t val) {
if (val >= 1800) {
return ElectricPowerLevel::ELECTRIC_POWER_LEVEL_1800;
} else if (val >= 900) {
return ElectricPowerLevel::ELECTRIC_POWER_LEVEL_900;
} else {
return ElectricPowerLevel::ELECTRIC_POWER_LEVEL_0;
}
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,23 @@
#pragma once
#include "TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
// First byte is service identifier and to be ignored.
const std::array<u_int8_t, 11> truma_message_header = {0x00, 0x00, 0x1F, 0x00, 0x1E, 0x00,
0x00, 0x22, 0xFF, 0xFF, 0xFF};
u_int8_t addr_parity(const u_int8_t pid);
u_int8_t data_checksum(const u_int8_t *message, u_int8_t length, uint16_t sum);
float temp_code_to_decimal(u_int16_t val, float zero = NAN);
float temp_code_to_decimal(TargetTemp val, float zero = NAN);
TargetTemp decimal_to_room_temp(u_int8_t val);
TargetTemp decimal_to_room_temp(float val);
TargetTemp deciaml_to_water_temp(u_int8_t val);
float offset_code_to_decimal(TempOffset val);
const std::string operating_status_to_str(OperatingStatus val);
ElectricPowerLevel decimal_to_el_power_level(u_int16_t val);
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,45 @@
#include "TrumaHeaterNumber.h"
#include "esphome/core/log.h"
#include "esphome/components/truma_inetbox/helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.sensor";
void TrumaHeaterNumber::setup() {
this->parent_->register_listener([this](const StatusFrameHeater *status_heater) {
switch (this->type_) {
case TRUMA_NUMBER_TYPE::TARGET_ROOM_TEMPERATURE:
this->publish_state(temp_code_to_decimal(status_heater->target_temp_room, 0));
break;
case TRUMA_NUMBER_TYPE::TARGET_WATER_TEMPERATURE:
this->publish_state(temp_code_to_decimal(status_heater->target_temp_water, 0));
break;
case TRUMA_NUMBER_TYPE::ELECTRIC_POWER_LEVEL:
this->publish_state(static_cast<float>(status_heater->el_power_level_a));
break;
}
});
}
void TrumaHeaterNumber::control(float value) {
switch (this->type_) {
case TRUMA_NUMBER_TYPE::TARGET_ROOM_TEMPERATURE:
this->parent_->action_heater_room(static_cast<u_int8_t>(value));
break;
case TRUMA_NUMBER_TYPE::TARGET_WATER_TEMPERATURE:
this->parent_->action_heater_water(static_cast<u_int8_t>(value));
break;
case TRUMA_NUMBER_TYPE::ELECTRIC_POWER_LEVEL:
this->parent_->action_heater_water(static_cast<u_int16_t>(value));
break;
}
}
void TrumaHeaterNumber::dump_config() {
ESP_LOGCONFIG("", "Truma Heater Number");
ESP_LOGCONFIG(TAG, "Type %u", this->type_);
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,30 @@
#pragma once
#include "esphome/components/number/number.h"
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
enum class TRUMA_NUMBER_TYPE {
TARGET_ROOM_TEMPERATURE,
TARGET_WATER_TEMPERATURE,
ELECTRIC_POWER_LEVEL,
};
class TrumaHeaterNumber : public Component, public number::Number, public Parented<TrumaiNetBoxApp> {
public:
void setup() override;
void dump_config() override;
void set_type(TRUMA_NUMBER_TYPE val) { this->type_ = val; }
protected:
TRUMA_NUMBER_TYPE type_;
void control(float value) override;
private:
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,119 @@
from esphome.components import number
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_ID,
CONF_TYPE,
DEVICE_CLASS_TEMPERATURE,
CONF_UNIT_OF_MEASUREMENT,
UNIT_CELSIUS,
CONF_ICON,
ICON_THERMOMETER,
CONF_DEVICE_CLASS,
UNIT_WATT,
ICON_POWER,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_STEP,
)
from .. import truma_inetbox_ns, CONF_TRUMA_INETBOX_ID, TrumaINetBoxApp
DEPENDENCIES = ["truma_inetbox"]
CODEOWNERS = ["@Fabian-Schmidt"]
CONF_CLASS = "class"
TrumaNumber = truma_inetbox_ns.class_(
"TrumaNumber", number.Number, cg.Component)
# `TRUMA_NUMBER_TYPE` is a enum class and not a namespace but it works.
TRUMA_NUMBER_TYPE_dummy_ns = truma_inetbox_ns.namespace("TRUMA_NUMBER_TYPE")
CONF_SUPPORTED_TYPE = {
"TARGET_ROOM_TEMPERATURE": {
CONF_CLASS: truma_inetbox_ns.class_("TrumaHeaterNumber", number.Number, cg.Component),
CONF_TYPE: TRUMA_NUMBER_TYPE_dummy_ns.TARGET_ROOM_TEMPERATURE,
CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
CONF_ICON: ICON_THERMOMETER,
CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
CONF_MAX_VALUE: 30,
# Values between 0 and 5 are handeld as off.
CONF_MIN_VALUE: 0,
CONF_STEP: 1,
},
"TARGET_WATER_TEMPERATURE": {
CONF_CLASS: truma_inetbox_ns.class_("TrumaHeaterNumber", number.Number, cg.Component),
CONF_TYPE: TRUMA_NUMBER_TYPE_dummy_ns.TARGET_WATER_TEMPERATURE,
CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
CONF_ICON: ICON_THERMOMETER,
CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
CONF_MAX_VALUE: 80,
# Values between 0 and 40 are handeld as off.
CONF_MIN_VALUE: 0,
CONF_STEP: 20,
},
"ELECTRIC_POWER_LEVEL": {
CONF_CLASS: truma_inetbox_ns.class_("TrumaHeaterNumber", number.Number, cg.Component),
CONF_TYPE: TRUMA_NUMBER_TYPE_dummy_ns.ELECTRIC_POWER_LEVEL,
CONF_UNIT_OF_MEASUREMENT: UNIT_WATT,
CONF_ICON: ICON_POWER,
CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
CONF_MAX_VALUE: 1800,
CONF_MIN_VALUE: 0,
CONF_STEP: 900,
},
}
def set_default_based_on_type():
def set_defaults_(config):
# update the class
config[CONF_ID].type = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][CONF_CLASS]
# set defaults based on sensor type:
if CONF_UNIT_OF_MEASUREMENT not in config:
config[CONF_UNIT_OF_MEASUREMENT] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]
][CONF_UNIT_OF_MEASUREMENT]
if CONF_ICON not in config:
config[CONF_ICON] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][CONF_ICON]
if CONF_DEVICE_CLASS not in config:
config[CONF_DEVICE_CLASS] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]
][CONF_DEVICE_CLASS]
if CONF_MAX_VALUE not in config:
config[CONF_MAX_VALUE] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]
][CONF_MAX_VALUE]
if CONF_MIN_VALUE not in config:
config[CONF_MIN_VALUE] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]
][CONF_MIN_VALUE]
if CONF_STEP not in config:
config[CONF_STEP] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][CONF_STEP]
return config
return set_defaults_
CONFIG_SCHEMA = number.NUMBER_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(TrumaNumber),
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
cv.Required(CONF_TYPE): cv.enum(CONF_SUPPORTED_TYPE, upper=True),
cv.Optional(CONF_MAX_VALUE): cv.float_,
cv.Optional(CONF_MIN_VALUE): cv.float_,
cv.Optional(CONF_STEP): cv.positive_float,
}
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = set_default_based_on_type()
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await number.register_number(
var,
config,
min_value=config[CONF_MIN_VALUE],
max_value=config[CONF_MAX_VALUE],
step=config[CONF_STEP],
)
await cg.register_parented(var, config[CONF_TRUMA_INETBOX_ID])
cg.add(var.set_type(CONF_SUPPORTED_TYPE[config[CONF_TYPE]][CONF_TYPE]))

View File

@@ -0,0 +1,46 @@
#include "TrumaSensor.h"
#include "esphome/core/log.h"
#include "esphome/components/truma_inetbox/helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.sensor";
void TrumaSensor::setup() {
this->parent_->register_listener([this](const StatusFrameHeater *status_heater) {
switch (this->type_) {
case TRUMA_SENSOR_TYPE::CURRENT_ROOM_TEMPERATURE:
this->publish_state(temp_code_to_decimal(status_heater->current_temp_room));
break;
case TRUMA_SENSOR_TYPE::CURRENT_WATER_TEMPERATURE:
this->publish_state(temp_code_to_decimal(status_heater->current_temp_water));
break;
case TRUMA_SENSOR_TYPE::TARGET_ROOM_TEMPERATURE:
this->publish_state(temp_code_to_decimal(status_heater->target_temp_room));
break;
case TRUMA_SENSOR_TYPE::TARGET_WATER_TEMPERATURE:
this->publish_state(temp_code_to_decimal(status_heater->target_temp_water));
break;
case TRUMA_SENSOR_TYPE::HEATING_MODE:
this->publish_state(static_cast<float>(status_heater->heating_mode));
break;
case TRUMA_SENSOR_TYPE::ELECTRIC_POWER_LEVEL:
this->publish_state(static_cast<float>(status_heater->el_power_level_a));
break;
case TRUMA_SENSOR_TYPE::ENERGY_MIX:
this->publish_state(static_cast<float>(status_heater->energy_mix_a));
break;
case TRUMA_SENSOR_TYPE::OPERATING_STATUS:
this->publish_state(static_cast<float>(status_heater->operating_status));
break;
}
});
}
void TrumaSensor::dump_config() {
LOG_SENSOR("", "Truma Sensor", this);
ESP_LOGCONFIG(TAG, "Type %u", this->type_);
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,32 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
enum class TRUMA_SENSOR_TYPE {
CURRENT_ROOM_TEMPERATURE,
CURRENT_WATER_TEMPERATURE,
TARGET_ROOM_TEMPERATURE,
TARGET_WATER_TEMPERATURE,
HEATING_MODE,
ELECTRIC_POWER_LEVEL,
ENERGY_MIX,
OPERATING_STATUS,
};
class TrumaSensor : public Component, public sensor::Sensor, public Parented<TrumaiNetBoxApp> {
public:
void setup() override;
void dump_config() override;
void set_type(TRUMA_SENSOR_TYPE val) { this->type_ = val; }
protected:
TRUMA_SENSOR_TYPE type_;
private:
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,82 @@
from esphome.components import sensor
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_ID,
CONF_TYPE,
DEVICE_CLASS_TEMPERATURE,
CONF_UNIT_OF_MEASUREMENT,
UNIT_CELSIUS,
CONF_ICON,
ICON_THERMOMETER,
STATE_CLASS_MEASUREMENT,
CONF_ACCURACY_DECIMALS,
CONF_DEVICE_CLASS,
UNIT_WATT,
UNIT_EMPTY,
ICON_GAS_CYLINDER,
ICON_POWER,
)
from .. import truma_inetbox_ns, CONF_TRUMA_INETBOX_ID, TrumaINetBoxApp
DEPENDENCIES = ["truma_inetbox"]
CODEOWNERS = ["@Fabian-Schmidt"]
TrumaSensor = truma_inetbox_ns.class_(
"TrumaSensor", sensor.Sensor, cg.Component)
# `TRUMA_SENSOR_TYPE` is a enum class and not a namespace but it works.
TRUMA_SENSOR_TYPE_dummy_ns = truma_inetbox_ns.namespace("TRUMA_SENSOR_TYPE")
# 0 - C++ enum
# 1 - CONF_UNIT_OF_MEASUREMENT
# 2 - CONF_ICON
# 3 - CONF_ACCURACY_DECIMALS
# 4 - CONF_DEVICE_CLASS
CONF_SUPPORTED_TYPE = {
"CURRENT_ROOM_TEMPERATURE": (TRUMA_SENSOR_TYPE_dummy_ns.CURRENT_ROOM_TEMPERATURE, UNIT_CELSIUS, ICON_THERMOMETER, 1, DEVICE_CLASS_TEMPERATURE),
"CURRENT_WATER_TEMPERATURE": (TRUMA_SENSOR_TYPE_dummy_ns.CURRENT_WATER_TEMPERATURE, UNIT_CELSIUS, ICON_THERMOMETER, 1, DEVICE_CLASS_TEMPERATURE),
"TARGET_ROOM_TEMPERATURE": (TRUMA_SENSOR_TYPE_dummy_ns.TARGET_ROOM_TEMPERATURE, UNIT_CELSIUS, ICON_THERMOMETER, 0, DEVICE_CLASS_TEMPERATURE),
"TARGET_WATER_TEMPERATURE": (TRUMA_SENSOR_TYPE_dummy_ns.TARGET_WATER_TEMPERATURE, UNIT_CELSIUS, ICON_THERMOMETER, 0, DEVICE_CLASS_TEMPERATURE),
"HEATING_MODE": (TRUMA_SENSOR_TYPE_dummy_ns.HEATING_MODE, UNIT_EMPTY, ICON_THERMOMETER, 0, DEVICE_CLASS_TEMPERATURE),
"ELECTRIC_POWER_LEVEL": (TRUMA_SENSOR_TYPE_dummy_ns.ELECTRIC_POWER_LEVEL, UNIT_WATT, ICON_POWER, 0, DEVICE_CLASS_TEMPERATURE),
"ENERGY_MIX": (TRUMA_SENSOR_TYPE_dummy_ns.ENERGY_MIX, UNIT_EMPTY, ICON_GAS_CYLINDER, 0, DEVICE_CLASS_TEMPERATURE),
"OPERATING_STATUS": (TRUMA_SENSOR_TYPE_dummy_ns.OPERATING_STATUS, UNIT_EMPTY, ICON_POWER, 0, DEVICE_CLASS_TEMPERATURE),
}
def set_default_based_on_type():
def set_defaults_(config):
# set defaults based on sensor type:
if CONF_UNIT_OF_MEASUREMENT not in config:
config[CONF_UNIT_OF_MEASUREMENT] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][1]
if CONF_ICON not in config:
config[CONF_ICON] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][2]
if CONF_ACCURACY_DECIMALS not in config:
config[CONF_ACCURACY_DECIMALS] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][3]
if CONF_DEVICE_CLASS not in config:
config[CONF_DEVICE_CLASS] = CONF_SUPPORTED_TYPE[config[CONF_TYPE]][4]
return config
return set_defaults_
CONFIG_SCHEMA = sensor.sensor_schema(
state_class=STATE_CLASS_MEASUREMENT
).extend(
{
cv.GenerateID(): cv.declare_id(TrumaSensor),
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
cv.Required(CONF_TYPE): cv.enum(CONF_SUPPORTED_TYPE, upper=True),
}
).extend(cv.COMPONENT_SCHEMA)
FINAL_VALIDATE_SCHEMA = set_default_based_on_type()
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
await cg.register_parented(var, config[CONF_TRUMA_INETBOX_ID])
cg.add(var.set_type(CONF_SUPPORTED_TYPE[config[CONF_TYPE]][0]))

View File

@@ -0,0 +1,47 @@
#include "TrumaTime.h"
#include "esphome/core/log.h"
#include "esphome/components/truma_inetbox/helpers.h"
namespace esphome {
namespace truma_inetbox {
static const char *const TAG = "truma_inetbox.time";
void TrumaTime::setup() {
this->parent_->register_listener([this](const StatusFrameClock *status_clock) {
if (this->auto_disable_count_ > 0) {
if (this->read_time() && this->auto_disable_) {
this->auto_disable_count_--;
}
}
});
}
void TrumaTime::update() {}
void TrumaTime::dump_config() { ESP_LOGCONFIG(TAG, "Truma Time", this); }
bool TrumaTime::read_time() {
if (!this->parent_->get_status_clock_valid()) {
return false;
}
auto status_clock = this->parent_->get_status_clock();
time::ESPTime rtc_time{.second = status_clock->clock_second,
.minute = status_clock->clock_minute,
.hour = status_clock->clock_hour,
.day_of_week = 1,
.day_of_month = 1,
.day_of_year = 1, // ignored by recalc_timestamp_utc(false)
.month = 1,
.year = 2020};
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return false;
}
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
return true;
}
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,26 @@
#pragma once
#include "esphome/components/time/real_time_clock.h"
#include "esphome/components/truma_inetbox/TrumaiNetBoxApp.h"
namespace esphome {
namespace truma_inetbox {
class TrumaTime : public time::RealTimeClock, public Parented<TrumaiNetBoxApp> {
public:
void setup() override;
void update() override;
void dump_config() override;
bool read_time();
void set_auto_disable(bool val) { this->auto_disable_ = val; }
protected:
bool auto_disable_ = false;
u_int8_t auto_disable_count_ = 3;
private:
};
} // namespace truma_inetbox
} // namespace esphome

View File

@@ -0,0 +1,34 @@
from esphome.components import time
import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.const import (
CONF_ID,
CONF_TYPE,
)
from .. import truma_inetbox_ns, CONF_TRUMA_INETBOX_ID, TrumaINetBoxApp
DEPENDENCIES = ["truma_inetbox"]
CODEOWNERS = ["@Fabian-Schmidt"]
CONF_AUTO_DISABLE = "auto_disable"
TrumaTime = truma_inetbox_ns.class_(
"TrumaTime", time.RealTimeClock, cg.Component)
CONFIG_SCHEMA = time.TIME_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(TrumaTime),
cv.GenerateID(CONF_TRUMA_INETBOX_ID): cv.use_id(TrumaINetBoxApp),
cv.Optional(CONF_AUTO_DISABLE, default=True): cv.boolean,
}
).extend(cv.polling_component_schema("never"))
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await time.register_time(var, config)
await cg.register_parented(var, config[CONF_TRUMA_INETBOX_ID])
if config[CONF_AUTO_DISABLE]:
cg.add(var.set_auto_disable(config[CONF_AUTO_DISABLE]))