diff --git a/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp b/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp index 8f29efa..963e56c 100644 --- a/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp +++ b/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp @@ -124,9 +124,8 @@ void PanasonicHeatpumpClimate::publish_new_state(const std::vector& dat this->target_temperature_low == new_target_temp_cool && this->current_temperature == new_current_temp) return; - if (new_mode != 255) + if (new_mode != 0xFF) this->mode = (climate::ClimateMode)new_mode; - // this->action = climate::CLIMATE_ACTION_IDLE; if (this->get_traits().has_feature_flags(climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { this->target_temperature_high = new_target_temp_heat; this->target_temperature_low = new_target_temp_cool; @@ -164,10 +163,10 @@ uint8_t PanasonicHeatpumpClimate::getClimateMode(const uint8_t input) { case 0b1010: return climate::CLIMATE_MODE_AUTO; // 0x1A = auto-cool default: - return 255; + return 0xFF; }; default: - return 255; + return 0xFF; }; } diff --git a/components/panasonic_heatpump/commands.cpp b/components/panasonic_heatpump/commands.cpp index 7a95e75..872f6c5 100644 --- a/components/panasonic_heatpump/commands.cpp +++ b/components/panasonic_heatpump/commands.cpp @@ -99,7 +99,7 @@ uint8_t PanasonicCommand::setOperationMode(size_t input) { } } -// start of optional pcb commands +// --- start of optional pcb commands --- uint8_t PanasonicCommand::temp2hex(float temp) { int hextemp = 0; diff --git a/components/panasonic_heatpump/panasonic_heatpump.cpp b/components/panasonic_heatpump/panasonic_heatpump.cpp index d450643..5cc16f4 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.cpp +++ b/components/panasonic_heatpump/panasonic_heatpump.cpp @@ -93,7 +93,13 @@ void PanasonicHeatpumpComponent::loop() { for (auto* entity : this->climates_) { entity->publish_new_state(this->heatpump_default_message_); } - this->loop_state_ = LoopState::SEND_REQUEST; + 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::PUBLISH_EXTRA_SENSOR; break; case LoopState::PUBLISH_EXTRA_SENSOR: for (auto* entity : this->extra_sensors_) { diff --git a/components/panasonic_heatpump/panasonic_heatpump.h b/components/panasonic_heatpump/panasonic_heatpump.h index 5ae9693..fc20c1f 100644 --- a/components/panasonic_heatpump/panasonic_heatpump.h +++ b/components/panasonic_heatpump/panasonic_heatpump.h @@ -27,6 +27,7 @@ enum LoopState : uint8_t { PUBLISH_SELECT, PUBLISH_SWITCH, PUBLISH_CLIMATE, + PUBLISH_WATER_HEATER, PUBLISH_EXTRA_SENSOR, SEND_REQUEST, READ_REQUEST, @@ -95,6 +96,9 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev void add_climate(PanasonicHeatpumpEntity* climate) { climates_.push_back(climate); } + void add_water_heater(PanasonicHeatpumpEntity* water_heater) { + water_heaters_.push_back(water_heater); + } void add_number(PanasonicHeatpumpEntity* number) { numbers_.push_back(number); } @@ -143,6 +147,7 @@ class PanasonicHeatpumpComponent : public PollingComponent, public uart::UARTDev // entity vectors std::vector binary_sensors_; std::vector climates_; + std::vector water_heaters_; std::vector numbers_; std::vector selects_; std::vector sensors_; diff --git a/components/panasonic_heatpump/water_heater/__init__.py b/components/panasonic_heatpump/water_heater/__init__.py new file mode 100644 index 0000000..d9eb74d --- /dev/null +++ b/components/panasonic_heatpump/water_heater/__init__.py @@ -0,0 +1,60 @@ +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.components import water_heater +from esphome.const import ( + CONF_MIN_TEMPERATURE, + CONF_MAX_TEMPERATURE, +) +from .. import ( + CONF_PANASONIC_HEATPUMP_ID, + PanasonicHeatpumpComponent, + panasonic_heatpump_ns, +) + +CONF_TARGET_TEMPERATURE_STEP = "target_temperature_step" + +CONF_HEATER_TANK = "tank" + +TYPES = [ + CONF_HEATER_TANK, +] + + +def water_heater_options(min_temp, max_temp, temp_step) -> cv.Schema: + schema = cv.Schema( + { + cv.Optional(CONF_MIN_TEMPERATURE, default=min_temp): cv.float_, + cv.Optional(CONF_MAX_TEMPERATURE, default=max_temp): cv.float_, + cv.Optional(CONF_TARGET_TEMPERATURE_STEP, default=temp_step): cv.float_, + } + ) + return schema + + +PanasonicHeatpumpWaterHeater = panasonic_heatpump_ns.class_( + "PanasonicHeatpumpWaterHeater", water_heater.WaterHeater, cg.Component +) + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_PANASONIC_HEATPUMP_ID): cv.use_id( + PanasonicHeatpumpComponent + ), + cv.Optional(CONF_HEATER_TANK): water_heater.water_heater_schema( + PanasonicHeatpumpWaterHeater + ).extend(water_heater_options(20.0, 65.0, 0.5)), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_PANASONIC_HEATPUMP_ID]) + for index, key in enumerate(TYPES): + if child_config := config.get(key): + var = await water_heater.new_water_heater(child_config) + cg.add(var.set_parent(parent)) + cg.add(var.set_id(index)) + cg.add(var.set_min_temperature(child_config[CONF_MIN_TEMPERATURE])) + cg.add(var.set_max_temperature(child_config[CONF_MAX_TEMPERATURE])) + cg.add(var.set_temperature_step(child_config[CONF_TARGET_TEMPERATURE_STEP])) + cg.add(parent.add_water_heater(var)) diff --git a/components/panasonic_heatpump/water_heater/panasonic_heatpump_water_heater.cpp b/components/panasonic_heatpump/water_heater/panasonic_heatpump_water_heater.cpp new file mode 100644 index 0000000..7073d0d --- /dev/null +++ b/components/panasonic_heatpump/water_heater/panasonic_heatpump_water_heater.cpp @@ -0,0 +1,116 @@ +#include "panasonic_heatpump_water_heater.h" +#include "esphome/core/log.h" + +namespace esphome { +namespace panasonic_heatpump { +static const char* const TAG = "panasonic_heatpump.water_heater"; + +void PanasonicHeatpumpWaterHeater::dump_config() { + LOG_WATER_HEATER("", "Panasonic Heatpump Water Heater", this); +} + +water_heater::WaterHeaterTraits PanasonicHeatpumpWaterHeater::traits() { + auto traits = water_heater::WaterHeaterTraits(); + + traits.add_feature_flags(water_heater::WATER_HEATER_SUPPORTS_CURRENT_TEMPERATURE); + traits.set_supported_modes({water_heater::WATER_HEATER_MODE_OFF, water_heater::WATER_HEATER_MODE_HEAT_PUMP}); + traits.set_min_temperature(this->min_temperature_); + traits.set_max_temperature(this->max_temperature_); + traits.set_target_temperature_step(this->temperature_step_); + + return traits; +} + +void PanasonicHeatpumpWaterHeater::control(const water_heater::WaterHeaterCall& call) { + if (call.get_mode().has_value()) { + int byte6 = this->parent_->getResponseByte(6); + if (byte6 >= 0) { + water_heater::WaterHeaterMode new_mode = *call.get_mode(); + uint8_t newByte6 = this->setWaterHeaterMode(new_mode, (uint8_t)byte6); + this->parent_->set_command_byte(newByte6, 6); + } + } + + float new_temp = call.get_target_temperature(); + switch (this->id_) { + case WaterHeaterIds::CONF_HEATER_TANK: + this->parent_->set_command_byte(PanasonicCommand::setPlus128(new_temp), 42); // set11 + break; + }; + + this->publish_state(); + this->keep_state_ = 2; +} + +void PanasonicHeatpumpWaterHeater::publish_new_state(const std::vector& data) { + if (this->keep_state_ > 0) { + this->keep_state_--; + return; + } + if (data.empty()) + return; + + uint8_t new_mode; + float new_target_temp_heat; + float new_current_temp; + + new_mode = this->getWaterHeaterMode(data[6]); // set9 + switch (this->id_) { + case WaterHeaterIds::CONF_HEATER_TANK: + new_target_temp_heat = PanasonicDecode::getByteMinus128(data[42]); // set11 + new_current_temp = PanasonicDecode::getByteMinus128(data[141]); // top10 + break; + default: + return; + }; + + if (this->mode_ == new_mode && this->target_temperature_ == new_target_temp_heat && + this->current_temperature_ == new_current_temp) + return; + + if (new_mode != 0xFF) + this->mode_ = (water_heater::WaterHeaterMode)new_mode; + this->target_temperature_ = new_target_temp_heat; + this->current_temperature_ = new_current_temp; + this->publish_state(); +} + +uint8_t PanasonicHeatpumpWaterHeater::getWaterHeaterMode(const uint8_t input) { + switch (this->id_) { + case WaterHeaterIds::CONF_HEATER_TANK: + switch ((uint8_t)(input & 0b110000)) { + case 0b010000: + return water_heater::WATER_HEATER_MODE_OFF; + case 0b100000: + return water_heater::WATER_HEATER_MODE_HEAT_PUMP; + default: + return 0xFF; + }; + default: + return 0xFF; + }; +} + +uint8_t PanasonicHeatpumpWaterHeater::setWaterHeaterMode(const water_heater::WaterHeaterMode mode, const uint8_t byte) { + uint8_t newByte = byte; + switch (this->id_) { + case WaterHeaterIds::CONF_HEATER_TANK: + newByte = newByte & 0b11001111; + switch (mode) { + case water_heater::WATER_HEATER_MODE_OFF: + return newByte + 0b010000; + case water_heater::WATER_HEATER_MODE_HEAT_PUMP: + return newByte + 0b100000; + default: + return 0; + }; + default: + return 0; + }; +} + +water_heater::WaterHeaterCallInternal PanasonicHeatpumpWaterHeater::make_call() { + return water_heater::WaterHeaterCallInternal(this); +} +} // namespace panasonic_heatpump +} // namespace esphome diff --git a/components/panasonic_heatpump/water_heater/panasonic_heatpump_water_heater.h b/components/panasonic_heatpump/water_heater/panasonic_heatpump_water_heater.h new file mode 100644 index 0000000..2a3e6d8 --- /dev/null +++ b/components/panasonic_heatpump/water_heater/panasonic_heatpump_water_heater.h @@ -0,0 +1,45 @@ +#pragma once +#include "esphome/core/component.h" +#include "esphome/components/water_heater/water_heater.h" +#include "../panasonic_heatpump.h" +#include "../decode.h" +#include "../commands.h" +#include + +namespace esphome { +namespace panasonic_heatpump { +enum WaterHeaterIds : uint8_t { + CONF_HEATER_TANK, +}; + +class PanasonicHeatpumpWaterHeater : public water_heater::WaterHeater, + public Parented, + public PanasonicHeatpumpEntity { + public: + PanasonicHeatpumpWaterHeater() = default; + void dump_config() override; + water_heater::WaterHeaterTraits traits() override; + void publish_new_state(const std::vector& data) override; + + void set_min_temperature(float value) { + this->min_temperature_ = value; + } + void set_max_temperature(float value) { + this->max_temperature_ = value; + } + void set_temperature_step(float value) { + this->temperature_step_ = value; + } + + protected: + void control(const water_heater::WaterHeaterCall& call) override; + water_heater::WaterHeaterCallInternal make_call() override; + uint8_t getWaterHeaterMode(const uint8_t input); + uint8_t setWaterHeaterMode(const water_heater::WaterHeaterMode mode, const uint8_t byte); + + float min_temperature_{20.0f}; + float max_temperature_{65.0f}; + float temperature_step_{0.5f}; +}; +} // namespace panasonic_heatpump +} // namespace esphome diff --git a/example_panasonic_heatpump.yaml b/example_panasonic_heatpump.yaml index ecf5442..963b371 100644 --- a/example_panasonic_heatpump.yaml +++ b/example_panasonic_heatpump.yaml @@ -504,9 +504,12 @@ select: climate: - platform: panasonic_heatpump cool_mode: true - tank: - name: "DHW" zone1: name: "Zone 1" zone2: name: "Zone 2" + +water_heater: + - platform: panasonic_heatpump + tank: + name: "DHW" diff --git a/tests/panasonic_heatpump/test_panasonic_heatpump_cztaw1.yaml b/tests/panasonic_heatpump/test_panasonic_heatpump_cztaw1.yaml index e31a4df..37d9333 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_cztaw1.yaml +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_cztaw1.yaml @@ -82,6 +82,12 @@ switch: # 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_esp32c3.yaml b/tests/panasonic_heatpump/test_panasonic_heatpump_esp32c3.yaml index bf48523..1b08fa5 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_esp32c3.yaml +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_esp32c3.yaml @@ -73,6 +73,12 @@ switch: # 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_esp32s2.yaml b/tests/panasonic_heatpump/test_panasonic_heatpump_esp32s2.yaml index 2959dad..979dd0b 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_esp32s2.yaml +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_esp32s2.yaml @@ -73,6 +73,12 @@ switch: # 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_esp8266.yaml b/tests/panasonic_heatpump/test_panasonic_heatpump_esp8266.yaml index 5abdd2d..39edccd 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_esp8266.yaml +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_esp8266.yaml @@ -70,6 +70,12 @@ switch: # 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_full.yaml b/tests/panasonic_heatpump/test_panasonic_heatpump_full.yaml index 32404ae..6aa062a 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_full.yaml +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_full.yaml @@ -472,9 +472,13 @@ switch: climate: - platform: panasonic_heatpump cool_mode: true + zone1: + name: "Zone 1" + zone2: + name: "Zone 2" + +# Test water_heater platform +water_heater: + - platform: panasonic_heatpump tank: name: "DHW" - zone1: - name: "Zone1" - zone2: - name: "Zone2" diff --git a/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py b/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py index 0c812ef..aac7d07 100644 --- a/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py +++ b/tests/panasonic_heatpump/test_panasonic_heatpump_unit.py @@ -325,6 +325,86 @@ class TestPanasonicHeatpumpPlatforms: except ImportError: pytest.skip("Climate platform not accessible in test environment") + def test_water_heater_platform_exists(self): + """Test that water_heater platform can be imported.""" + try: + import sys + import os + + components_path = os.path.join( + os.path.dirname(__file__), "..", "..", "components" + ) + sys.path.insert(0, components_path) + + from panasonic_heatpump.water_heater import __init__ as wh_init + + assert wh_init is not None + except ImportError: + pytest.skip("Water heater platform not accessible in test environment") + + def test_water_heater_platform_metadata(self): + """Test that water_heater platform has correct configuration.""" + try: + import sys + import os + + components_path = os.path.join( + os.path.dirname(__file__), "..", "..", "components" + ) + sys.path.insert(0, components_path) + + from panasonic_heatpump.water_heater import ( + CONFIG_SCHEMA, + CONF_HEATER_TANK, + TYPES, + ) + + assert CONFIG_SCHEMA is not None + assert CONF_HEATER_TANK == "tank" + assert CONF_HEATER_TANK in TYPES + except ImportError: + pytest.skip("Water heater platform not accessible in test environment") + + def test_water_heater_platform_options(self): + """Test that water_heater_options schema is defined.""" + try: + import sys + import os + + components_path = os.path.join( + os.path.dirname(__file__), "..", "..", "components" + ) + sys.path.insert(0, components_path) + + from panasonic_heatpump.water_heater import water_heater_options + + # Test default temperature range and step + schema = water_heater_options(20.0, 65.0, 0.5) + assert schema is not None + + # Verify defaults work + schema2 = water_heater_options(15.0, 70.0, 1.0) + assert schema2 is not None + except ImportError: + pytest.skip("Water heater platform not accessible in test environment") + + def test_water_heater_to_code_function(self): + """Test that water_heater to_code function is defined.""" + try: + import sys + import os + + components_path = os.path.join( + os.path.dirname(__file__), "..", "..", "components" + ) + sys.path.insert(0, components_path) + + from panasonic_heatpump.water_heater import to_code + + assert callable(to_code) + except ImportError: + pytest.skip("Water heater platform not accessible in test environment") + class TestPanasonicHeatpumpCodeGeneration: """Test suite for code generation logic."""