#include "panasonic_heatpump.h" #include "esphome/core/application.h" namespace esphome { namespace panasonic_heatpump { static const char* const TAG = "panasonic_heatpump"; void PanasonicHeatpumpComponent::dump_config() { size_t version_len = strlen(PANASONIC_HEATPUMP_VERSION); size_t line_width = 38 + version_len; std::string border(line_width, '*'); if (strstr(PANASONIC_HEATPUMP_VERSION, "beta") != nullptr) { ESP_LOGE(TAG, "%s", border.c_str()); ESP_LOGE(TAG, "*** Panasonic Heatpump Component v%s ***", PANASONIC_HEATPUMP_VERSION); ESP_LOGE(TAG, "%s", border.c_str()); } else { ESP_LOGW(TAG, "%s", border.c_str()); ESP_LOGW(TAG, "*** Panasonic Heatpump Component v%s ***", PANASONIC_HEATPUMP_VERSION); ESP_LOGW(TAG, "%s", border.c_str()); } delay(10); // NOLINT } void PanasonicHeatpumpComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up Panasonic Heatpump ..."); this->check_uart_settings(9600, 1, uart::UART_CONFIG_PARITY_EVEN, 8); 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() { // 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; // 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: { 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_); } this->loop_state_ = LoopState::PUBLISH_BINARY_SENSOR; break; case LoopState::PUBLISH_BINARY_SENSOR: for (auto* entity : this->binary_sensors_) { entity->publish_new_state(this->heatpump_default_message_); } this->loop_state_ = LoopState::PUBLISH_TEXT_SENSOR; break; case LoopState::PUBLISH_TEXT_SENSOR: for (auto* entity : this->text_sensors_) { entity->publish_new_state(this->heatpump_default_message_); } this->loop_state_ = LoopState::PUBLISH_NUMBER; break; case LoopState::PUBLISH_NUMBER: for (auto* entity : this->numbers_) { entity->publish_new_state(this->heatpump_default_message_); } this->loop_state_ = LoopState::PUBLISH_SELECT; break; case LoopState::PUBLISH_SELECT: for (auto* entity : this->selects_) { entity->publish_new_state(this->heatpump_default_message_); } this->loop_state_ = LoopState::PUBLISH_SWITCH; break; case LoopState::PUBLISH_SWITCH: for (auto* entity : this->switches_) { entity->publish_new_state(this->heatpump_default_message_); } this->loop_state_ = LoopState::PUBLISH_CLIMATE; break; case LoopState::PUBLISH_CLIMATE: for (auto* entity : this->climates_) { entity->publish_new_state(this->heatpump_default_message_); } this->loop_state_ = LoopState::PUBLISH_WATER_HEATER; break; case LoopState::PUBLISH_WATER_HEATER: for (auto* entity : this->water_heaters_) { entity->publish_new_state(this->heatpump_default_message_); } this->loop_state_ = LoopState::SEND_REQUEST; break; case LoopState::PUBLISH_EXTRA_SENSOR: for (auto* entity : this->extra_sensors_) { entity->publish_new_state(this->heatpump_extra_message_); } this->loop_state_ = LoopState::SEND_REQUEST; break; case LoopState::SEND_REQUEST: this->send_request(); // fallthrough default: this->loop_state_ = LoopState::READ_RESPONSE; break; }; } 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::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)); } } } // Used for both uart interfaces bool PanasonicHeatpumpComponent::receive_from_uart(uart::UARTComponent* uartComp, std::vector& 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 if (start_byte != 0x31 && start_byte != 0x71 && start_byte != 0xF1) { ESP_LOGW(TAG, "Invalid start byte: 0x%x", start_byte); return 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; } // Verify checksum if (!verify_message_checksum(buffer)) { return false; } // Message is complete return true; } 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 (message.size() < HEADER_SIZE) { ESP_LOGW(TAG, "Message too short to contain valid header"); return false; } 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; } 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 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_); 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& 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; } // 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_); // 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) { this->command_message_ = build_message(PanasonicCommand::CommandMessage); uint8_t lowNibble = this->heatpump_default_message_[index] & 0b1111; uint8_t highNibble = value << 4; // set command byte this->command_message_[index] = highNibble + lowNibble; // calculate and set set checksum (last element) this->command_message_.back() = PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1); 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) { this->command_message_ = build_message(PanasonicCommand::CommandMessage); uint8_t highNibble = this->heatpump_default_message_[index] & 0b11110000; uint8_t lowNibble = value & 0b1111; // set command byte this->command_message_[index] = highNibble + lowNibble; // calculate and set set checksum (last element) this->command_message_.back() = PanasonicCommand::calcChecksum(this->command_message_, this->command_message_.size() - 1); 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) { 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); 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) { 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 || index == 89) { this->command_message_[75] = this->heatpump_default_message_[75]; this->command_message_[76] = this->heatpump_default_message_[76]; this->command_message_[77] = this->heatpump_default_message_[77]; this->command_message_[78] = this->heatpump_default_message_[78]; this->command_message_[86] = this->heatpump_default_message_[86]; this->command_message_[87] = this->heatpump_default_message_[87]; this->command_message_[88] = this->heatpump_default_message_[88]; this->command_message_[89] = this->heatpump_default_message_[89]; } // Set zone 2 curve bytes if (index == 79 || index == 80 || index == 81 || index == 82 || index == 90 || index == 91 || index == 92 || index == 93) { this->command_message_[79] = this->heatpump_default_message_[79]; this->command_message_[80] = this->heatpump_default_message_[80]; this->command_message_[81] = this->heatpump_default_message_[81]; this->command_message_[82] = this->heatpump_default_message_[82]; this->command_message_[90] = this->heatpump_default_message_[90]; this->command_message_[91] = this->heatpump_default_message_[91]; this->command_message_[92] = this->heatpump_default_message_[92]; this->command_message_[93] = this->heatpump_default_message_[93]; } // 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); 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