Merge pull request #27 from ElVit/heatpump/feature/uart-task-handling
Use threads to read UART message
This commit is contained in:
commit
41b7588eea
|
|
@ -26,7 +26,7 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
board: [esp8266, esp32s2, esp32c3, full]
|
board: [esp32s2, esp32c3, full]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
### What you need
|
### What you need
|
||||||
|
|
||||||
* ESPHome compatible microcontroller (e.g. ESP8266, ESP32, ESP32-S2, ESP32-C3, ...)
|
* ESPHome compatible microcontroller (e.g. ESP32, ESP32-S2, ESP32-C3, ...)
|
||||||
* RJ12 cable (phone cable, RJ11 may also work)
|
* RJ12 cable (phone cable, RJ11 may also work)
|
||||||
|
|
||||||
### Wiring
|
### Wiring
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,28 @@
|
||||||
|
|
||||||
### What you need
|
### What you need
|
||||||
|
|
||||||
* ESPHome compatible microcontroller (e.g. ESP8266, ESP32, ESP32-S2, ESP32-C3, ...)
|
* ESPHome compatible microcontroller (e.g. ESP32, ESP32-S2, ESP32-C3, ...)
|
||||||
* ADUM1201 Dual Channel Digital Magnetic Isolator
|
* ADUM1201 Dual Channel Digital Magnetic Isolator
|
||||||
(to convert 5V UART signal from the heatpump to 3.3V UART signal of the ESP controller)
|
(to convert 5V UART signal from the heatpump to 3.3V UART signal of the ESP controller)
|
||||||
* CN-CNT cable/connectors to Heatpump/CZ-TAW1 (see [Heishamon](https://github.com/Egyras/HeishaMon) github site for more information)
|
* CN-CNT cable/connectors to Heatpump/CZ-TAW1 (see [Heishamon](https://github.com/heishamon/HeishaMon) github site for more information)
|
||||||
* For example: [S05B-XASK-1 JST Connector](https://a.aliexpress.com/_EvkmGVo)
|
* For example: [S05B-XASK-1 JST Connector](https://a.aliexpress.com/_EvkmGVo)
|
||||||
* For example: [XAP-05V-1 5Pin Cable with Female to Female Connector](https://a.aliexpress.com/_ExPT82E)
|
* For example: [XAP-05V-1 5Pin Cable with Female to Female Connector](https://a.aliexpress.com/_ExPT82E)
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> The support for ESP8266 and the arduino framework is deprecated for this component.
|
||||||
|
> Since v0.0.8 this component uses threads to process the UART communication.
|
||||||
|
> But if you still want to use an ESP8266 controller or the arduino framework,
|
||||||
|
> please use the branch "heatpump/arduino".
|
||||||
|
>
|
||||||
|
> ```yaml
|
||||||
|
> external_components:
|
||||||
|
> - source:
|
||||||
|
> type: git
|
||||||
|
> url: https://github.com/ElVit/esphome_components
|
||||||
|
> ref: 'heatpump/arduino'
|
||||||
|
> components: [ panasonic_heatpump ]
|
||||||
|
> ```
|
||||||
|
|
||||||
### Wiring
|
### Wiring
|
||||||
|
|
||||||

|

|
||||||
|
|
@ -644,7 +659,7 @@ water_heater:
|
||||||
|
|
||||||
## Custom Entities (For Advanced Users)
|
## Custom Entities (For Advanced Users)
|
||||||
|
|
||||||
If you review the [ProtocolByteDecrypt.md](https://github.com/Egyras/HeishaMon/blob/master/ProtocolByteDecrypt.md) file you will find also some TOPs and SETs which are not implemented yet in heishamon.
|
If you review the [ProtocolByteDecrypt.md](https://github.com/heishamon/HeishaMon/blob/main/ProtocolByteDecrypt.md) file you will find also some TOPs and SETs which are not implemented yet in heishamon.
|
||||||
They are usually marked as TOP (without a number).
|
They are usually marked as TOP (without a number).
|
||||||
The nice part of ESPHome is that it is so highly customizeable.
|
The nice part of ESPHome is that it is so highly customizeable.
|
||||||
So if you want some additional TOP or SET entities you can easily create your own.
|
So if you want some additional TOP or SET entities you can easily create your own.
|
||||||
|
|
@ -664,7 +679,7 @@ sensor:
|
||||||
unit_of_measurement: °C
|
unit_of_measurement: °C
|
||||||
lambda: |-
|
lambda: |-
|
||||||
// get the requried byte
|
// get the requried byte
|
||||||
int byte = my_heatpump->getResponseByte(46);
|
int byte = my_heatpump->get_response_byte(46);
|
||||||
// a valid byte range is 0x00-0xFF
|
// a valid byte range is 0x00-0xFF
|
||||||
// do not update if the byte is invalid
|
// do not update if the byte is invalid
|
||||||
if (byte < 0) return {};
|
if (byte < 0) return {};
|
||||||
|
|
@ -678,7 +693,7 @@ text_sensor:
|
||||||
update_interval: 3s
|
update_interval: 3s
|
||||||
lambda: |-
|
lambda: |-
|
||||||
// get the requried byte
|
// get the requried byte
|
||||||
int byte = my_heatpump->getResponseByte(9);
|
int byte = my_heatpump->get_response_byte(9);
|
||||||
// a valid byte range is 0x00-0xFF
|
// a valid byte range is 0x00-0xFF
|
||||||
// do not update if the byte is invalid
|
// do not update if the byte is invalid
|
||||||
if (byte < 0) return {};
|
if (byte < 0) return {};
|
||||||
|
|
@ -703,5 +718,5 @@ After a power on the heatpump should respond to the requests.
|
||||||
|
|
||||||
## Sources
|
## Sources
|
||||||
|
|
||||||
:heart: A big THANKS to [Egyras](https://github.com/Egyras) and the work done on the repository [HeishaMon](https://github.com/Egyras/HeishaMon) for decoding the panasonic uart protocol and providing information to build hardware based on an ESP Chip.
|
:heart: A big THANKS to [Egyras](https://github.com/Egyras), [IgorYbema](https://github.com/IgorYbema) and the work done on the repository [HeishaMon](https://github.com/heishamon/HeishaMon) for decoding the panasonic uart protocol and providing information to build hardware based on an ESP Chip.
|
||||||
:heart: Thanks to the whole home assistant community for sharing their knowlege and helping me to create this ESPHome component!
|
:heart: Thanks to the whole home assistant community for sharing their knowlege and helping me to create this ESPHome component!
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ climate::ClimateTraits PanasonicHeatpumpClimate::traits() {
|
||||||
|
|
||||||
void PanasonicHeatpumpClimate::control(const climate::ClimateCall& call) {
|
void PanasonicHeatpumpClimate::control(const climate::ClimateCall& call) {
|
||||||
if (call.get_mode().has_value()) {
|
if (call.get_mode().has_value()) {
|
||||||
int byte6 = this->parent_->getResponseByte(6);
|
int byte6 = this->parent_->get_response_byte(6);
|
||||||
if (byte6 >= 0) {
|
if (byte6 >= 0) {
|
||||||
climate::ClimateMode new_mode = *call.get_mode();
|
climate::ClimateMode new_mode = *call.get_mode();
|
||||||
uint8_t newByte6 = this->setClimateMode(new_mode, (uint8_t)byte6);
|
uint8_t newByte6 = this->setClimateMode(new_mode, (uint8_t)byte6);
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,13 @@ namespace esphome {
|
||||||
namespace panasonic_heatpump {
|
namespace panasonic_heatpump {
|
||||||
static const char* const TAG = "panasonic_heatpump";
|
static const char* const TAG = "panasonic_heatpump";
|
||||||
|
|
||||||
void PanasonicHelpers::log_uart_hex(UartLogDirection direction, const std::vector<uint8_t>& data,
|
void PanasonicHelpers::write_uart_log(UartLogDirection direction, const std::vector<uint8_t>& data,
|
||||||
const char separator) {
|
const char separator, bool logBytes) {
|
||||||
PanasonicHelpers::log_uart_hex(direction, &data[0], data.size(), separator);
|
PanasonicHelpers::write_uart_log(direction, &data[0], data.size(), separator, logBytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PanasonicHelpers::log_uart_hex(UartLogDirection direction, const uint8_t* data, const size_t length,
|
void PanasonicHelpers::write_uart_log(UartLogDirection direction, const uint8_t* data, const size_t length,
|
||||||
const char separator) {
|
const char separator, bool logBytes) {
|
||||||
std::string logStr = "";
|
std::string logStr = "";
|
||||||
std::string msgDir = direction == UART_LOG_TX ? ">>>" : "<<<";
|
std::string msgDir = direction == UART_LOG_TX ? ">>>" : "<<<";
|
||||||
std::string msgType = direction == UART_LOG_TX ? "request" : "response";
|
std::string msgType = direction == UART_LOG_TX ? "request" : "response";
|
||||||
|
|
@ -31,6 +31,9 @@ void PanasonicHelpers::log_uart_hex(UartLogDirection direction, const uint8_t* d
|
||||||
ESP_LOGI(TAG, "%s %s[%i]", msgDir.c_str(), msgType.c_str(), length);
|
ESP_LOGI(TAG, "%s %s[%i]", msgDir.c_str(), msgType.c_str(), length);
|
||||||
delay(10);
|
delay(10);
|
||||||
|
|
||||||
|
if (!logBytes)
|
||||||
|
return;
|
||||||
|
|
||||||
logStr += byte_array_to_hex_string(data, length, separator);
|
logStr += byte_array_to_hex_string(data, length, separator);
|
||||||
|
|
||||||
// Log in chunks to avoid ESP_LOG buffer overflow (https://developers.esphome.io/architecture/logging/).
|
// Log in chunks to avoid ESP_LOG buffer overflow (https://developers.esphome.io/architecture/logging/).
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,10 @@ enum UartLogDirection : uint8_t {
|
||||||
|
|
||||||
class PanasonicHelpers {
|
class PanasonicHelpers {
|
||||||
public:
|
public:
|
||||||
static void log_uart_hex(UartLogDirection direction, const std::vector<uint8_t>& data, const char separator);
|
static void write_uart_log(UartLogDirection direction, const std::vector<uint8_t>& data, const char separator,
|
||||||
static void log_uart_hex(UartLogDirection direction, const uint8_t* data, const size_t length, const char separator);
|
bool logBytes);
|
||||||
|
static void write_uart_log(UartLogDirection direction, const uint8_t* data, const size_t length, const char separator,
|
||||||
|
bool logBytes);
|
||||||
static std::string byte_array_to_hex_string(const std::vector<uint8_t>& data, const char separator);
|
static std::string byte_array_to_hex_string(const std::vector<uint8_t>& data, const char separator);
|
||||||
static std::string byte_array_to_hex_string(const uint8_t* data, const size_t length, const char separator);
|
static std::string byte_array_to_hex_string(const uint8_t* data, const size_t length, const char separator);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -25,51 +25,75 @@ void PanasonicHeatpumpComponent::dump_config() {
|
||||||
|
|
||||||
void PanasonicHeatpumpComponent::setup() {
|
void PanasonicHeatpumpComponent::setup() {
|
||||||
ESP_LOGCONFIG(TAG, "Setting up Panasonic Heatpump ...");
|
ESP_LOGCONFIG(TAG, "Setting up Panasonic Heatpump ...");
|
||||||
delay(10); // NOLINT
|
|
||||||
this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
|
||||||
|
|
||||||
this->update();
|
this->response_queue_handle_ = xQueueCreate(8, sizeof(std::vector<uint8_t>*));
|
||||||
|
if (this->response_queue_handle_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create response queue!");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->request_queue_handle_ = xQueueCreate(8, sizeof(std::vector<uint8_t>*));
|
||||||
|
if (this->request_queue_handle_ == nullptr) {
|
||||||
|
ESP_LOGE(TAG, "Failed to create request queue!");
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start task
|
||||||
|
xTaskCreatePinnedToCore(PanasonicHeatpumpComponent::uart_task, "uart_handler", 4096, this,
|
||||||
|
tskIDLE_PRIORITY + 1, // Low priority, important for single-core C3
|
||||||
|
&this->uart_task_handle_,
|
||||||
|
tskNO_AFFINITY // important for single-core C3
|
||||||
|
);
|
||||||
|
if (this->uart_client_ != nullptr) {
|
||||||
|
xTaskCreatePinnedToCore(PanasonicHeatpumpComponent::uart_client_task, "uart_client_handler", 4096, this,
|
||||||
|
tskIDLE_PRIORITY + 1, // Low priority, important for single-core C3
|
||||||
|
&this->uart_client_task_handle_,
|
||||||
|
tskNO_AFFINITY // important for single-core C3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->uart_client_ != nullptr && this->uart_client_timeout_ < 100) {
|
||||||
|
ESP_LOGI(TAG, "Self polling disabled (uart_client_timeout_ < 100ms). Not sending initial request.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void PanasonicHeatpumpComponent::update() {
|
void PanasonicHeatpumpComponent::update() {
|
||||||
if (this->uart_client_ != nullptr)
|
// Do not send polling requests if a uart client (CZ-TAW1) is configured and timeout is set too low.
|
||||||
|
if (this->uart_client_ != nullptr && this->uart_client_timeout_ < 100)
|
||||||
return;
|
return;
|
||||||
this->next_request_ = this->send_extra_request_ ? RequestType::POLLING_EXTRA : RequestType::POLLING;
|
|
||||||
|
// If a uart client (CZ-TAW1) is configured, check if the last request from the client is too long ago.
|
||||||
|
// If so, send polling request to heatpump again.
|
||||||
|
if (this->uart_client_ != nullptr && !this->uart_client_timeout_exceeded_) {
|
||||||
|
if (millis() - this->last_client_request_time_ > uart_client_timeout_)
|
||||||
|
this->uart_client_timeout_exceeded_ = true;
|
||||||
|
else
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Queue polling request");
|
||||||
|
this->queue_request(build_message(PanasonicCommand::PollingMessage));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PanasonicHeatpumpComponent::loop() {
|
void PanasonicHeatpumpComponent::loop() {
|
||||||
// Check if no request was sent for uart_client_timeout when uart_client is configured
|
|
||||||
if (this->uart_client_ != nullptr && this->uart_client_timeout_ > 100) {
|
|
||||||
uint32_t current_time = millis();
|
|
||||||
if (current_time - this->last_request_time_ >= this->uart_client_timeout_) {
|
|
||||||
this->next_request_ = RequestType::POLLING;
|
|
||||||
this->uart_client_timeout_exceeded_ = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (this->loop_state_) {
|
switch (this->loop_state_) {
|
||||||
case LoopState::READ_RESPONSE:
|
case LoopState::READ_RESPONSE: {
|
||||||
this->read_response();
|
switch (this->read_response()) {
|
||||||
this->loop_state_ = LoopState::CHECK_RESPONSE;
|
|
||||||
break;
|
|
||||||
case LoopState::CHECK_RESPONSE:
|
|
||||||
this->current_response_ = this->check_response(this->response_message_);
|
|
||||||
switch (this->current_response_) {
|
|
||||||
case ResponseType::UNKNOWN:
|
|
||||||
this->response_message_.clear();
|
|
||||||
this->loop_state_ = LoopState::SEND_REQUEST;
|
|
||||||
break;
|
|
||||||
case ResponseType::RECEIVING:
|
|
||||||
this->loop_state_ = LoopState::SEND_REQUEST;
|
|
||||||
break;
|
|
||||||
case ResponseType::STANDARD:
|
case ResponseType::STANDARD:
|
||||||
this->loop_state_ = LoopState::PUBLISH_SENSOR;
|
this->loop_state_ = LoopState::PUBLISH_SENSOR;
|
||||||
break;
|
break;
|
||||||
case ResponseType::EXTRA:
|
case ResponseType::EXTRA:
|
||||||
this->loop_state_ = LoopState::PUBLISH_EXTRA_SENSOR;
|
this->loop_state_ = LoopState::PUBLISH_EXTRA_SENSOR;
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
this->loop_state_ = LoopState::SEND_REQUEST;
|
||||||
|
break;
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case LoopState::PUBLISH_SENSOR:
|
case LoopState::PUBLISH_SENSOR:
|
||||||
for (auto* entity : this->sensors_) {
|
for (auto* entity : this->sensors_) {
|
||||||
entity->publish_new_state(this->heatpump_default_message_);
|
entity->publish_new_state(this->heatpump_default_message_);
|
||||||
|
|
@ -125,233 +149,226 @@ void PanasonicHeatpumpComponent::loop() {
|
||||||
this->loop_state_ = LoopState::SEND_REQUEST;
|
this->loop_state_ = LoopState::SEND_REQUEST;
|
||||||
break;
|
break;
|
||||||
case LoopState::SEND_REQUEST:
|
case LoopState::SEND_REQUEST:
|
||||||
this->send_request(this->next_request_);
|
this->send_request();
|
||||||
this->loop_state_ = LoopState::READ_REQUEST;
|
// fallthrough
|
||||||
break;
|
|
||||||
case LoopState::READ_REQUEST:
|
|
||||||
this->read_request();
|
|
||||||
this->loop_state_ = LoopState::RESTART_LOOP;
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
this->loop_state_ = LoopState::READ_RESPONSE;
|
this->loop_state_ = LoopState::READ_RESPONSE;
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
void PanasonicHeatpumpComponent::read_response() {
|
void PanasonicHeatpumpComponent::uart_task(void* pvParameters) {
|
||||||
while (this->available()) {
|
auto* self = static_cast<PanasonicHeatpumpComponent*>(pvParameters);
|
||||||
// Read each byte from heatpump and forward it directly to the client (CZ-TAW1)
|
std::vector<uint8_t> rx_buffer;
|
||||||
this->read_byte(&byte_);
|
rx_buffer.reserve(256);
|
||||||
if (this->uart_client_ != nullptr) {
|
|
||||||
this->uart_client_->write_byte(byte_);
|
while (true) {
|
||||||
|
// Process the data from the UART interface connected to the heatpump
|
||||||
|
if (self->receive_from_uart(self->parent_, rx_buffer)) {
|
||||||
|
auto* message = new std::vector<uint8_t>(rx_buffer);
|
||||||
|
if (xQueueSend(self->response_queue_handle_, &message, 0) != pdPASS) {
|
||||||
|
ESP_LOGW(TAG, "Response queue full or unavailable, dropping message");
|
||||||
|
delete message;
|
||||||
}
|
}
|
||||||
|
// ... and pass on a copy to CZ-TAW1
|
||||||
|
if (self->uart_client_ != nullptr) {
|
||||||
|
self->uart_client_->write_array(rx_buffer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PanasonicHeatpumpComponent::uart_client_task(void* pvParameters) {
|
||||||
|
auto* self = static_cast<PanasonicHeatpumpComponent*>(pvParameters);
|
||||||
|
std::vector<uint8_t> rx_buffer;
|
||||||
|
rx_buffer.reserve(256);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
// Process the data from the UART interface connected to the client (CZ-TAW1)
|
||||||
|
if (self->receive_from_uart(self->uart_client_, rx_buffer)) {
|
||||||
|
auto* message = new std::vector<uint8_t>(rx_buffer);
|
||||||
|
if (xQueueSend(self->request_queue_handle_, &message, 0) != pdPASS) {
|
||||||
|
ESP_LOGW(TAG, "Request queue full or unavailable, dropping message");
|
||||||
|
delete message;
|
||||||
|
}
|
||||||
|
self->last_client_request_time_ = millis();
|
||||||
|
self->uart_client_timeout_exceeded_ = false;
|
||||||
|
} else {
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used for both uart interfaces
|
||||||
|
bool PanasonicHeatpumpComponent::receive_from_uart(uart::UARTComponent* uartComp, std::vector<uint8_t>& buffer) {
|
||||||
|
uint8_t start_byte;
|
||||||
|
|
||||||
|
// Wait for the start byte to be available
|
||||||
|
while (!uartComp->available())
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
|
|
||||||
|
// Read the first byte
|
||||||
|
if (!uartComp->read_byte(&start_byte))
|
||||||
|
return false;
|
||||||
|
|
||||||
// Message shall start with 0x31, 0x71 or 0xF1, if not skip this byte
|
// Message shall start with 0x31, 0x71 or 0xF1, if not skip this byte
|
||||||
if (!this->response_receiving_) {
|
if (start_byte != 0x31 && start_byte != 0x71 && start_byte != 0xF1) {
|
||||||
if (byte_ != 0x31 && byte_ != 0x71 && byte_ != 0xF1)
|
ESP_LOGW(TAG, "Invalid start byte: 0x%x", start_byte);
|
||||||
continue;
|
return false;
|
||||||
this->response_message_.clear();
|
|
||||||
this->response_receiving_ = true;
|
|
||||||
}
|
|
||||||
// Add current byte to message buffer
|
|
||||||
this->response_message_.push_back(byte_);
|
|
||||||
|
|
||||||
// 2. byte contains the payload size
|
|
||||||
if (this->response_message_.size() == 2) {
|
|
||||||
this->payload_length_ = byte_;
|
|
||||||
}
|
|
||||||
// 3. byte shall be 0x01 or 0x10
|
|
||||||
if (this->response_message_.size() == 3 && byte_ != 0x01 && byte_ != 0x10) {
|
|
||||||
this->response_receiving_ = false;
|
|
||||||
ESP_LOGW(TAG, "Invalid response message: 0x%s. Expected last byte to be 0x01 or 0x10",
|
|
||||||
PanasonicHelpers::byte_array_to_hex_string(this->response_message_, ',').c_str());
|
|
||||||
delay(10); // NOLINT
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// 4. byte shall be 0x01, 0x10 or 0x21
|
|
||||||
if (this->response_message_.size() == 4 && byte_ != 0x01 && byte_ != 0x10 && byte_ != 0x21) {
|
|
||||||
this->response_receiving_ = false;
|
|
||||||
ESP_LOGW(TAG, "Invalid response message: 0x%s. Expected last byte to be 0x01, 0x10 or 0x21",
|
|
||||||
PanasonicHelpers::byte_array_to_hex_string(this->response_message_, ',').c_str());
|
|
||||||
delay(10); // NOLINT
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if message is complete
|
// Prepare buffer for header reading.
|
||||||
if (this->response_message_.size() > 2 && this->response_message_.size() == this->payload_length_ + 3) {
|
// Header is 4 bytes long and first byte is already read.
|
||||||
this->response_receiving_ = false;
|
// Message may be up to 256 bytes long,
|
||||||
this->current_response_count_++;
|
// so reserve enough space to avoid dynamic resizing during reading.
|
||||||
if (this->log_uart_msg_)
|
buffer.clear();
|
||||||
PanasonicHelpers::log_uart_hex(UART_LOG_RX, this->response_message_, ',');
|
buffer.reserve(256);
|
||||||
}
|
buffer.resize(HEADER_SIZE);
|
||||||
|
buffer[0] = start_byte;
|
||||||
|
|
||||||
|
// Wait for header
|
||||||
|
if (uartComp->available() < HEADER_SIZE - 1)
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(5));
|
||||||
|
// Write header to buffer
|
||||||
|
auto succeed = uartComp->read_array(&buffer[1], HEADER_SIZE - 1);
|
||||||
|
|
||||||
|
// Verify header (start byte, message type and length)
|
||||||
|
if (!verify_message_header(buffer, succeed))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Calculate total message length
|
||||||
|
size_t total_expected = buffer[1] + 3;
|
||||||
|
size_t remaining = total_expected - buffer.size();
|
||||||
|
// Write rest of the message to buffer
|
||||||
|
while (remaining > 0) {
|
||||||
|
size_t current_size = buffer.size();
|
||||||
|
size_t to_read = std::min((size_t)8, remaining);
|
||||||
|
buffer.resize(current_size + to_read);
|
||||||
|
if (uartComp->available() < to_read)
|
||||||
|
vTaskDelay(pdMS_TO_TICKS(10));
|
||||||
|
if (!uartComp->read_array(&buffer[current_size], to_read)) {
|
||||||
|
ESP_LOGW(TAG, "Timeout while reading message body");
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
remaining -= to_read;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PanasonicHeatpumpComponent::send_request(RequestType requestType) {
|
// Verify checksum
|
||||||
switch (requestType) {
|
if (!verify_message_checksum(buffer)) {
|
||||||
case RequestType::COMMAND:
|
return false;
|
||||||
if (this->log_uart_msg_)
|
|
||||||
PanasonicHelpers::log_uart_hex(UART_LOG_TX, this->command_message_, ',');
|
|
||||||
this->write_array(this->command_message_);
|
|
||||||
this->flush();
|
|
||||||
break;
|
|
||||||
case RequestType::INITIAL:
|
|
||||||
// Probably not necessary but CZ-TAW1 sends this query on boot
|
|
||||||
if (this->log_uart_msg_)
|
|
||||||
PanasonicHelpers::log_uart_hex(UART_LOG_TX, PanasonicCommand::InitialRequest, INIT_REQUEST_SIZE, ',');
|
|
||||||
this->write_array(PanasonicCommand::InitialRequest, INIT_REQUEST_SIZE);
|
|
||||||
this->flush();
|
|
||||||
break;
|
|
||||||
case RequestType::POLLING:
|
|
||||||
if (this->log_uart_msg_)
|
|
||||||
PanasonicHelpers::log_uart_hex(UART_LOG_TX, PanasonicCommand::PollingMessage, DATA_MESSAGE_SIZE, ',');
|
|
||||||
this->write_array(PanasonicCommand::PollingMessage, DATA_MESSAGE_SIZE);
|
|
||||||
this->flush();
|
|
||||||
break;
|
|
||||||
case RequestType::POLLING_EXTRA:
|
|
||||||
if (this->log_uart_msg_)
|
|
||||||
PanasonicHelpers::log_uart_hex(UART_LOG_TX, PanasonicCommand::PollingExtraMessage, DATA_MESSAGE_SIZE, ',');
|
|
||||||
this->write_array(PanasonicCommand::PollingExtraMessage, DATA_MESSAGE_SIZE);
|
|
||||||
this->flush();
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (requestType != RequestType::NONE && requestType != RequestType::INITIAL) {
|
|
||||||
// Update last request time when request was sent
|
|
||||||
this->last_request_time_ = millis();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->next_request_ = RequestType::NONE;
|
// Message is complete
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PanasonicHeatpumpComponent::read_request() {
|
bool PanasonicHeatpumpComponent::verify_message_header(const std::vector<uint8_t>& message, bool reading_succeeded) {
|
||||||
if (this->uart_client_ == nullptr)
|
if (!reading_succeeded) {
|
||||||
return;
|
ESP_LOGW(TAG, "Timeout while reading message header");
|
||||||
|
return false;
|
||||||
while (this->uart_client_->available()) {
|
|
||||||
// Read each byte from client and forward it directly to the heatpump
|
|
||||||
this->uart_client_->read_byte(&byte_);
|
|
||||||
this->write_byte(byte_);
|
|
||||||
|
|
||||||
// Message shall start with 0x31, 0x71 or 0xF1, if not skip this byte
|
|
||||||
if (!this->request_receiving_) {
|
|
||||||
if (byte_ != 0x31 && byte_ != 0x71 && byte_ != 0xF1)
|
|
||||||
continue;
|
|
||||||
this->request_message_.clear();
|
|
||||||
this->request_receiving_ = true;
|
|
||||||
}
|
|
||||||
// Add current byte to message buffer
|
|
||||||
this->request_message_.push_back(byte_);
|
|
||||||
|
|
||||||
// 2. byte contains the payload size
|
|
||||||
if (this->request_message_.size() == 2) {
|
|
||||||
this->payload_length_ = byte_;
|
|
||||||
}
|
|
||||||
// 3. byte shall be 0x01 or 0x10
|
|
||||||
if (this->request_message_.size() == 3 && byte_ != 0x01 && byte_ != 0x10) {
|
|
||||||
this->request_receiving_ = false;
|
|
||||||
ESP_LOGW(TAG, "Invalid request message: 0x%s. Expected last byte to be 0x01 or 0x10",
|
|
||||||
PanasonicHelpers::byte_array_to_hex_string(this->request_message_, ',').c_str());
|
|
||||||
delay(10); // NOLINT
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// 4. byte shall be 0x01, 0x10 or 0x21
|
|
||||||
if (this->request_message_.size() == 4 && byte_ != 0x01 && byte_ != 0x10 && byte_ != 0x21) {
|
|
||||||
this->request_receiving_ = false;
|
|
||||||
ESP_LOGW(TAG, "Invalid request message: 0x%s. Expected last byte to be 0x01, 0x10 or 0x21",
|
|
||||||
PanasonicHelpers::byte_array_to_hex_string(this->request_message_, ',').c_str());
|
|
||||||
delay(10); // NOLINT
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if message is complete
|
if (message.size() < HEADER_SIZE) {
|
||||||
if (this->request_message_.size() > 2 && this->request_message_.size() == this->payload_length_ + 3) {
|
ESP_LOGW(TAG, "Message too short to contain valid header");
|
||||||
this->request_receiving_ = false;
|
return false;
|
||||||
if (this->log_uart_msg_)
|
|
||||||
PanasonicHelpers::log_uart_hex(UART_LOG_TX, this->request_message_, ',');
|
|
||||||
|
|
||||||
if (this->request_message_[0] != 0x31) {
|
|
||||||
// Update last request time when request is complete
|
|
||||||
this->last_request_time_ = millis();
|
|
||||||
this->uart_client_timeout_exceeded_ = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int PanasonicHeatpumpComponent::getResponseByte(const int index) {
|
if ((message[2] != 0x01 && message[2] != 0x10) || // 3. byte shall be 0x01 or 0x10
|
||||||
if (this->heatpump_default_message_.size() > index)
|
(message[3] != 0x01 && message[3] != 0x10 && message[3] != 0x21)) { // 4. byte shall be 0x01, 0x10 or 0x21
|
||||||
return this->heatpump_default_message_[index];
|
ESP_LOGW(TAG, "Invalid message header: 0x%s. Drop message.",
|
||||||
return -1;
|
PanasonicHelpers::byte_array_to_hex_string(message, ',').c_str());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int PanasonicHeatpumpComponent::getExtraResponseByte(const int index) {
|
return true;
|
||||||
if (this->heatpump_extra_message_.size() > index)
|
|
||||||
return this->heatpump_extra_message_[index];
|
|
||||||
return -1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ResponseType PanasonicHeatpumpComponent::check_response(const std::vector<uint8_t>& data) {
|
bool PanasonicHeatpumpComponent::verify_message_checksum(const std::vector<uint8_t>& message) {
|
||||||
|
uint8_t checksum = 0;
|
||||||
|
for (const auto b : message)
|
||||||
|
checksum += b;
|
||||||
|
|
||||||
|
// Last byte contains chechsum.
|
||||||
|
// Only if the sum of all bytes & 0xFF is 0, the message is valid.
|
||||||
|
if (checksum != 0) {
|
||||||
|
ESP_LOGW(TAG, "Invalid message checksum: 0x%02X. Last byte: 0x%02X", checksum, message.back());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResponseType PanasonicHeatpumpComponent::read_response() {
|
||||||
|
std::vector<uint8_t>* message{nullptr};
|
||||||
|
if (xQueueReceive(this->response_queue_handle_, &message, 0) != pdPASS || message == nullptr) {
|
||||||
|
return ResponseType::UNKNOWN;
|
||||||
|
}
|
||||||
|
PanasonicHelpers::write_uart_log(UART_LOG_RX, *message, ',', this->log_uart_msg_);
|
||||||
|
|
||||||
|
if (!this->check_response_length(*message)) {
|
||||||
|
delete message;
|
||||||
|
return ResponseType::UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get response type and save the response
|
||||||
|
auto responseType = ResponseType::UNKNOWN;
|
||||||
|
const uint8_t type = (*message)[3];
|
||||||
|
if (type == 0x10) {
|
||||||
|
responseType = ResponseType::STANDARD;
|
||||||
|
this->heatpump_default_message_ = std::move(*message);
|
||||||
|
|
||||||
|
// Is an extra request required?
|
||||||
|
if (this->heatpump_default_message_.size() > 199 && this->heatpump_default_message_[199] > 0x02) {
|
||||||
|
ESP_LOGD(TAG, "Queue extra polling request");
|
||||||
|
this->queue_request(build_message(PanasonicCommand::PollingExtraMessage));
|
||||||
|
}
|
||||||
|
} else if (type == 0x21) {
|
||||||
|
responseType = ResponseType::EXTRA;
|
||||||
|
this->heatpump_extra_message_ = std::move(*message);
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "Unknown response type in byte 3: 0x%02X", type);
|
||||||
|
responseType = ResponseType::UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete message;
|
||||||
|
return responseType;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PanasonicHeatpumpComponent::check_response_length(const std::vector<uint8_t>& message) {
|
||||||
// Read response message:
|
// Read response message:
|
||||||
// format: 0x71 [payload_length] 0x01 [0x10 || 0x21] [[TOP0 - TOP114] ...] 0x00 [checksum]
|
// format: 0x71 [payload_length] 0x01 [0x10 || 0x21] [[TOP0 - TOP114] ...] 0x00 [checksum]
|
||||||
// payload_length: payload_length + 3 = packet_length
|
// payload_length: payload_length + 3 = packet_length
|
||||||
// checksum: if (sum(all bytes) & 0xFF == 0) ==> valid packet
|
// checksum: if (sum(all bytes) & 0xFF == 0) ==> valid packet
|
||||||
|
if (message.size() == RESPONSE_MSG_SIZE)
|
||||||
|
return true;
|
||||||
|
|
||||||
if (data.empty())
|
ESP_LOGW(TAG, "Response message too short: received %u - expected %u", message.size(), RESPONSE_MSG_SIZE);
|
||||||
return ResponseType::UNKNOWN;
|
return false;
|
||||||
if (data[0] != 0x71)
|
|
||||||
return ResponseType::UNKNOWN;
|
|
||||||
if (this->response_receiving_)
|
|
||||||
return ResponseType::RECEIVING;
|
|
||||||
if (data.size() != RESPONSE_MSG_SIZE) {
|
|
||||||
ESP_LOGW(TAG, "Invalid response message length: recieved %d - expected %d", data.size(), RESPONSE_MSG_SIZE);
|
|
||||||
delay(10); // NOLINT
|
|
||||||
return ResponseType::UNKNOWN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify checksum
|
void PanasonicHeatpumpComponent::send_request() {
|
||||||
uint8_t checksum = 0;
|
if (millis() - request_send_time_ < REQUEST_SEND_INTERVAL) {
|
||||||
for (int i = 0; i < data.size(); i++) {
|
// wait until the interval is over
|
||||||
checksum += data[i];
|
return;
|
||||||
}
|
|
||||||
// all bytes (including checksum byte) shall be 0x00
|
|
||||||
if (checksum != 0) {
|
|
||||||
ESP_LOGW(TAG, "Invalid response message: checksum = 0x%02X, last_byte = 0x%02X", checksum, data[202]);
|
|
||||||
delay(10); // NOLINT
|
|
||||||
return ResponseType::UNKNOWN;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this->send_extra_request_ = data[3] == 0x10 && data[199] > 0x02 && this->send_extra_request_ == false ? true : false;
|
// Get message from queue
|
||||||
|
std::vector<uint8_t>* message{nullptr};
|
||||||
// Get response type and save the response
|
if (xQueueReceive(this->request_queue_handle_, &message, 0) != pdPASS || message == nullptr) {
|
||||||
auto responseType = ResponseType::UNKNOWN;
|
return; // nothing queued
|
||||||
if (data[3] == 0x10) {
|
|
||||||
responseType = ResponseType::STANDARD;
|
|
||||||
this->heatpump_default_message_ = data;
|
|
||||||
} else if (data[3] == 0x21) {
|
|
||||||
responseType = ResponseType::EXTRA;
|
|
||||||
this->heatpump_extra_message_ = data;
|
|
||||||
}
|
|
||||||
if (responseType == ResponseType::UNKNOWN) {
|
|
||||||
ESP_LOGW(TAG, "Unknown response type (4. byte): 0x%02X. Expected 0x10 or 0x21.", data[3]);
|
|
||||||
delay(10); // NOLINT
|
|
||||||
return responseType;
|
|
||||||
}
|
}
|
||||||
|
PanasonicHelpers::write_uart_log(UART_LOG_TX, *message, ',', this->log_uart_msg_);
|
||||||
|
|
||||||
// Check if the current response is a new response
|
// Send vector content over UART (robust API usage)
|
||||||
if (this->last_response_count_ == this->current_response_count_)
|
this->write_array(message->data(), message->size());
|
||||||
return ResponseType::UNKNOWN;
|
delete message;
|
||||||
this->last_response_count_ = this->current_response_count_;
|
request_send_time_ = millis();
|
||||||
|
|
||||||
return responseType;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void PanasonicHeatpumpComponent::set_command_high_nibble(const uint8_t value, const uint8_t index) {
|
void PanasonicHeatpumpComponent::set_command_high_nibble(const uint8_t value, const uint8_t index) {
|
||||||
if (this->next_request_ != RequestType::COMMAND) {
|
this->command_message_ = build_message(PanasonicCommand::CommandMessage);
|
||||||
// initialize the command
|
|
||||||
this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage),
|
|
||||||
std::end(PanasonicCommand::CommandMessage));
|
|
||||||
}
|
|
||||||
uint8_t lowNibble = this->heatpump_default_message_[index] & 0b1111;
|
uint8_t lowNibble = this->heatpump_default_message_[index] & 0b1111;
|
||||||
uint8_t highNibble = value << 4;
|
uint8_t highNibble = value << 4;
|
||||||
// set command byte
|
// set command byte
|
||||||
|
|
@ -360,16 +377,13 @@ void PanasonicHeatpumpComponent::set_command_high_nibble(const uint8_t value, co
|
||||||
this->command_message_.back() =
|
this->command_message_.back() =
|
||||||
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
|
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
|
||||||
|
|
||||||
// command will be send on next loop
|
ESP_LOGD(TAG, "Queue command request");
|
||||||
this->next_request_ = RequestType::COMMAND;
|
this->queue_request(this->command_message_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PanasonicHeatpumpComponent::set_command_low_nibble(const uint8_t value, const uint8_t index) {
|
void PanasonicHeatpumpComponent::set_command_low_nibble(const uint8_t value, const uint8_t index) {
|
||||||
if (this->next_request_ != RequestType::COMMAND) {
|
this->command_message_ = build_message(PanasonicCommand::CommandMessage);
|
||||||
// initialize the command
|
|
||||||
this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage),
|
|
||||||
std::end(PanasonicCommand::CommandMessage));
|
|
||||||
}
|
|
||||||
uint8_t highNibble = this->heatpump_default_message_[index] & 0b11110000;
|
uint8_t highNibble = this->heatpump_default_message_[index] & 0b11110000;
|
||||||
uint8_t lowNibble = value & 0b1111;
|
uint8_t lowNibble = value & 0b1111;
|
||||||
// set command byte
|
// set command byte
|
||||||
|
|
@ -378,32 +392,25 @@ void PanasonicHeatpumpComponent::set_command_low_nibble(const uint8_t value, con
|
||||||
this->command_message_.back() =
|
this->command_message_.back() =
|
||||||
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
|
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
|
||||||
|
|
||||||
// command will be send on next loop
|
ESP_LOGD(TAG, "Queue command request");
|
||||||
this->next_request_ = RequestType::COMMAND;
|
this->queue_request(this->command_message_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PanasonicHeatpumpComponent::set_command_byte(const uint8_t value, const uint8_t index) {
|
void PanasonicHeatpumpComponent::set_command_byte(const uint8_t value, const uint8_t index) {
|
||||||
if (this->next_request_ != RequestType::COMMAND) {
|
this->command_message_ = build_message(PanasonicCommand::CommandMessage);
|
||||||
// initialize the command
|
|
||||||
this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage),
|
|
||||||
std::end(PanasonicCommand::CommandMessage));
|
|
||||||
}
|
|
||||||
// set command byte
|
// set command byte
|
||||||
this->command_message_[index] = value;
|
this->command_message_[index] = value;
|
||||||
// calculate and set set checksum (last element)
|
// calculate and set set checksum (last element)
|
||||||
this->command_message_.back() =
|
this->command_message_.back() =
|
||||||
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
|
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
|
||||||
|
|
||||||
// command will be send on next loop
|
ESP_LOGD(TAG, "Queue command request");
|
||||||
this->next_request_ = RequestType::COMMAND;
|
this->queue_request(this->command_message_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void PanasonicHeatpumpComponent::set_command_curve(const uint8_t value, const uint8_t index) {
|
void PanasonicHeatpumpComponent::set_command_curve(const uint8_t value, const uint8_t index) {
|
||||||
if (this->next_request_ != RequestType::COMMAND) {
|
this->command_message_ = build_message(PanasonicCommand::CommandMessage);
|
||||||
// initialize the command
|
|
||||||
this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage),
|
|
||||||
std::end(PanasonicCommand::CommandMessage));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set zone 1 curve bytes
|
// Set zone 1 curve bytes
|
||||||
if (index == 75 || index == 76 || index == 77 || index == 78 || index == 86 || index == 87 || index == 88 ||
|
if (index == 75 || index == 76 || index == 77 || index == 78 || index == 86 || index == 87 || index == 88 ||
|
||||||
|
|
@ -436,8 +443,32 @@ void PanasonicHeatpumpComponent::set_command_curve(const uint8_t value, const ui
|
||||||
this->command_message_.back() =
|
this->command_message_.back() =
|
||||||
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
|
PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1);
|
||||||
|
|
||||||
// command will be send on next loop
|
ESP_LOGD(TAG, "Queue command request");
|
||||||
this->next_request_ = RequestType::COMMAND;
|
this->queue_request(this->command_message_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PanasonicHeatpumpComponent::queue_request(const std::vector<uint8_t>& message) {
|
||||||
|
auto* cmd = new std::vector<uint8_t>(message);
|
||||||
|
|
||||||
|
// Check request_queue_handle_, function is called before setup() initializes it!
|
||||||
|
if (this->request_queue_handle_ == nullptr || xQueueSend(this->request_queue_handle_, &cmd, 0) != pdPASS) {
|
||||||
|
ESP_LOGW(TAG, "Request queue full or unavailable, dropping message");
|
||||||
|
delete cmd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function can be used in esphome lambda to get a specific byte
|
||||||
|
int PanasonicHeatpumpComponent::get_response_byte(const int index) {
|
||||||
|
if (this->heatpump_default_message_.size() > index)
|
||||||
|
return this->heatpump_default_message_[index];
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function can be used in esphome lambda to get a specific byte
|
||||||
|
int PanasonicHeatpumpComponent::get_extra_response_byte(const int index) {
|
||||||
|
if (this->heatpump_extra_message_.size() > index)
|
||||||
|
return this->heatpump_extra_message_[index];
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
} // namespace panasonic_heatpump
|
} // namespace panasonic_heatpump
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ namespace esphome {
|
||||||
namespace panasonic_heatpump {
|
namespace panasonic_heatpump {
|
||||||
enum LoopState : uint8_t {
|
enum LoopState : uint8_t {
|
||||||
READ_RESPONSE,
|
READ_RESPONSE,
|
||||||
CHECK_RESPONSE,
|
|
||||||
PUBLISH_SENSOR,
|
PUBLISH_SENSOR,
|
||||||
PUBLISH_BINARY_SENSOR,
|
PUBLISH_BINARY_SENSOR,
|
||||||
PUBLISH_TEXT_SENSOR,
|
PUBLISH_TEXT_SENSOR,
|
||||||
|
|
@ -34,7 +33,6 @@ enum LoopState : uint8_t {
|
||||||
PUBLISH_WATER_HEATER,
|
PUBLISH_WATER_HEATER,
|
||||||
PUBLISH_EXTRA_SENSOR,
|
PUBLISH_EXTRA_SENSOR,
|
||||||
SEND_REQUEST,
|
SEND_REQUEST,
|
||||||
READ_REQUEST,
|
|
||||||
RESTART_LOOP,
|
RESTART_LOOP,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -48,7 +46,6 @@ enum RequestType : uint8_t {
|
||||||
|
|
||||||
enum ResponseType : uint8_t {
|
enum ResponseType : uint8_t {
|
||||||
UNKNOWN,
|
UNKNOWN,
|
||||||
RECEIVING,
|
|
||||||
STANDARD,
|
STANDARD,
|
||||||
EXTRA,
|
EXTRA,
|
||||||
};
|
};
|
||||||
|
|
@ -86,9 +83,9 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev
|
||||||
void set_log_uart_msg(bool active) {
|
void set_log_uart_msg(bool active) {
|
||||||
this->log_uart_msg_ = active;
|
this->log_uart_msg_ = active;
|
||||||
}
|
}
|
||||||
// uart message variables to use in lambda functions
|
// functions to use in esphome lambda
|
||||||
int getResponseByte(const int index);
|
int get_response_byte(const int index);
|
||||||
int getExtraResponseByte(const int index);
|
int get_extra_response_byte(const int index);
|
||||||
// command functions
|
// command functions
|
||||||
void set_command_high_nibble(const uint8_t value, const uint8_t index);
|
void set_command_high_nibble(const uint8_t value, const uint8_t index);
|
||||||
void set_command_low_nibble(const uint8_t value, const uint8_t index);
|
void set_command_low_nibble(const uint8_t value, const uint8_t index);
|
||||||
|
|
@ -128,27 +125,25 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
// options variables
|
// options variables
|
||||||
uart::UARTComponent* uart_client_{nullptr};
|
|
||||||
bool log_uart_msg_{false};
|
bool log_uart_msg_{false};
|
||||||
uint32_t last_request_time_{0};
|
uint32_t last_client_request_time_{0};
|
||||||
uint32_t uart_client_timeout_{10000};
|
uint32_t uart_client_timeout_{10000}; // 10 sec
|
||||||
// uart message variables
|
uint32_t request_send_time_{5000}; // 5 sec --> default is 5 sec so first request is not sent too fast after startup
|
||||||
|
static const uint32_t REQUEST_SEND_INTERVAL{250}; // 250 ms
|
||||||
|
static const size_t HEADER_SIZE = 4;
|
||||||
|
|
||||||
|
// uart message variables, process in main loop
|
||||||
|
TaskHandle_t uart_task_handle_{nullptr};
|
||||||
|
TaskHandle_t uart_client_task_handle_{nullptr};
|
||||||
|
QueueHandle_t response_queue_handle_{nullptr};
|
||||||
|
QueueHandle_t request_queue_handle_{nullptr};
|
||||||
|
uart::UARTComponent* uart_client_{nullptr};
|
||||||
std::vector<uint8_t> heatpump_default_message_;
|
std::vector<uint8_t> heatpump_default_message_;
|
||||||
std::vector<uint8_t> heatpump_extra_message_;
|
std::vector<uint8_t> heatpump_extra_message_;
|
||||||
std::vector<uint8_t> response_message_;
|
|
||||||
std::vector<uint8_t> request_message_;
|
|
||||||
std::vector<uint8_t> command_message_;
|
std::vector<uint8_t> command_message_;
|
||||||
uint8_t payload_length_;
|
|
||||||
uint8_t byte_;
|
|
||||||
uint8_t current_response_count_{0};
|
|
||||||
uint8_t last_response_count_{0};
|
|
||||||
bool response_receiving_{false};
|
|
||||||
bool request_receiving_{false};
|
|
||||||
bool send_extra_request_{false};
|
|
||||||
bool uart_client_timeout_exceeded_{false};
|
bool uart_client_timeout_exceeded_{false};
|
||||||
LoopState loop_state_{LoopState::RESTART_LOOP};
|
LoopState loop_state_{LoopState::RESTART_LOOP};
|
||||||
RequestType next_request_{RequestType::INITIAL};
|
|
||||||
ResponseType current_response_{ResponseType::UNKNOWN};
|
|
||||||
// entity vectors
|
// entity vectors
|
||||||
std::vector<PanasonicHeatpumpEntity*> binary_sensors_;
|
std::vector<PanasonicHeatpumpEntity*> binary_sensors_;
|
||||||
std::vector<PanasonicHeatpumpEntity*> climates_;
|
std::vector<PanasonicHeatpumpEntity*> climates_;
|
||||||
|
|
@ -161,10 +156,21 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev
|
||||||
std::vector<PanasonicHeatpumpEntity*> extra_sensors_;
|
std::vector<PanasonicHeatpumpEntity*> extra_sensors_;
|
||||||
|
|
||||||
// uart message functions
|
// uart message functions
|
||||||
void read_response();
|
static void uart_task(void* pvParameters);
|
||||||
void send_request(RequestType requestType);
|
static void uart_client_task(void* pvParameters);
|
||||||
void read_request();
|
bool receive_from_uart(uart::UARTComponent* src, std::vector<uint8_t>& buffer);
|
||||||
ResponseType check_response(const std::vector<uint8_t>& data);
|
|
||||||
|
void send_request();
|
||||||
|
void queue_request(const std::vector<uint8_t>& message);
|
||||||
|
ResponseType read_response();
|
||||||
|
static bool check_response_length(const std::vector<uint8_t>& message);
|
||||||
|
static bool verify_message_header(const std::vector<uint8_t>& message, bool reading_succeeded);
|
||||||
|
static bool verify_message_checksum(const std::vector<uint8_t>& message);
|
||||||
|
|
||||||
|
template <size_t N>
|
||||||
|
static std::vector<uint8_t> build_message(const uint8_t (&msg)[N]) {
|
||||||
|
return std::vector<uint8_t>(msg, msg + N);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
} // namespace panasonic_heatpump
|
} // namespace panasonic_heatpump
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ water_heater::WaterHeaterTraits PanasonicHeatpumpWaterHeater::traits() {
|
||||||
|
|
||||||
void PanasonicHeatpumpWaterHeater::control(const water_heater::WaterHeaterCall& call) {
|
void PanasonicHeatpumpWaterHeater::control(const water_heater::WaterHeaterCall& call) {
|
||||||
if (call.get_mode().has_value()) {
|
if (call.get_mode().has_value()) {
|
||||||
int byte6 = this->parent_->getResponseByte(6);
|
int byte6 = this->parent_->get_response_byte(6);
|
||||||
if (byte6 >= 0) {
|
if (byte6 >= 0) {
|
||||||
water_heater::WaterHeaterMode new_mode = *call.get_mode();
|
water_heater::WaterHeaterMode new_mode = *call.get_mode();
|
||||||
uint8_t newByte6 = this->setWaterHeaterMode(new_mode, (uint8_t)byte6);
|
uint8_t newByte6 = this->setWaterHeaterMode(new_mode, (uint8_t)byte6);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ tests/
|
||||||
│ ├── README.md
|
│ ├── README.md
|
||||||
│ ├── test_panasonic_heatpump_unit.py # Unit tests
|
│ ├── test_panasonic_heatpump_unit.py # Unit tests
|
||||||
│ ├── test_panasonic_heatpump_integration.py # Integration tests
|
│ ├── test_panasonic_heatpump_integration.py # Integration tests
|
||||||
│ ├── test_panasonic_heatpump_esp8266.yaml # ESP8266 test config
|
|
||||||
│ ├── test_panasonic_heatpump_esp32s2.yaml # ESP32-S2 test config
|
│ ├── test_panasonic_heatpump_esp32s2.yaml # ESP32-S2 test config
|
||||||
│ ├── test_panasonic_heatpump_esp32c3.yaml # ESP32-C3 test config
|
│ ├── test_panasonic_heatpump_esp32c3.yaml # ESP32-C3 test config
|
||||||
│ ├── test_panasonic_heatpump_cztaw1.yaml # CZ-TAW1 test config
|
│ ├── test_panasonic_heatpump_cztaw1.yaml # CZ-TAW1 test config
|
||||||
|
|
|
||||||
|
|
@ -14,13 +14,11 @@ This directory contains comprehensive tests for the `panasonic_heatpump` ESPHome
|
||||||
### Integration Tests (`test_panasonic_heatpump_integration.py`)
|
### Integration Tests (`test_panasonic_heatpump_integration.py`)
|
||||||
- Tests minimal feature configuration on ESP32 board
|
- Tests minimal feature configuration on ESP32 board
|
||||||
- Tests full feature configuration on ESP32 board
|
- Tests full feature configuration on ESP32 board
|
||||||
- Tests configuration on ESP8266 board
|
|
||||||
- Tests configuration on ESP32-S2 board
|
- Tests configuration on ESP32-S2 board
|
||||||
- Tests configuration on ESP32-C3 board
|
- Tests configuration on ESP32-C3 board
|
||||||
- Tests configuration with CZ-TAW1 (UART-proxy)
|
- Tests configuration with CZ-TAW1 (UART-proxy)
|
||||||
|
|
||||||
### Test Configuration Files
|
### Test Configuration Files
|
||||||
- `test_panasonic_heatpump_esp8266.yaml` - ESP8266 Wemos D1 Mini
|
|
||||||
- `test_panasonic_heatpump_esp32s2.yaml` - ESP32-S2 Wemos S2 Mini
|
- `test_panasonic_heatpump_esp32s2.yaml` - ESP32-S2 Wemos S2 Mini
|
||||||
- `test_panasonic_heatpump_esp32c3.yaml` - ESP32-C3 mini (RISC-V)
|
- `test_panasonic_heatpump_esp32c3.yaml` - ESP32-C3 mini (RISC-V)
|
||||||
- `test_panasonic_heatpump_cztaw1.yaml` - UART proxy setup for CZ-TAW1 client support
|
- `test_panasonic_heatpump_cztaw1.yaml` - UART proxy setup for CZ-TAW1 client support
|
||||||
|
|
@ -68,7 +66,7 @@ The GitHub Actions workflow (`.github/workflows/test_panasonic_heatpump.yml`) in
|
||||||
### Jobs
|
### Jobs
|
||||||
|
|
||||||
1. **test-build**: Validates and compiles the component
|
1. **test-build**: Validates and compiles the component
|
||||||
- Tests on multiple board types: ESP8266, ESP32 (full), ESP32-S2, ESP32-C3 and with CZ-TAW1 (UART-proxy)
|
- Tests on multiple board types: ESP32 (full), ESP32-S2, ESP32-C3 and with CZ-TAW1 (UART-proxy)
|
||||||
- Uses esphome/build-action for firmware compilation
|
- Uses esphome/build-action for firmware compilation
|
||||||
|
|
||||||
2. **lint-code**: Runs clang-format on C++ code
|
2. **lint-code**: Runs clang-format on C++ code
|
||||||
|
|
@ -196,7 +194,7 @@ All tests must pass before merging to main branch. The GitHub Actions workflow e
|
||||||
- Code follows formatting standards
|
- Code follows formatting standards
|
||||||
- All unit and integration tests pass
|
- All unit and integration tests pass
|
||||||
- Latest ESPHome versions is supported
|
- Latest ESPHome versions is supported
|
||||||
- ESP8266 and ESP32 boards are supported
|
- Only ESP32 boards are supported
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
---
|
|
||||||
# Test configuration for panasonic_heatpump component on ESP8266
|
|
||||||
# ESP8266 has limited GPIO pins and only one UART
|
|
||||||
|
|
||||||
esp8266:
|
|
||||||
board: d1_mini
|
|
||||||
|
|
||||||
esphome:
|
|
||||||
name: test-panasonic-heatpump-esp8266
|
|
||||||
friendly_name: "Test Panasonic Heatpump ESP8266"
|
|
||||||
|
|
||||||
logger:
|
|
||||||
level: DEBUG
|
|
||||||
|
|
||||||
external_components:
|
|
||||||
- source:
|
|
||||||
type: local
|
|
||||||
path: ../../components
|
|
||||||
components: [panasonic_heatpump]
|
|
||||||
|
|
||||||
uart:
|
|
||||||
- id: uart_heatpump
|
|
||||||
tx_pin: GPIO13
|
|
||||||
rx_pin: GPIO15
|
|
||||||
baud_rate: 9600
|
|
||||||
data_bits: 8
|
|
||||||
parity: EVEN
|
|
||||||
stop_bits: 1
|
|
||||||
|
|
||||||
# Test basic configuration
|
|
||||||
panasonic_heatpump:
|
|
||||||
id: my_heatpump
|
|
||||||
uart_id: uart_heatpump
|
|
||||||
|
|
||||||
# Test sensor types - first item only
|
|
||||||
sensor:
|
|
||||||
- platform: panasonic_heatpump
|
|
||||||
top1:
|
|
||||||
name: "Pump Flow"
|
|
||||||
|
|
||||||
# Test binary sensors - first item only
|
|
||||||
binary_sensor:
|
|
||||||
- platform: panasonic_heatpump
|
|
||||||
top0:
|
|
||||||
name: "Heatpump State"
|
|
||||||
|
|
||||||
# Test text sensors - first item only
|
|
||||||
text_sensor:
|
|
||||||
- platform: panasonic_heatpump
|
|
||||||
top4:
|
|
||||||
name: "Operating Mode State"
|
|
||||||
|
|
||||||
# Test number controls - first item only
|
|
||||||
number:
|
|
||||||
- platform: panasonic_heatpump
|
|
||||||
set5:
|
|
||||||
name: "Set Z1 Heat Request Temperature"
|
|
||||||
|
|
||||||
# Test select controls - first item only
|
|
||||||
select:
|
|
||||||
- platform: panasonic_heatpump
|
|
||||||
set2:
|
|
||||||
name: "Set Holiday Mode"
|
|
||||||
|
|
||||||
# Test switch controls - first item only
|
|
||||||
switch:
|
|
||||||
- platform: panasonic_heatpump
|
|
||||||
set1:
|
|
||||||
name: "Set Heatpump"
|
|
||||||
|
|
||||||
# Test climate controls - first item only
|
|
||||||
climate:
|
|
||||||
- platform: panasonic_heatpump
|
|
||||||
zone1:
|
|
||||||
name: "Zone 1"
|
|
||||||
|
|
||||||
# Test water_heater controls - first item only
|
|
||||||
water_heater:
|
|
||||||
- platform: panasonic_heatpump
|
|
||||||
tank:
|
|
||||||
name: "DHW"
|
|
||||||
|
|
@ -12,17 +12,6 @@ import os
|
||||||
class TestPanasonicHeatpumpIntegration:
|
class TestPanasonicHeatpumpIntegration:
|
||||||
"""Integration tests using ESPHome CLI."""
|
"""Integration tests using ESPHome CLI."""
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def test_yaml_esp8266(self):
|
|
||||||
"""Get the path to the ESP8266 test YAML file."""
|
|
||||||
base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
|
|
||||||
return os.path.join(
|
|
||||||
base_dir,
|
|
||||||
"tests",
|
|
||||||
"panasonic_heatpump",
|
|
||||||
"test_panasonic_heatpump_esp8266.yaml",
|
|
||||||
)
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def test_yaml_esp32(self):
|
def test_yaml_esp32(self):
|
||||||
"""Get the path to the ESP32 test YAML file."""
|
"""Get the path to the ESP32 test YAML file."""
|
||||||
|
|
@ -64,26 +53,6 @@ class TestPanasonicHeatpumpIntegration:
|
||||||
"test_panasonic_heatpump_cztaw1.yaml",
|
"test_panasonic_heatpump_cztaw1.yaml",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_validate_esp8266_config(self, test_yaml_esp8266: str):
|
|
||||||
"""Test that the ESP8266 configuration is valid."""
|
|
||||||
if not os.path.exists(test_yaml_esp8266):
|
|
||||||
pytest.skip(f"Test file not found: {test_yaml_esp8266}")
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
["esphome", "config", test_yaml_esp8266],
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
timeout=120,
|
|
||||||
)
|
|
||||||
assert (
|
|
||||||
result.returncode == 0
|
|
||||||
), f"ESP8266 config validation failed: {result.stderr}"
|
|
||||||
except FileNotFoundError:
|
|
||||||
pytest.skip("ESPHome not installed")
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
pytest.fail("ESPHome config validation timed out")
|
|
||||||
|
|
||||||
def test_validate_esp32_config(self, test_yaml_esp32: str):
|
def test_validate_esp32_config(self, test_yaml_esp32: str):
|
||||||
"""Test that the ESP32 configuration is valid."""
|
"""Test that the ESP32 configuration is valid."""
|
||||||
if not os.path.exists(test_yaml_esp32):
|
if not os.path.exists(test_yaml_esp32):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue