diff --git a/.github/workflows/test_panasonic_heatpump.yml b/.github/workflows/test_panasonic_heatpump.yml index e77a641..8fba2d2 100644 --- a/.github/workflows/test_panasonic_heatpump.yml +++ b/.github/workflows/test_panasonic_heatpump.yml @@ -26,7 +26,7 @@ jobs: strategy: fail-fast: false matrix: - board: [esp8266, esp32s2, esp32c3, full] + board: [esp32s2, esp32c3, full] steps: - name: Checkout repository uses: actions/checkout@v4 diff --git a/components/maidesite_desk/README.md b/components/maidesite_desk/README.md index 43dc69f..573c22d 100644 --- a/components/maidesite_desk/README.md +++ b/components/maidesite_desk/README.md @@ -6,7 +6,7 @@ ### 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) ### Wiring diff --git a/components/panasonic_heatpump/README.md b/components/panasonic_heatpump/README.md index 1bfe72e..8056e0b 100644 --- a/components/panasonic_heatpump/README.md +++ b/components/panasonic_heatpump/README.md @@ -4,13 +4,28 @@ ### 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 (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: [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_adum1201.png](../../prototypes/panasonic_heatpump/wiring_adum1201.png) @@ -644,7 +659,7 @@ water_heater: ## 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). 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. @@ -664,7 +679,7 @@ sensor: unit_of_measurement: °C lambda: |- // 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 // do not update if the byte is invalid if (byte < 0) return {}; @@ -678,7 +693,7 @@ text_sensor: update_interval: 3s lambda: |- // 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 // do not update if the byte is invalid if (byte < 0) return {}; @@ -703,5 +718,5 @@ After a power on the heatpump should respond to the requests. ## 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! diff --git a/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp b/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp index ec219b2..a12ead1 100644 --- a/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp +++ b/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp @@ -31,7 +31,7 @@ climate::ClimateTraits PanasonicHeatpumpClimate::traits() { void PanasonicHeatpumpClimate::control(const climate::ClimateCall& call) { if (call.get_mode().has_value()) { - int byte6 = this->parent_->getResponseByte(6); + int byte6 = this->parent_->get_response_byte(6); if (byte6 >= 0) { climate::ClimateMode new_mode = *call.get_mode(); uint8_t newByte6 = this->setClimateMode(new_mode, (uint8_t)byte6); diff --git a/components/panasonic_heatpump/helpers.cpp b/components/panasonic_heatpump/helpers.cpp index 5546ea0..691ba70 100644 --- a/components/panasonic_heatpump/helpers.cpp +++ b/components/panasonic_heatpump/helpers.cpp @@ -4,13 +4,13 @@ namespace esphome { namespace panasonic_heatpump { static const char* const TAG = "panasonic_heatpump"; -void PanasonicHelpers::log_uart_hex(UartLogDirection direction, const std::vector& data, - const char separator) { - PanasonicHelpers::log_uart_hex(direction, &data[0], data.size(), separator); +void PanasonicHelpers::write_uart_log(UartLogDirection direction, const std::vector& data, + const char separator, bool logBytes) { + 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, - const char separator) { +void PanasonicHelpers::write_uart_log(UartLogDirection direction, const uint8_t* data, const size_t length, + const char separator, bool logBytes) { std::string logStr = ""; std::string msgDir = direction == UART_LOG_TX ? ">>>" : "<<<"; 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); delay(10); + if (!logBytes) + return; + logStr += byte_array_to_hex_string(data, length, separator); // Log in chunks to avoid ESP_LOG buffer overflow (https://developers.esphome.io/architecture/logging/). diff --git a/components/panasonic_heatpump/helpers.h b/components/panasonic_heatpump/helpers.h index af71f9c..6eeb756 100644 --- a/components/panasonic_heatpump/helpers.h +++ b/components/panasonic_heatpump/helpers.h @@ -18,8 +18,10 @@ enum UartLogDirection : uint8_t { class PanasonicHelpers { public: - static void log_uart_hex(UartLogDirection direction, const std::vector& data, const char separator); - static void log_uart_hex(UartLogDirection direction, const uint8_t* data, const size_t length, const char separator); + static void write_uart_log(UartLogDirection direction, const std::vector& data, 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& data, const char separator); static std::string byte_array_to_hex_string(const uint8_t* data, const size_t length, const char separator); }; diff --git a/components/panasonic_heatpump/panasonic_heatpump.cpp b/components/panasonic_heatpump/panasonic_heatpump.cpp index 3f10afa..2376197 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.cpp +++ b/components/panasonic_heatpump/panasonic_heatpump.cpp @@ -25,51 +25,75 @@ void PanasonicHeatpumpComponent::dump_config() { void PanasonicHeatpumpComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Panasonic Heatpump ..."); - delay(10); // NOLINT this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_EVEN, 8); - this->update(); + this->response_queue_handle_ = xQueueCreate(8, sizeof(std::vector*)); + 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*)); + 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() { - 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; - this->next_request_ = this->send_extra_request_ ? RequestType::POLLING_EXTRA : RequestType::POLLING; -} -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; + // 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() { switch (this->loop_state_) { - case LoopState::READ_RESPONSE: - 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 LoopState::READ_RESPONSE: { + switch (this->read_response()) { case ResponseType::STANDARD: this->loop_state_ = LoopState::PUBLISH_SENSOR; break; case ResponseType::EXTRA: this->loop_state_ = LoopState::PUBLISH_EXTRA_SENSOR; break; + default: + this->loop_state_ = LoopState::SEND_REQUEST; + break; }; break; + } case LoopState::PUBLISH_SENSOR: for (auto* entity : this->sensors_) { entity->publish_new_state(this->heatpump_default_message_); @@ -125,233 +149,226 @@ void PanasonicHeatpumpComponent::loop() { this->loop_state_ = LoopState::SEND_REQUEST; break; case LoopState::SEND_REQUEST: - this->send_request(this->next_request_); - this->loop_state_ = LoopState::READ_REQUEST; - break; - case LoopState::READ_REQUEST: - this->read_request(); - this->loop_state_ = LoopState::RESTART_LOOP; - break; + this->send_request(); + // fallthrough default: this->loop_state_ = LoopState::READ_RESPONSE; break; }; } -void PanasonicHeatpumpComponent::read_response() { - while (this->available()) { - // Read each byte from heatpump and forward it directly to the client (CZ-TAW1) - this->read_byte(&byte_); - if (this->uart_client_ != nullptr) { - this->uart_client_->write_byte(byte_); - } - - // Message shall start with 0x31, 0x71 or 0xF1, if not skip this byte - if (!this->response_receiving_) { - if (byte_ != 0x31 && byte_ != 0x71 && byte_ != 0xF1) - continue; - 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 - if (this->response_message_.size() > 2 && this->response_message_.size() == this->payload_length_ + 3) { - this->response_receiving_ = false; - this->current_response_count_++; - if (this->log_uart_msg_) - PanasonicHelpers::log_uart_hex(UART_LOG_RX, this->response_message_, ','); +void PanasonicHeatpumpComponent::uart_task(void* pvParameters) { + auto* self = static_cast(pvParameters); + std::vector rx_buffer; + rx_buffer.reserve(256); + + 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(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::send_request(RequestType requestType) { - switch (requestType) { - case RequestType::COMMAND: - 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(); +void PanasonicHeatpumpComponent::uart_client_task(void* pvParameters) { + auto* self = static_cast(pvParameters); + std::vector 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(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)); + } } - - this->next_request_ = RequestType::NONE; } -void PanasonicHeatpumpComponent::read_request() { - if (this->uart_client_ == nullptr) - return; +// Used for both uart interfaces +bool PanasonicHeatpumpComponent::receive_from_uart(uart::UARTComponent* uartComp, std::vector& buffer) { + uint8_t start_byte; - 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_); + // Wait for the start byte to be available + while (!uartComp->available()) + vTaskDelay(pdMS_TO_TICKS(5)); - // 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; - } + // Read the first byte + if (!uartComp->read_byte(&start_byte)) + return false; - // Check if message is complete - if (this->request_message_.size() > 2 && this->request_message_.size() == this->payload_length_ + 3) { - this->request_receiving_ = false; - if (this->log_uart_msg_) - PanasonicHelpers::log_uart_hex(UART_LOG_TX, this->request_message_, ','); + // Message shall start with 0x31, 0x71 or 0xF1, if not skip this byte + if (start_byte != 0x31 && start_byte != 0x71 && start_byte != 0xF1) { + ESP_LOGW(TAG, "Invalid start byte: 0x%x", start_byte); + return false; + } - 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; - } + // Prepare buffer for header reading. + // Header is 4 bytes long and first byte is already read. + // Message may be up to 256 bytes long, + // so reserve enough space to avoid dynamic resizing during reading. + buffer.clear(); + 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; } -} -int PanasonicHeatpumpComponent::getResponseByte(const int index) { - if (this->heatpump_default_message_.size() > index) - return this->heatpump_default_message_[index]; - return -1; -} + // Verify checksum + if (!verify_message_checksum(buffer)) { + return false; + } -int PanasonicHeatpumpComponent::getExtraResponseByte(const int index) { - if (this->heatpump_extra_message_.size() > index) - return this->heatpump_extra_message_[index]; - return -1; + // Message is complete + return true; } -ResponseType PanasonicHeatpumpComponent::check_response(const std::vector& data) { - // Read response message: - // format: 0x71 [payload_length] 0x01 [0x10 || 0x21] [[TOP0 - TOP114] ...] 0x00 [checksum] - // payload_length: payload_length + 3 = packet_length - // checksum: if (sum(all bytes) & 0xFF == 0) ==> valid packet +bool PanasonicHeatpumpComponent::verify_message_header(const std::vector& message, bool reading_succeeded) { + if (!reading_succeeded) { + ESP_LOGW(TAG, "Timeout while reading message header"); + return false; + } - if (data.empty()) - return ResponseType::UNKNOWN; - 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; + if (message.size() < HEADER_SIZE) { + ESP_LOGW(TAG, "Message too short to contain valid header"); + return false; } - // Verify checksum - uint8_t checksum = 0; - for (int i = 0; i < data.size(); i++) { - checksum += data[i]; + if ((message[2] != 0x01 && message[2] != 0x10) || // 3. byte shall be 0x01 or 0x10 + (message[3] != 0x01 && message[3] != 0x10 && message[3] != 0x21)) { // 4. byte shall be 0x01, 0x10 or 0x21 + ESP_LOGW(TAG, "Invalid message header: 0x%s. Drop message.", + PanasonicHelpers::byte_array_to_hex_string(message, ',').c_str()); + return false; } - // all bytes (including checksum byte) shall be 0x00 + + return true; +} + +bool PanasonicHeatpumpComponent::verify_message_checksum(const std::vector& 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 response message: checksum = 0x%02X, last_byte = 0x%02X", checksum, data[202]); - delay(10); // NOLINT + 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* 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_); - this->send_extra_request_ = data[3] == 0x10 && data[199] > 0x02 && this->send_extra_request_ == false ? true : false; + if (!this->check_response_length(*message)) { + delete message; + return ResponseType::UNKNOWN; + } // Get response type and save the response auto responseType = ResponseType::UNKNOWN; - if (data[3] == 0x10) { + const uint8_t type = (*message)[3]; + if (type == 0x10) { responseType = ResponseType::STANDARD; - this->heatpump_default_message_ = data; - } else if (data[3] == 0x21) { + 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_ = data; + this->heatpump_extra_message_ = std::move(*message); + } else { + ESP_LOGW(TAG, "Unknown response type in byte 3: 0x%02X", type); + responseType = ResponseType::UNKNOWN; } - 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; + + delete message; + return responseType; +} + +bool PanasonicHeatpumpComponent::check_response_length(const std::vector& message) { + // Read response message: + // format: 0x71 [payload_length] 0x01 [0x10 || 0x21] [[TOP0 - TOP114] ...] 0x00 [checksum] + // payload_length: payload_length + 3 = packet_length + // checksum: if (sum(all bytes) & 0xFF == 0) ==> valid packet + if (message.size() == RESPONSE_MSG_SIZE) + return true; + + ESP_LOGW(TAG, "Response message too short: received %u - expected %u", message.size(), RESPONSE_MSG_SIZE); + return false; +} + +void PanasonicHeatpumpComponent::send_request() { + if (millis() - request_send_time_ < REQUEST_SEND_INTERVAL) { + // wait until the interval is over + return; } - // Check if the current response is a new response - if (this->last_response_count_ == this->current_response_count_) - return ResponseType::UNKNOWN; - this->last_response_count_ = this->current_response_count_; + // Get message from queue + std::vector* message{nullptr}; + if (xQueueReceive(this->request_queue_handle_, &message, 0) != pdPASS || message == nullptr) { + return; // nothing queued + } + PanasonicHelpers::write_uart_log(UART_LOG_TX, *message, ',', this->log_uart_msg_); - return responseType; + // Send vector content over UART (robust API usage) + this->write_array(message->data(), message->size()); + delete message; + request_send_time_ = millis(); } void PanasonicHeatpumpComponent::set_command_high_nibble(const uint8_t value, const uint8_t index) { - if (this->next_request_ != RequestType::COMMAND) { - // initialize the command - this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage), - std::end(PanasonicCommand::CommandMessage)); - } + this->command_message_ = build_message(PanasonicCommand::CommandMessage); + uint8_t lowNibble = this->heatpump_default_message_[index] & 0b1111; uint8_t highNibble = value << 4; // set command byte @@ -360,16 +377,13 @@ void PanasonicHeatpumpComponent::set_command_high_nibble(const uint8_t value, co this->command_message_.back() = PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1); - // command will be send on next loop - this->next_request_ = RequestType::COMMAND; + ESP_LOGD(TAG, "Queue command request"); + this->queue_request(this->command_message_); } void PanasonicHeatpumpComponent::set_command_low_nibble(const uint8_t value, const uint8_t index) { - if (this->next_request_ != RequestType::COMMAND) { - // initialize the command - this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage), - std::end(PanasonicCommand::CommandMessage)); - } + this->command_message_ = build_message(PanasonicCommand::CommandMessage); + uint8_t highNibble = this->heatpump_default_message_[index] & 0b11110000; uint8_t lowNibble = value & 0b1111; // set command byte @@ -378,32 +392,25 @@ void PanasonicHeatpumpComponent::set_command_low_nibble(const uint8_t value, con this->command_message_.back() = PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1); - // command will be send on next loop - this->next_request_ = RequestType::COMMAND; + ESP_LOGD(TAG, "Queue command request"); + this->queue_request(this->command_message_); } void PanasonicHeatpumpComponent::set_command_byte(const uint8_t value, const uint8_t index) { - if (this->next_request_ != RequestType::COMMAND) { - // initialize the command - this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage), - std::end(PanasonicCommand::CommandMessage)); - } + this->command_message_ = build_message(PanasonicCommand::CommandMessage); + // set command byte this->command_message_[index] = value; // calculate and set set checksum (last element) this->command_message_.back() = PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1); - // command will be send on next loop - this->next_request_ = RequestType::COMMAND; + ESP_LOGD(TAG, "Queue command request"); + this->queue_request(this->command_message_); } void PanasonicHeatpumpComponent::set_command_curve(const uint8_t value, const uint8_t index) { - if (this->next_request_ != RequestType::COMMAND) { - // initialize the command - this->command_message_.assign(std::begin(PanasonicCommand::CommandMessage), - std::end(PanasonicCommand::CommandMessage)); - } + this->command_message_ = build_message(PanasonicCommand::CommandMessage); // Set zone 1 curve bytes 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() = PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1); - // command will be send on next loop - this->next_request_ = RequestType::COMMAND; + ESP_LOGD(TAG, "Queue command request"); + this->queue_request(this->command_message_); +} + +void PanasonicHeatpumpComponent::queue_request(const std::vector& message) { + auto* cmd = new std::vector(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 esphome diff --git a/components/panasonic_heatpump/panasonic_heatpump.h b/components/panasonic_heatpump/panasonic_heatpump.h index 6e32fa1..ea34ca1 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.h +++ b/components/panasonic_heatpump/panasonic_heatpump.h @@ -23,7 +23,6 @@ namespace esphome { namespace panasonic_heatpump { enum LoopState : uint8_t { READ_RESPONSE, - CHECK_RESPONSE, PUBLISH_SENSOR, PUBLISH_BINARY_SENSOR, PUBLISH_TEXT_SENSOR, @@ -34,7 +33,6 @@ enum LoopState : uint8_t { PUBLISH_WATER_HEATER, PUBLISH_EXTRA_SENSOR, SEND_REQUEST, - READ_REQUEST, RESTART_LOOP, }; @@ -48,7 +46,6 @@ enum RequestType : uint8_t { enum ResponseType : uint8_t { UNKNOWN, - RECEIVING, STANDARD, EXTRA, }; @@ -86,9 +83,9 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev void set_log_uart_msg(bool active) { this->log_uart_msg_ = active; } - // uart message variables to use in lambda functions - int getResponseByte(const int index); - int getExtraResponseByte(const int index); + // functions to use in esphome lambda + int get_response_byte(const int index); + int get_extra_response_byte(const int index); // command functions 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); @@ -128,27 +125,25 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev protected: // options variables - uart::UARTComponent* uart_client_{nullptr}; bool log_uart_msg_{false}; - uint32_t last_request_time_{0}; - uint32_t uart_client_timeout_{10000}; - // uart message variables + uint32_t last_client_request_time_{0}; + uint32_t uart_client_timeout_{10000}; // 10 sec + 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 heatpump_default_message_; std::vector heatpump_extra_message_; - std::vector response_message_; - std::vector request_message_; std::vector 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}; LoopState loop_state_{LoopState::RESTART_LOOP}; - RequestType next_request_{RequestType::INITIAL}; - ResponseType current_response_{ResponseType::UNKNOWN}; + // entity vectors std::vector binary_sensors_; std::vector climates_; @@ -161,10 +156,21 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev std::vector extra_sensors_; // uart message functions - void read_response(); - void send_request(RequestType requestType); - void read_request(); - ResponseType check_response(const std::vector& data); + static void uart_task(void* pvParameters); + static void uart_client_task(void* pvParameters); + bool receive_from_uart(uart::UARTComponent* src, std::vector& buffer); + + void send_request(); + void queue_request(const std::vector& message); + ResponseType read_response(); + static bool check_response_length(const std::vector& message); + static bool verify_message_header(const std::vector& message, bool reading_succeeded); + static bool verify_message_checksum(const std::vector& message); + + template + static std::vector build_message(const uint8_t (&msg)[N]) { + return std::vector(msg, msg + N); + } }; } // namespace panasonic_heatpump } // namespace esphome diff --git a/components/panasonic_heatpump/water_heater/panasonic_heatpump_water_heater.cpp b/components/panasonic_heatpump/water_heater/panasonic_heatpump_water_heater.cpp index 383a7d9..40f1ebd 100644 --- a/components/panasonic_heatpump/water_heater/panasonic_heatpump_water_heater.cpp +++ b/components/panasonic_heatpump/water_heater/panasonic_heatpump_water_heater.cpp @@ -23,7 +23,7 @@ water_heater::WaterHeaterTraits PanasonicHeatpumpWaterHeater::traits() { void PanasonicHeatpumpWaterHeater::control(const water_heater::WaterHeaterCall& call) { if (call.get_mode().has_value()) { - int byte6 = this->parent_->getResponseByte(6); + int byte6 = this->parent_->get_response_byte(6); if (byte6 >= 0) { water_heater::WaterHeaterMode new_mode = *call.get_mode(); uint8_t newByte6 = this->setWaterHeaterMode(new_mode, (uint8_t)byte6); diff --git a/tests/README.md b/tests/README.md index d6ef58e..a7cf92d 100644 --- a/tests/README.md +++ b/tests/README.md @@ -11,7 +11,6 @@ tests/ │ ├── README.md │ ├── test_panasonic_heatpump_unit.py # Unit 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_esp32c3.yaml # ESP32-C3 test config │ ├── test_panasonic_heatpump_cztaw1.yaml # CZ-TAW1 test config diff --git a/tests/panasonic_heatpump/README.md b/tests/panasonic_heatpump/README.md index 6ae0b5a..9ee8793 100644 --- a/tests/panasonic_heatpump/README.md +++ b/tests/panasonic_heatpump/README.md @@ -14,13 +14,11 @@ This directory contains comprehensive tests for the `panasonic_heatpump` ESPHome ### Integration Tests (`test_panasonic_heatpump_integration.py`) - Tests minimal 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-C3 board - Tests configuration with CZ-TAW1 (UART-proxy) ### 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_esp32c3.yaml` - ESP32-C3 mini (RISC-V) - `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 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 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 - All unit and integration tests pass - Latest ESPHome versions is supported -- ESP8266 and ESP32 boards are supported +- Only ESP32 boards are supported ## Contributing diff --git a/tests/panasonic_heatpump/test_panasonic_heatpump_esp8266.yaml b/tests/panasonic_heatpump/test_panasonic_heatpump_esp8266.yaml deleted file mode 100644 index 39edccd..0000000 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_esp8266.yaml +++ /dev/null @@ -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" diff --git a/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py b/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py index a826887..f189de4 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py @@ -12,17 +12,6 @@ class TestPanasonicHeatpumpIntegration: """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 def test_yaml_esp32(self): """Get the path to the ESP32 test YAML file.""" @@ -64,26 +53,6 @@ def test_yaml_cztaw1(self): "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): """Test that the ESP32 configuration is valid.""" if not os.path.exists(test_yaml_esp32):