From 98836df74f27abc5902220c2d30b6133f192b0eb Mon Sep 17 00:00:00 2001 From: ElVit Date: Mon, 1 Dec 2025 19:34:59 +0100 Subject: [PATCH 01/22] Added uart client timeout fix --- components/panasonic_heatpump/__init__.py | 3 +++ .../panasonic_heatpump/panasonic_heatpump.cpp | 14 ++++++++++++++ components/panasonic_heatpump/panasonic_heatpump.h | 5 +++++ 3 files changed, 22 insertions(+) diff --git a/components/panasonic_heatpump/__init__.py b/components/panasonic_heatpump/__init__.py index d94e32b..752521b 100644 --- a/components/panasonic_heatpump/__init__.py +++ b/components/panasonic_heatpump/__init__.py @@ -9,6 +9,7 @@ MULTICONF = True CONF_PANASONIC_HEATPUMP_ID = "panasonic_heatpump" CONF_UART_CLIENT = "uart_client_id" +CONF_UART_CLIENT_TIMEOUT = "uart_client_timeout" CONF_LOG_UART_MSG = "log_uart_msg" panasonic_heatpump_ns = cg.esphome_ns.namespace("panasonic_heatpump") @@ -21,6 +22,7 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PanasonicHeatpumpComponent), cv.Optional(CONF_UART_CLIENT): cv.use_id(uart.UARTComponent), + cv.Optional(CONF_UART_CLIENT_TIMEOUT, default="10000ms"): cv.positive_time_period_milliseconds, cv.Optional(CONF_LOG_UART_MSG, default=False): cv.boolean, } ) @@ -37,5 +39,6 @@ async def to_code(config): if CONF_UART_CLIENT in config: uart_client = await cg.get_variable(config[CONF_UART_CLIENT]) cg.add(var.set_uart_client(uart_client)) + cg.add(var.set_uart_client_timeout(config[CONF_UART_CLIENT_TIMEOUT])) cg.add(var.set_log_uart_msg(config[CONF_LOG_UART_MSG])) diff --git a/components/panasonic_heatpump/panasonic_heatpump.cpp b/components/panasonic_heatpump/panasonic_heatpump.cpp index c2abfb3..acec113 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.cpp +++ b/components/panasonic_heatpump/panasonic_heatpump.cpp @@ -25,6 +25,14 @@ void PanasonicHeatpumpComponent::update() { } 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_ > 0) { + uint32_t current_time = millis(); + if (current_time - this->last_request_time_ >= this->uart_client_timeout_) { + this->next_request_ = RequestType::POLLING; + } + } + switch (this->loop_state_) { case LoopState::READ_RESPONSE: this->read_response(); @@ -184,6 +192,9 @@ void PanasonicHeatpumpComponent::send_request(RequestType requestType) { break; }; + // Update last request time when request was sent + this->last_request_time_ = millis(); + this->next_request_ = RequestType::NONE; } @@ -232,6 +243,9 @@ void PanasonicHeatpumpComponent::read_request() { this->request_receiving_ = false; if (this->log_uart_msg_) PanasonicHelpers::log_uart_hex(UART_LOG_TX, this->request_message_, ','); + + // Update last request time when request is complete + this->last_request_time_ = millis(); } } } diff --git a/components/panasonic_heatpump/panasonic_heatpump.h b/components/panasonic_heatpump/panasonic_heatpump.h index de5789e..82887f8 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.h +++ b/components/panasonic_heatpump/panasonic_heatpump.h @@ -74,6 +74,9 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev void set_uart_client(uart::UARTComponent* uart) { this->uart_client_ = uart; } + void set_uart_client_timeout(uint32_t timeout_ms) { + this->uart_client_timeout_ = timeout_ms; + } void set_log_uart_msg(bool active) { this->log_uart_msg_ = active; } @@ -115,6 +118,8 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev // 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 std::vector heatpump_default_message_; std::vector heatpump_extra_message_; From f1ca4403129fec9a32811f1467c75fa6b04a9baa Mon Sep 17 00:00:00 2001 From: ElVit Date: Mon, 1 Dec 2025 19:38:29 +0100 Subject: [PATCH 02/22] Fix python formatting --- components/panasonic_heatpump/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/panasonic_heatpump/__init__.py b/components/panasonic_heatpump/__init__.py index 752521b..f3949b1 100644 --- a/components/panasonic_heatpump/__init__.py +++ b/components/panasonic_heatpump/__init__.py @@ -22,7 +22,9 @@ CONFIG_SCHEMA = ( { cv.GenerateID(): cv.declare_id(PanasonicHeatpumpComponent), cv.Optional(CONF_UART_CLIENT): cv.use_id(uart.UARTComponent), - cv.Optional(CONF_UART_CLIENT_TIMEOUT, default="10000ms"): cv.positive_time_period_milliseconds, + cv.Optional( + CONF_UART_CLIENT_TIMEOUT, default="10000ms" + ): cv.positive_time_period_milliseconds, cv.Optional(CONF_LOG_UART_MSG, default=False): cv.boolean, } ) From 1bbc1db03b52c510718073b4bb3d690ae1d37ebd Mon Sep 17 00:00:00 2001 From: ElVit Date: Tue, 2 Dec 2025 12:39:44 +0100 Subject: [PATCH 03/22] Fixed warning message if request or response was invalid --- components/panasonic_heatpump/panasonic_heatpump.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/panasonic_heatpump/panasonic_heatpump.cpp b/components/panasonic_heatpump/panasonic_heatpump.cpp index acec113..01dfff7 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.cpp +++ b/components/panasonic_heatpump/panasonic_heatpump.cpp @@ -140,7 +140,7 @@ void PanasonicHeatpumpComponent::read_response() { 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_, ',')); + PanasonicHelpers::byte_array_to_hex_string(this->response_message_, ',').c_str()); delay(10); // NOLINT continue; } @@ -148,7 +148,7 @@ void PanasonicHeatpumpComponent::read_response() { if (this->response_message_.size() == 4 && byte_ != 0x10 && byte_ != 0x21) { this->response_receiving_ = false; ESP_LOGW(TAG, "Invalid response message: 0x%s. Expected last byte to be 0x10 or 0x21", - PanasonicHelpers::byte_array_to_hex_string(this->response_message_, ',')); + PanasonicHelpers::byte_array_to_hex_string(this->response_message_, ',').c_str()); delay(10); // NOLINT continue; } @@ -225,7 +225,7 @@ void PanasonicHeatpumpComponent::read_request() { 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_, ',')); + PanasonicHelpers::byte_array_to_hex_string(this->request_message_, ',').c_str()); delay(10); // NOLINT continue; } @@ -233,7 +233,7 @@ void PanasonicHeatpumpComponent::read_request() { if (this->request_message_.size() == 4 && byte_ != 0x10 && byte_ != 0x21) { this->request_receiving_ = false; ESP_LOGW(TAG, "Invalid request message: 0x%s. Expected last byte to be 0x10 or 0x21", - PanasonicHelpers::byte_array_to_hex_string(this->request_message_, ',')); + PanasonicHelpers::byte_array_to_hex_string(this->request_message_, ',').c_str()); delay(10); // NOLINT continue; } From ec9bfb0b79edafb624e19b97502f321086788b01 Mon Sep 17 00:00:00 2001 From: ElVit Date: Tue, 2 Dec 2025 13:06:23 +0100 Subject: [PATCH 04/22] Updated README.md --- components/panasonic_heatpump/README.md | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/components/panasonic_heatpump/README.md b/components/panasonic_heatpump/README.md index 758956a..6369917 100644 --- a/components/panasonic_heatpump/README.md +++ b/components/panasonic_heatpump/README.md @@ -62,6 +62,7 @@ panasonic_heatpump: uart_client_id: uart_cz_taw1 log_uart_msg: true update_interval: 5s + uart_client_timeout: 10s sensor: - platform: panasonic_heatpump @@ -110,14 +111,15 @@ climate: ## Configuration variables - **id** (*Optional*, ID): Manually specify the ID used for actions. -- **update_interval** (*Optional*, Time): The interval of the polling request message to get the heatpump values. This time applies only if no `uart_client_id` is set. Defaults to 3s. -- **uart_id** (Optional, ID): Manually specify the UART ID of the Heatpump. Required if multiple UART buses are defined. -- **uart_client_id** (*Optional*, ID): Manually specify the UART ID of an additonal UART client like the Panasonic CZ-TAW1. If this ID is not set then your ESP controller will send the polling request messages. +- **update_interval** (*Optional*, [Time](https://esphome.io/guides/configuration-types/#time)): The interval of the polling request message to get the heatpump values. This time applies only if no `uart_client_id` is set. Defaults to 3s. +- **uart_id** (Optional, [ID](https://esphome.io/guides/configuration-types/#id)): Manually specify the UART ID of the Heatpump. Required if multiple UART buses are defined. +- **uart_client_id** (*Optional*, [ID](https://esphome.io/guides/configuration-types/#id)): Manually specify the UART ID of an additonal UART client like the Panasonic CZ-TAW1. If this ID is not set then your ESP controller will send the polling request messages. - **log_uart_msg** (*Optional*, boolean): Shows the raw UART messages in the logs, if set to `true`. The messages will be written to the log level `INFO`. Defaults to false. +- **uart_client_timeout** (*Optional*, [Time](https://esphome.io/guides/configuration-types/#time)): Maximum idle time for the `uart_client_id` connection. If no request is sent from the UART client for this duration, the component will send a POLLING request to the heatpump. This will (hopefully) prevent the heatpump from becoming unresponsive. Defaults to `10s`. ### Sensors -All sensors are optional and all default sensor variables can be applied. +All sensors are optional and all options from [sensor component](https://esphome.io/components/sensor/) can be applied. Here a list of all supported sensors: ```yaml @@ -330,7 +332,7 @@ sensor: ### Binary Sensors -All binary sensors are optional and all default binary sensor variables can be applied. +All binary sensors are optional and all options from [binary sensor component](https://esphome.io/components/binary_sensor/) can be applied. Here a list of all supported binary sensors: ```yaml @@ -386,7 +388,7 @@ binary_sensor: ### Text Sensors -All text sensors are optional and all default text sensor variables can be applied. +All text sensors are optional and all options from [text sensor component](https://esphome.io/components/text_sensor/) can be applied. Here a list of all supported text sensors: ```yaml @@ -438,7 +440,7 @@ text_sensor: ### Numbers -All numbers are optional and all default number variables can be applied. +All numbers are optional and all options from [number component](https://esphome.io/components/number/) can be applied. Additionally the options `min_value`, `max_value` and `step` can override the default limits of each set entitiy. This is usefull for example for `set5` to `set8` if `direct temperature` is configured instead of `compensation curve` (see `top76` and `top81`). Here a list of all supported numbers: @@ -516,7 +518,7 @@ number: ### Switches -All switches are optional and all default switch variables can be applied. +All switches are optional and all options from [switch component](https://esphome.io/components/switch/) can be applied. Here a list of all supported switches: ```yaml @@ -552,7 +554,7 @@ switch: ### Selects -All selects are optional and all default select variables can be applied. +All selects are optional and all options from [select component](https://esphome.io/components/select/) can be applied. Additionally the option `cool_mode` can be configured. If `cool_mode` is set to `true` the entity `set9` will have the additional select options `COOL`, `COOL+TANK`, `AUTO` and `AUTO+TANK`. Here a list of all supported selects: @@ -579,7 +581,7 @@ select: ### Climates -All climates are optional and all default climate variables can be applied. +All climates are optional and all options from [climate component](https://esphome.io/components/climate/) can be applied. Additionally the option `cool_mode` can be configured. If `cool_mode` is set to `true` the entity `zone1` and `zone2` will have the additional climate modes `COOL` and `AUTO`. Additionally the options `min_temperature`, `max_temperature` and `temperature_step` can override the default limits on each climate entitiy. From db478a30fadfe847df2bc909628a119560c047e4 Mon Sep 17 00:00:00 2001 From: ElVit Date: Tue, 2 Dec 2025 13:09:31 +0100 Subject: [PATCH 05/22] Updated README.md --- components/panasonic_heatpump/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/panasonic_heatpump/README.md b/components/panasonic_heatpump/README.md index 6369917..cc6afc8 100644 --- a/components/panasonic_heatpump/README.md +++ b/components/panasonic_heatpump/README.md @@ -111,7 +111,7 @@ climate: ## Configuration variables - **id** (*Optional*, ID): Manually specify the ID used for actions. -- **update_interval** (*Optional*, [Time](https://esphome.io/guides/configuration-types/#time)): The interval of the polling request message to get the heatpump values. This time applies only if no `uart_client_id` is set. Defaults to 3s. +- **update_interval** (*Optional*, [Time](https://esphome.io/guides/configuration-types/#time)): The interval of the polling request message to get the heatpump values. This time applies only if no `uart_client_id` is set. Defaults to `3s`. - **uart_id** (Optional, [ID](https://esphome.io/guides/configuration-types/#id)): Manually specify the UART ID of the Heatpump. Required if multiple UART buses are defined. - **uart_client_id** (*Optional*, [ID](https://esphome.io/guides/configuration-types/#id)): Manually specify the UART ID of an additonal UART client like the Panasonic CZ-TAW1. If this ID is not set then your ESP controller will send the polling request messages. - **log_uart_msg** (*Optional*, boolean): Shows the raw UART messages in the logs, if set to `true`. The messages will be written to the log level `INFO`. Defaults to false. From 94d156b30ae3cd49b7ec002582dd44a016d987ef Mon Sep 17 00:00:00 2001 From: ElVit Date: Tue, 2 Dec 2025 13:15:17 +0100 Subject: [PATCH 06/22] Disable uart_client_timeout if 100 or lower --- components/panasonic_heatpump/README.md | 2 +- components/panasonic_heatpump/panasonic_heatpump.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/panasonic_heatpump/README.md b/components/panasonic_heatpump/README.md index cc6afc8..f76f863 100644 --- a/components/panasonic_heatpump/README.md +++ b/components/panasonic_heatpump/README.md @@ -115,7 +115,7 @@ climate: - **uart_id** (Optional, [ID](https://esphome.io/guides/configuration-types/#id)): Manually specify the UART ID of the Heatpump. Required if multiple UART buses are defined. - **uart_client_id** (*Optional*, [ID](https://esphome.io/guides/configuration-types/#id)): Manually specify the UART ID of an additonal UART client like the Panasonic CZ-TAW1. If this ID is not set then your ESP controller will send the polling request messages. - **log_uart_msg** (*Optional*, boolean): Shows the raw UART messages in the logs, if set to `true`. The messages will be written to the log level `INFO`. Defaults to false. -- **uart_client_timeout** (*Optional*, [Time](https://esphome.io/guides/configuration-types/#time)): Maximum idle time for the `uart_client_id` connection. If no request is sent from the UART client for this duration, the component will send a POLLING request to the heatpump. This will (hopefully) prevent the heatpump from becoming unresponsive. Defaults to `10s`. +- **uart_client_timeout** (*Optional*, [Time](https://esphome.io/guides/configuration-types/#time)): Maximum idle time for the `uart_client_id` connection. If no request is sent from the UART client for this duration, the component will send a POLLING request to the heatpump. This will (hopefully) prevent the heatpump from becoming unresponsive. This feature can also be disabled by setting this option to `100ms` or lower. Defaults to `10s`. ### Sensors diff --git a/components/panasonic_heatpump/panasonic_heatpump.cpp b/components/panasonic_heatpump/panasonic_heatpump.cpp index 01dfff7..d8ddba2 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.cpp +++ b/components/panasonic_heatpump/panasonic_heatpump.cpp @@ -26,7 +26,7 @@ void PanasonicHeatpumpComponent::update() { 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_ > 0) { + 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; From f03d7f5bf8cd3d03008915de38725e7b0f71acb0 Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:00:59 +0100 Subject: [PATCH 07/22] Update Panasonic Heatpump version to 0.0.5-beta.1 --- components/panasonic_heatpump/panasonic_heatpump.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/panasonic_heatpump/panasonic_heatpump.h b/components/panasonic_heatpump/panasonic_heatpump.h index 82887f8..bbe7045 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.h +++ b/components/panasonic_heatpump/panasonic_heatpump.h @@ -12,7 +12,7 @@ #include "commands.h" #ifndef PANASONIC_HEATPUMP_VERSION -#define PANASONIC_HEATPUMP_VERSION "0.0.5" +#define PANASONIC_HEATPUMP_VERSION "0.0.5-beta.1" #endif namespace esphome { From f9366fe43d8b72402cfa24117d8f8323567f7ca2 Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:48:54 +0100 Subject: [PATCH 08/22] Update __init__.py --- components/panasonic_heatpump/text_sensor/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/panasonic_heatpump/text_sensor/__init__.py b/components/panasonic_heatpump/text_sensor/__init__.py index 112239b..517af7b 100644 --- a/components/panasonic_heatpump/text_sensor/__init__.py +++ b/components/panasonic_heatpump/text_sensor/__init__.py @@ -24,7 +24,7 @@ ICON_PUMP = "mdi:pump" ICON_EXTERNAL_PAD_HEATER = "mdi:radiator" CONF_TOP4 = "top4" # Operation Mode -# ToDo: Split up top4 into top4_1 (Heating Mode State) and top4_2 (DHW Mode State) +# TODO: Split up top4 into top4_1 (Heating Mode State) and top4_2 (DHW Mode State) CONF_TOP17 = "top17" # Powerful Mode Time CONF_TOP18 = "top18" # Quiet Mode Level CONF_TOP19 = "top19" # Holiday Mode State From 0f2403f02cc77af07dc4d5417fa45b23cb25476b Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:49:49 +0100 Subject: [PATCH 09/22] Update test_panasonic_heatpump_integration.py --- .../test_panasonic_heatpump_integration.py | 46 +++++++++++++++---- 1 file changed, 36 insertions(+), 10 deletions(-) diff --git a/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py b/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py index c9c0b5c..0f55874 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py @@ -16,33 +16,58 @@ class TestPanasonicHeatpumpIntegration: 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') + 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.""" base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) - return os.path.join(base_dir, 'tests', 'panasonic_heatpump', 'test_panasonic_heatpump_full.yaml') + return os.path.join( + base_dir, + 'tests', + 'panasonic_heatpump', + 'test_panasonic_heatpump_full.yaml' + ) @pytest.fixture def test_yaml_esp32s2(self): """Get the path to the ESP32-S2 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_esp32s2.yaml') + return os.path.join( + base_dir, + 'tests', + 'panasonic_heatpump', + 'test_panasonic_heatpump_esp32s2.yaml', + ) @pytest.fixture def test_yaml_esp32c3(self): """Get the path to the ESP32-C3 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_esp32c3.yaml') + return os.path.join( + base_dir, + 'tests', + 'panasonic_heatpump', + 'test_panasonic_heatpump_esp32c3.yaml', + ) @pytest.fixture def test_yaml_cztaw1(self): """Get the path to the CZ-TAW1 client 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_cztaw1.yaml') + return os.path.join( + base_dir, + 'tests', + 'panasonic_heatpump', + 'test_panasonic_heatpump_cztaw1.yaml', + ) - def test_validate_esp8266_config(self, test_yaml_esp8266): + 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}") @@ -60,7 +85,7 @@ class TestPanasonicHeatpumpIntegration: except subprocess.TimeoutExpired: pytest.fail("ESPHome config validation timed out") - def test_validate_esp32_config(self, test_yaml_esp32): + 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): pytest.skip(f"Test file not found: {test_yaml_esp32}") @@ -78,7 +103,7 @@ class TestPanasonicHeatpumpIntegration: except subprocess.TimeoutExpired: pytest.fail("ESPHome config validation timed out") - def test_validate_esp32s2_config(self, test_yaml_esp32s2): + def test_validate_esp32s2_config(self, test_yaml_esp32s2: str): """Test that the ESP32-S2 configuration is valid.""" if not os.path.exists(test_yaml_esp32s2): pytest.skip(f"Test file not found: {test_yaml_esp32s2}") @@ -96,7 +121,7 @@ class TestPanasonicHeatpumpIntegration: except subprocess.TimeoutExpired: pytest.fail("ESPHome config validation timed out") - def test_validate_esp32c3_config(self, test_yaml_esp32c3): + def test_validate_esp32c3_config(self, test_yaml_esp32c3: str): """Test that the ESP32-C3 configuration is valid.""" if not os.path.exists(test_yaml_esp32c3): pytest.skip(f"Test file not found: {test_yaml_esp32c3}") @@ -114,7 +139,7 @@ class TestPanasonicHeatpumpIntegration: except subprocess.TimeoutExpired: pytest.fail("ESPHome config validation timed out") - def test_validate_cztaw1_config(self, test_yaml_cztaw1): + def test_validate_cztaw1_config(self, test_yaml_cztaw1: str): """Test that the CZ-TAW1 client configuration with UART-proxy is valid.""" if not os.path.exists(test_yaml_cztaw1): pytest.skip(f"Test file not found: {test_yaml_cztaw1}") @@ -132,5 +157,6 @@ class TestPanasonicHeatpumpIntegration: except subprocess.TimeoutExpired: pytest.fail("ESPHome config validation timed out") + if __name__ == '__main__': pytest.main([__file__, '-v']) From e705dc15c20cadd6948c4377687b4013be468529 Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:52:48 +0100 Subject: [PATCH 10/22] Update test_panasonic_heatpump_unit.py --- .../test_panasonic_heatpump_unit.py | 61 +++++++++---------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py b/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py index b27ca41..7ca5628 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py @@ -6,7 +6,6 @@ Tests the Python configuration schema and validation logic. import pytest from unittest.mock import Mock, MagicMock, patch -import esphome.config_validation as cv from esphome.core import CORE @@ -44,7 +43,7 @@ class TestPanasonicHeatpumpConfig: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + import panasonic_heatpump as ph_init assert ph_init is not None except ImportError as e: @@ -58,15 +57,15 @@ class TestPanasonicHeatpumpConfig: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + import panasonic_heatpump as ph_init - + assert hasattr(ph_init, 'CODEOWNERS') assert ph_init.CODEOWNERS == ['@elvit'] - + assert hasattr(ph_init, 'MULTICONF') assert ph_init.MULTICONF is True - + assert hasattr(ph_init, 'DEPENDENCIES') assert 'uart' in ph_init.DEPENDENCIES @@ -78,15 +77,15 @@ class TestPanasonicHeatpumpConfig: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + import panasonic_heatpump as ph_init - + assert hasattr(ph_init, 'CONF_PANASONIC_HEATPUMP_ID') assert ph_init.CONF_PANASONIC_HEATPUMP_ID == "panasonic_heatpump" - + assert hasattr(ph_init, 'CONF_UART_CLIENT') assert ph_init.CONF_UART_CLIENT == "uart_client_id" - + assert hasattr(ph_init, 'CONF_LOG_UART_MSG') assert ph_init.CONF_LOG_UART_MSG == "log_uart_msg" @@ -100,9 +99,9 @@ class TestPanasonicHeatpumpConfig: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + import panasonic_heatpump as ph_init - + assert hasattr(ph_init, 'CONFIG_SCHEMA') assert ph_init.CONFIG_SCHEMA is not None @@ -140,9 +139,9 @@ class TestPanasonicHeatpumpConfig: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + import panasonic_heatpump as ph_init - + # The default is specified in the schema as False # This test verifies the constant exists assert ph_init.CONF_LOG_UART_MSG == "log_uart_msg" @@ -161,9 +160,9 @@ class TestPanasonicHeatpumpConfig: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + import panasonic_heatpump as ph_init - + # Verify MULTICONF is True, allowing multiple instances assert ph_init.MULTICONF is True @@ -175,9 +174,9 @@ class TestPanasonicHeatpumpConfig: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + import panasonic_heatpump as ph_init - + assert 'uart' in ph_init.DEPENDENCIES def test_namespace_definition(self): @@ -188,9 +187,9 @@ class TestPanasonicHeatpumpConfig: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + import panasonic_heatpump as ph_init - + # Verify namespace constant exists assert hasattr(ph_init, 'panasonic_heatpump_ns') @@ -207,7 +206,7 @@ class TestPanasonicHeatpumpPlatforms: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + from panasonic_heatpump.sensor import __init__ as sensor_init assert sensor_init is not None except ImportError: @@ -222,7 +221,7 @@ class TestPanasonicHeatpumpPlatforms: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + from panasonic_heatpump.binary_sensor import __init__ as bs_init assert bs_init is not None except ImportError: @@ -237,7 +236,7 @@ class TestPanasonicHeatpumpPlatforms: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + from panasonic_heatpump.text_sensor import __init__ as ts_init assert ts_init is not None except ImportError: @@ -252,7 +251,7 @@ class TestPanasonicHeatpumpPlatforms: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + from panasonic_heatpump.number import __init__ as num_init assert num_init is not None except ImportError: @@ -267,7 +266,7 @@ class TestPanasonicHeatpumpPlatforms: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + from panasonic_heatpump.select import __init__ as sel_init assert sel_init is not None except ImportError: @@ -282,7 +281,7 @@ class TestPanasonicHeatpumpPlatforms: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + from panasonic_heatpump.switch import __init__ as sw_init assert sw_init is not None except ImportError: @@ -297,7 +296,7 @@ class TestPanasonicHeatpumpPlatforms: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + from panasonic_heatpump.climate import __init__ as clim_init assert clim_init is not None except ImportError: @@ -313,13 +312,13 @@ class TestPanasonicHeatpumpCodeGeneration: with patch('esphome.codegen') as mock_cg, \ patch('esphome.components.uart') as mock_uart, \ patch('esphome.config_validation'): - + mock_cg.new_Pvariable = MagicMock(return_value=Mock()) mock_cg.register_component = MagicMock() mock_uart.register_uart_device = MagicMock() mock_cg.get_variable = MagicMock(return_value=Mock()) mock_cg.add = MagicMock() - + yield { 'cg': mock_cg, 'uart': mock_uart, @@ -333,9 +332,9 @@ class TestPanasonicHeatpumpCodeGeneration: os.path.dirname(__file__), '..', '..', 'components' ) sys.path.insert(0, components_path) - + import panasonic_heatpump as ph_init - + assert hasattr(ph_init, 'to_code') assert callable(ph_init.to_code) From a71a23e1f2fe371cc4f92a2564a185b2819a07ae Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:57:29 +0100 Subject: [PATCH 11/22] Update test_panasonic_heatpump_unit.py --- .../test_panasonic_heatpump_unit.py | 130 +++++++++++------- 1 file changed, 77 insertions(+), 53 deletions(-) diff --git a/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py b/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py index 7ca5628..0c812ef 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py @@ -22,13 +22,13 @@ class TestPanasonicHeatpumpConfig: @pytest.fixture def mock_uart(self): """Mock UART component.""" - with patch('esphome.components.uart') as mock: + with patch("esphome.components.uart") as mock: yield mock @pytest.fixture def mock_cg(self): """Mock code generator.""" - with patch('esphome.codegen') as mock: + with patch("esphome.codegen") as mock: mock.esphome_ns = MagicMock() mock.esphome_ns.namespace.return_value.class_ = MagicMock() yield mock @@ -38,13 +38,15 @@ class TestPanasonicHeatpumpConfig: try: import sys import os + # Add components directory to path components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) import panasonic_heatpump as ph_init + assert ph_init is not None except ImportError as e: pytest.fail(f"Failed to import panasonic_heatpump: {e}") @@ -53,90 +55,94 @@ class TestPanasonicHeatpumpConfig: """Test component metadata is correctly defined.""" import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) import panasonic_heatpump as ph_init - assert hasattr(ph_init, 'CODEOWNERS') - assert ph_init.CODEOWNERS == ['@elvit'] + assert hasattr(ph_init, "CODEOWNERS") + assert ph_init.CODEOWNERS == ["@elvit"] - assert hasattr(ph_init, 'MULTICONF') + assert hasattr(ph_init, "MULTICONF") assert ph_init.MULTICONF is True - assert hasattr(ph_init, 'DEPENDENCIES') - assert 'uart' in ph_init.DEPENDENCIES + assert hasattr(ph_init, "DEPENDENCIES") + assert "uart" in ph_init.DEPENDENCIES def test_config_constants(self): """Test that configuration constants are defined.""" import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) import panasonic_heatpump as ph_init - assert hasattr(ph_init, 'CONF_PANASONIC_HEATPUMP_ID') + assert hasattr(ph_init, "CONF_PANASONIC_HEATPUMP_ID") assert ph_init.CONF_PANASONIC_HEATPUMP_ID == "panasonic_heatpump" - assert hasattr(ph_init, 'CONF_UART_CLIENT') + assert hasattr(ph_init, "CONF_UART_CLIENT") assert ph_init.CONF_UART_CLIENT == "uart_client_id" - assert hasattr(ph_init, 'CONF_LOG_UART_MSG') + assert hasattr(ph_init, "CONF_LOG_UART_MSG") assert ph_init.CONF_LOG_UART_MSG == "log_uart_msg" - @patch('esphome.components.uart') - @patch('esphome.codegen') + @patch("esphome.components.uart") + @patch("esphome.codegen") def test_config_schema_structure(self, mock_cg, mock_uart): """Test that CONFIG_SCHEMA has the correct structure.""" import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) import panasonic_heatpump as ph_init - assert hasattr(ph_init, 'CONFIG_SCHEMA') + assert hasattr(ph_init, "CONFIG_SCHEMA") assert ph_init.CONFIG_SCHEMA is not None def test_valid_minimal_config(self): """Test validation of minimal valid configuration.""" config = { - 'id': 'my_heatpump', - 'uart_id': 'uart_bus', + "id": "my_heatpump", + "uart_id": "uart_bus", } # This would require full ESPHome environment to validate # Just verify the config structure is reasonable - assert 'id' in config - assert 'uart_id' in config + assert "id" in config + assert "uart_id" in config def test_valid_full_config(self): """Test validation of full configuration with all options.""" config = { - 'id': 'my_heatpump', - 'uart_id': 'uart_bus', - 'uart_client_id': 'uart_client', - 'log_uart_msg': True, - 'update_interval': '3s', + "id": "my_heatpump", + "uart_id": "uart_bus", + "uart_client_id": "uart_client", + "log_uart_msg": True, + "update_interval": "3s", } - assert 'id' in config - assert 'uart_id' in config - assert 'uart_client_id' in config - assert 'log_uart_msg' in config - assert 'update_interval' in config + assert "id" in config + assert "uart_id" in config + assert "uart_client_id" in config + assert "log_uart_msg" in config + assert "update_interval" in config def test_log_uart_msg_default(self): """Test that log_uart_msg defaults to False.""" import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) @@ -156,8 +162,9 @@ class TestPanasonicHeatpumpConfig: """Test that component supports multiple instances.""" import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) @@ -170,28 +177,30 @@ class TestPanasonicHeatpumpConfig: """Test that UART is listed as a dependency.""" import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) import panasonic_heatpump as ph_init - assert 'uart' in ph_init.DEPENDENCIES + assert "uart" in ph_init.DEPENDENCIES def test_namespace_definition(self): """Test that component namespace is correctly defined.""" import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) import panasonic_heatpump as ph_init # Verify namespace constant exists - assert hasattr(ph_init, 'panasonic_heatpump_ns') + assert hasattr(ph_init, "panasonic_heatpump_ns") class TestPanasonicHeatpumpPlatforms: @@ -202,12 +211,14 @@ class TestPanasonicHeatpumpPlatforms: try: import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) from panasonic_heatpump.sensor import __init__ as sensor_init + assert sensor_init is not None except ImportError: pytest.skip("Sensor platform not accessible in test environment") @@ -217,12 +228,14 @@ class TestPanasonicHeatpumpPlatforms: try: import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) from panasonic_heatpump.binary_sensor import __init__ as bs_init + assert bs_init is not None except ImportError: pytest.skip("Binary sensor platform not accessible in test environment") @@ -232,12 +245,14 @@ class TestPanasonicHeatpumpPlatforms: try: import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) from panasonic_heatpump.text_sensor import __init__ as ts_init + assert ts_init is not None except ImportError: pytest.skip("Text sensor platform not accessible in test environment") @@ -247,12 +262,14 @@ class TestPanasonicHeatpumpPlatforms: try: import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) from panasonic_heatpump.number import __init__ as num_init + assert num_init is not None except ImportError: pytest.skip("Number platform not accessible in test environment") @@ -262,12 +279,14 @@ class TestPanasonicHeatpumpPlatforms: try: import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) from panasonic_heatpump.select import __init__ as sel_init + assert sel_init is not None except ImportError: pytest.skip("Select platform not accessible in test environment") @@ -277,12 +296,14 @@ class TestPanasonicHeatpumpPlatforms: try: import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) from panasonic_heatpump.switch import __init__ as sw_init + assert sw_init is not None except ImportError: pytest.skip("Switch platform not accessible in test environment") @@ -292,12 +313,14 @@ class TestPanasonicHeatpumpPlatforms: try: import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) from panasonic_heatpump.climate import __init__ as clim_init + assert clim_init is not None except ImportError: pytest.skip("Climate platform not accessible in test environment") @@ -309,9 +332,9 @@ class TestPanasonicHeatpumpCodeGeneration: @pytest.fixture def mock_dependencies(self): """Mock all ESPHome dependencies.""" - with patch('esphome.codegen') as mock_cg, \ - patch('esphome.components.uart') as mock_uart, \ - patch('esphome.config_validation'): + with patch("esphome.codegen") as mock_cg, patch( + "esphome.components.uart" + ) as mock_uart, patch("esphome.config_validation"): mock_cg.new_Pvariable = MagicMock(return_value=Mock()) mock_cg.register_component = MagicMock() @@ -320,24 +343,25 @@ class TestPanasonicHeatpumpCodeGeneration: mock_cg.add = MagicMock() yield { - 'cg': mock_cg, - 'uart': mock_uart, + "cg": mock_cg, + "uart": mock_uart, } def test_to_code_function_exists(self): """Test that to_code function is defined.""" import sys import os + components_path = os.path.join( - os.path.dirname(__file__), '..', '..', 'components' + os.path.dirname(__file__), "..", "..", "components" ) sys.path.insert(0, components_path) import panasonic_heatpump as ph_init - assert hasattr(ph_init, 'to_code') + assert hasattr(ph_init, "to_code") assert callable(ph_init.to_code) -if __name__ == '__main__': - pytest.main([__file__, '-v']) +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From bfaf15407011f3d800af60fd9f8947b175f857fe Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 09:57:56 +0100 Subject: [PATCH 12/22] Update test_panasonic_heatpump_integration.py --- .../test_panasonic_heatpump_integration.py | 73 ++++++++++--------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py b/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py index 0f55874..a826887 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_integration.py @@ -18,9 +18,9 @@ class TestPanasonicHeatpumpIntegration: 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' + "tests", + "panasonic_heatpump", + "test_panasonic_heatpump_esp8266.yaml", ) @pytest.fixture @@ -28,10 +28,7 @@ class TestPanasonicHeatpumpIntegration: """Get the path to the ESP32 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_full.yaml' + base_dir, "tests", "panasonic_heatpump", "test_panasonic_heatpump_full.yaml" ) @pytest.fixture @@ -40,9 +37,9 @@ class TestPanasonicHeatpumpIntegration: base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) return os.path.join( base_dir, - 'tests', - 'panasonic_heatpump', - 'test_panasonic_heatpump_esp32s2.yaml', + "tests", + "panasonic_heatpump", + "test_panasonic_heatpump_esp32s2.yaml", ) @pytest.fixture @@ -51,9 +48,9 @@ class TestPanasonicHeatpumpIntegration: base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) return os.path.join( base_dir, - 'tests', - 'panasonic_heatpump', - 'test_panasonic_heatpump_esp32c3.yaml', + "tests", + "panasonic_heatpump", + "test_panasonic_heatpump_esp32c3.yaml", ) @pytest.fixture @@ -62,9 +59,9 @@ class TestPanasonicHeatpumpIntegration: base_dir = os.path.dirname(os.path.dirname(os.path.dirname(__file__))) return os.path.join( base_dir, - 'tests', - 'panasonic_heatpump', - 'test_panasonic_heatpump_cztaw1.yaml', + "tests", + "panasonic_heatpump", + "test_panasonic_heatpump_cztaw1.yaml", ) def test_validate_esp8266_config(self, test_yaml_esp8266: str): @@ -74,12 +71,14 @@ class TestPanasonicHeatpumpIntegration: try: result = subprocess.run( - ['esphome', 'config', test_yaml_esp8266], + ["esphome", "config", test_yaml_esp8266], capture_output=True, text=True, - timeout=120 + timeout=120, ) - assert result.returncode == 0, f"ESP8266 config validation failed: {result.stderr}" + assert ( + result.returncode == 0 + ), f"ESP8266 config validation failed: {result.stderr}" except FileNotFoundError: pytest.skip("ESPHome not installed") except subprocess.TimeoutExpired: @@ -92,12 +91,14 @@ class TestPanasonicHeatpumpIntegration: try: result = subprocess.run( - ['esphome', 'config', test_yaml_esp32], + ["esphome", "config", test_yaml_esp32], capture_output=True, text=True, - timeout=120 + timeout=120, ) - assert result.returncode == 0, f"ESP32 config validation failed: {result.stderr}" + assert ( + result.returncode == 0 + ), f"ESP32 config validation failed: {result.stderr}" except FileNotFoundError: pytest.skip("ESPHome not installed") except subprocess.TimeoutExpired: @@ -110,12 +111,14 @@ class TestPanasonicHeatpumpIntegration: try: result = subprocess.run( - ['esphome', 'config', test_yaml_esp32s2], + ["esphome", "config", test_yaml_esp32s2], capture_output=True, text=True, - timeout=120 + timeout=120, ) - assert result.returncode == 0, f"ESP32-S2 config validation failed: {result.stderr}" + assert ( + result.returncode == 0 + ), f"ESP32-S2 config validation failed: {result.stderr}" except FileNotFoundError: pytest.skip("ESPHome not installed") except subprocess.TimeoutExpired: @@ -128,12 +131,14 @@ class TestPanasonicHeatpumpIntegration: try: result = subprocess.run( - ['esphome', 'config', test_yaml_esp32c3], + ["esphome", "config", test_yaml_esp32c3], capture_output=True, text=True, - timeout=120 + timeout=120, ) - assert result.returncode == 0, f"ESP32-C3 config validation failed: {result.stderr}" + assert ( + result.returncode == 0 + ), f"ESP32-C3 config validation failed: {result.stderr}" except FileNotFoundError: pytest.skip("ESPHome not installed") except subprocess.TimeoutExpired: @@ -146,17 +151,19 @@ class TestPanasonicHeatpumpIntegration: try: result = subprocess.run( - ['esphome', 'config', test_yaml_cztaw1], + ["esphome", "config", test_yaml_cztaw1], capture_output=True, text=True, - timeout=120 + timeout=120, ) - assert result.returncode == 0, f"CZ-TAW1 config validation failed: {result.stderr}" + assert ( + result.returncode == 0 + ), f"CZ-TAW1 config validation failed: {result.stderr}" except FileNotFoundError: pytest.skip("ESPHome not installed") except subprocess.TimeoutExpired: pytest.fail("ESPHome config validation timed out") -if __name__ == '__main__': - pytest.main([__file__, '-v']) +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From c5434957a451a702801fd90bae089766bbbfbc1f Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:01:27 +0100 Subject: [PATCH 13/22] Update README.md --- components/panasonic_heatpump/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/components/panasonic_heatpump/README.md b/components/panasonic_heatpump/README.md index f76f863..b6ec3b9 100644 --- a/components/panasonic_heatpump/README.md +++ b/components/panasonic_heatpump/README.md @@ -110,12 +110,12 @@ climate: ## Configuration variables -- **id** (*Optional*, ID): Manually specify the ID used for actions. -- **update_interval** (*Optional*, [Time](https://esphome.io/guides/configuration-types/#time)): The interval of the polling request message to get the heatpump values. This time applies only if no `uart_client_id` is set. Defaults to `3s`. -- **uart_id** (Optional, [ID](https://esphome.io/guides/configuration-types/#id)): Manually specify the UART ID of the Heatpump. Required if multiple UART buses are defined. -- **uart_client_id** (*Optional*, [ID](https://esphome.io/guides/configuration-types/#id)): Manually specify the UART ID of an additonal UART client like the Panasonic CZ-TAW1. If this ID is not set then your ESP controller will send the polling request messages. -- **log_uart_msg** (*Optional*, boolean): Shows the raw UART messages in the logs, if set to `true`. The messages will be written to the log level `INFO`. Defaults to false. -- **uart_client_timeout** (*Optional*, [Time](https://esphome.io/guides/configuration-types/#time)): Maximum idle time for the `uart_client_id` connection. If no request is sent from the UART client for this duration, the component will send a POLLING request to the heatpump. This will (hopefully) prevent the heatpump from becoming unresponsive. This feature can also be disabled by setting this option to `100ms` or lower. Defaults to `10s`. +* **id** (*Optional*, ID): Manually specify the ID used for actions. +* **update_interval** (*Optional*, [Time](https://esphome.io/guides/configuration-types/#time)): The interval of the polling request message to get the heatpump values. This time applies only if no `uart_client_id` is set. Defaults to `3s`. +* **uart_id** (Optional, [ID](https://esphome.io/guides/configuration-types/#id)): Manually specify the UART ID of the Heatpump. Required if multiple UART buses are defined. +* **uart_client_id** (*Optional*, [ID](https://esphome.io/guides/configuration-types/#id)): Manually specify the UART ID of an additonal UART client like the Panasonic CZ-TAW1. If this ID is not set then your ESP controller will send the polling request messages. +* **log_uart_msg** (*Optional*, boolean): Shows the raw UART messages in the logs, if set to `true`. The messages will be written to the log level `INFO`. Defaults to false. +* **uart_client_timeout** (*Optional*, [Time](https://esphome.io/guides/configuration-types/#time)): Maximum idle time for the `uart_client_id` connection. If no request is sent from the UART client for this duration, the component will send a POLLING request to the heatpump. This will (hopefully) prevent the heatpump from becoming unresponsive. This feature can also be disabled by setting this option to `100ms` or lower. Defaults to `10s`. ### Sensors From 292d098df20cff40e0440b3ccb87f85050821983 Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:14:49 +0100 Subject: [PATCH 14/22] Update panasonic_heatpump.h --- components/panasonic_heatpump/panasonic_heatpump.h | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/components/panasonic_heatpump/panasonic_heatpump.h b/components/panasonic_heatpump/panasonic_heatpump.h index bbe7045..5ae9693 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.h +++ b/components/panasonic_heatpump/panasonic_heatpump.h @@ -12,7 +12,7 @@ #include "commands.h" #ifndef PANASONIC_HEATPUMP_VERSION -#define PANASONIC_HEATPUMP_VERSION "0.0.5-beta.1" +#define PANASONIC_HEATPUMP_VERSION "0.0.5-beta.2" #endif namespace esphome { @@ -113,6 +113,9 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev void add_extra_sensor(PanasonicHeatpumpEntity* sensor) { extra_sensors_.push_back(sensor); } + bool get_uart_client_timeout_exceeded() { + return this->uart_client_timeout_exceeded_; + } protected: // options variables @@ -133,6 +136,7 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev 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}; From b8b75685af76058a682778ed8e1345d075abb5e4 Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:16:20 +0100 Subject: [PATCH 15/22] Update panasonic_heatpump.cpp --- components/panasonic_heatpump/panasonic_heatpump.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/panasonic_heatpump/panasonic_heatpump.cpp b/components/panasonic_heatpump/panasonic_heatpump.cpp index d8ddba2..921b46d 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.cpp +++ b/components/panasonic_heatpump/panasonic_heatpump.cpp @@ -30,6 +30,7 @@ void PanasonicHeatpumpComponent::loop() { 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; } } @@ -246,6 +247,7 @@ void PanasonicHeatpumpComponent::read_request() { // Update last request time when request is complete this->last_request_time_ = millis(); + this->uart_client_timeout_exceeded_ = false; } } } From 2b34eed86e066bd10b246faf4810790b0f50b72a Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:16:58 +0100 Subject: [PATCH 16/22] Update panasonic_heatpump_binary_sensor.h --- .../binary_sensor/panasonic_heatpump_binary_sensor.h | 1 + 1 file changed, 1 insertion(+) diff --git a/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.h b/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.h index 157692d..6fced97 100644 --- a/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.h +++ b/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.h @@ -7,6 +7,7 @@ namespace esphome { namespace panasonic_heatpump { enum BinarySensorIds : uint8_t { + CONF_UART_CLIENT_TIMED_OUT, CONF_TOP0, CONF_TOP2, CONF_TOP3, From 96c12162075796b64e56d292fcbcb3bbc31f0d39 Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:17:46 +0100 Subject: [PATCH 17/22] Update panasonic_heatpump_binary_sensor.cpp --- .../binary_sensor/panasonic_heatpump_binary_sensor.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp b/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp index a3f5992..3220ad8 100644 --- a/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp +++ b/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp @@ -15,6 +15,11 @@ void PanasonicHeatpumpBinarySensor::publish_new_state(const std::vector bool new_state; switch (this->id_) { + case BinarySensorIds::CONF_UART_CLIENT_TIMED_OUT: + new_state = this->parent()->get_uart_client_timeout_exceeded(); + if (this->has_state() && this->state == new_state) + return; + break; case BinarySensorIds::CONF_TOP0: new_state = PanasonicDecode::getBinaryState(PanasonicDecode::getBit7and8(data[4])); if (this->has_state() && this->state == new_state) From a55e76c095ffae84d7e5db3eb71dbadb7f3e32ce Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:18:36 +0100 Subject: [PATCH 18/22] Update __init__.py --- components/panasonic_heatpump/binary_sensor/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/panasonic_heatpump/binary_sensor/__init__.py b/components/panasonic_heatpump/binary_sensor/__init__.py index 3483759..5b47a77 100644 --- a/components/panasonic_heatpump/binary_sensor/__init__.py +++ b/components/panasonic_heatpump/binary_sensor/__init__.py @@ -10,6 +10,7 @@ from .. import ( panasonic_heatpump_ns, ) +CONF_UART_CLIENT_TIMED_OUT = "uart_client_timed_out" CONF_TOP0 = "top0" # Heatpump State CONF_TOP2 = "top2" # Force DHW State CONF_TOP3 = "top3" # Quiet Mode Schedule @@ -35,6 +36,7 @@ CONF_TOP132 = "top132" # Bivalent Advanced Heat CONF_TOP133 = "top133" # Bivalent Advanced DHW TYPES = [ + CONF_UART_CLIENT_TIMED_OUT, CONF_TOP0, CONF_TOP2, CONF_TOP3, @@ -69,6 +71,9 @@ CONFIG_SCHEMA = cv.Schema( cv.GenerateID(CONF_PANASONIC_HEATPUMP_ID): cv.use_id( PanasonicHeatpumpComponent ), + cv.Optional(CONF_UART_CLIENT_TIMED_OUT): binary_sensor.binary_sensor_schema( + PanasonicHeatpumpBinarySensor, + ), cv.Optional(CONF_TOP0): binary_sensor.binary_sensor_schema( PanasonicHeatpumpBinarySensor, device_class=DEVICE_CLASS_RUNNING, From 4dbab2567dce0b5c3d65934dfe1ce29bc9eb9fbb Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:22:15 +0100 Subject: [PATCH 19/22] Update panasonic_heatpump_binary_sensor.cpp --- .../binary_sensor/panasonic_heatpump_binary_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp b/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp index 3220ad8..59eaad7 100644 --- a/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp +++ b/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp @@ -16,7 +16,7 @@ void PanasonicHeatpumpBinarySensor::publish_new_state(const std::vector bool new_state; switch (this->id_) { case BinarySensorIds::CONF_UART_CLIENT_TIMED_OUT: - new_state = this->parent()->get_uart_client_timeout_exceeded(); + new_state = this->parent_()->get_uart_client_timeout_exceeded(); if (this->has_state() && this->state == new_state) return; break; From 615e01472eaa54382e1d24ccde7c326146a6d464 Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:27:38 +0100 Subject: [PATCH 20/22] Update panasonic_heatpump_binary_sensor.cpp --- .../binary_sensor/panasonic_heatpump_binary_sensor.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp b/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp index 59eaad7..d5687ee 100644 --- a/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp +++ b/components/panasonic_heatpump/binary_sensor/panasonic_heatpump_binary_sensor.cpp @@ -16,7 +16,7 @@ void PanasonicHeatpumpBinarySensor::publish_new_state(const std::vector bool new_state; switch (this->id_) { case BinarySensorIds::CONF_UART_CLIENT_TIMED_OUT: - new_state = this->parent_()->get_uart_client_timeout_exceeded(); + new_state = this->parent_->get_uart_client_timeout_exceeded(); if (this->has_state() && this->state == new_state) return; break; From b39d786d3f82778f3aa36ef85829b9481fd45ac2 Mon Sep 17 00:00:00 2001 From: ElVit <54866762+ElVit@users.noreply.github.com> Date: Mon, 8 Dec 2025 10:52:44 +0100 Subject: [PATCH 21/22] Update README.md --- components/panasonic_heatpump/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/components/panasonic_heatpump/README.md b/components/panasonic_heatpump/README.md index b6ec3b9..d1ece4e 100644 --- a/components/panasonic_heatpump/README.md +++ b/components/panasonic_heatpump/README.md @@ -338,6 +338,8 @@ Here a list of all supported binary sensors: ```yaml binary_sensor: - platform: panasonic_heatpump + uart_client_timed_out: + name: "UART Client Timed Out" top0: name: "Heatpump State" top2: From e28ffa3288db50a4ca1e553a3f1b984096cca0af Mon Sep 17 00:00:00 2001 From: ElVit Date: Mon, 15 Dec 2025 20:05:39 +0100 Subject: [PATCH 22/22] fixed warning message (#13) --- components/panasonic_heatpump/panasonic_heatpump.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/components/panasonic_heatpump/panasonic_heatpump.cpp b/components/panasonic_heatpump/panasonic_heatpump.cpp index 921b46d..ab4a4b7 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.cpp +++ b/components/panasonic_heatpump/panasonic_heatpump.cpp @@ -145,10 +145,10 @@ void PanasonicHeatpumpComponent::read_response() { delay(10); // NOLINT continue; } - // 4. byte shall be 0x10 or 0x21 - if (this->response_message_.size() == 4 && byte_ != 0x10 && byte_ != 0x21) { + // 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 0x10 or 0x21", + 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;