diff --git a/components/panasonic_heatpump/climate/__init__.py b/components/panasonic_heatpump/climate/__init__.py index 774bebf..b4d13b8 100644 --- a/components/panasonic_heatpump/climate/__init__.py +++ b/components/panasonic_heatpump/climate/__init__.py @@ -13,12 +13,10 @@ from .. import ( ) CONF_COOL_MODE = "cool_mode" -CONF_CLIMATE_TANK = "tank" CONF_CLIMATE_ZONE1 = "zone1" CONF_CLIMATE_ZONE2 = "zone2" TYPES = [ - CONF_CLIMATE_TANK, CONF_CLIMATE_ZONE1, CONF_CLIMATE_ZONE2, ] @@ -45,9 +43,6 @@ CONFIG_SCHEMA = cv.Schema( PanasonicHeatpumpComponent ), cv.Optional(CONF_COOL_MODE, default=False): cv.boolean, - cv.Optional(CONF_CLIMATE_TANK): climate.climate_schema( - PanasonicHeatpumpClimate - ).extend(climate_options(20.0, 65.0, 0.5)), cv.Optional(CONF_CLIMATE_ZONE1): climate.climate_schema( PanasonicHeatpumpClimate ).extend(climate_options(-5.0, 5.0, 0.5)), diff --git a/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp b/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp index 8f29efa..b313413 100644 --- a/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp +++ b/components/panasonic_heatpump/climate/panasonic_heatpump_climate.cpp @@ -42,9 +42,6 @@ void PanasonicHeatpumpClimate::control(const climate::ClimateCall& call) { if (call.get_target_temperature().has_value()) { float new_temp = *call.get_target_temperature(); switch (this->id_) { - case ClimateIds::CONF_CLIMATE_TANK: - this->parent_->set_command_byte(PanasonicCommand::setPlus128(new_temp), 42); // set11 - break; case ClimateIds::CONF_CLIMATE_ZONE1: this->parent_->set_command_byte(PanasonicCommand::setPlus128(new_temp), 38); // set5 break; @@ -97,10 +94,6 @@ void PanasonicHeatpumpClimate::publish_new_state(const std::vector& dat new_mode = this->getClimateMode(data[6]); // set9 switch (this->id_) { - case ClimateIds::CONF_CLIMATE_TANK: - new_target_temp_heat = PanasonicDecode::getByteMinus128(data[42]); // set11 - new_current_temp = PanasonicDecode::getByteMinus128(data[141]); // top10 - break; case ClimateIds::CONF_CLIMATE_ZONE1: new_target_temp_heat = PanasonicDecode::getByteMinus128(data[38]); // set5 new_target_temp_cool = PanasonicDecode::getByteMinus128(data[39]); // set6 @@ -124,9 +117,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; @@ -139,15 +131,6 @@ void PanasonicHeatpumpClimate::publish_new_state(const std::vector& dat uint8_t PanasonicHeatpumpClimate::getClimateMode(const uint8_t input) { switch (this->id_) { - case ClimateIds::CONF_CLIMATE_TANK: - switch ((uint8_t)(input & 0b110000)) { - case 0b010000: - return climate::CLIMATE_MODE_OFF; - case 0b100000: - return climate::CLIMATE_MODE_HEAT; - default: - return 255; - }; case ClimateIds::CONF_CLIMATE_ZONE1: case ClimateIds::CONF_CLIMATE_ZONE2: switch ((uint8_t)(input & 0b1111)) { @@ -164,26 +147,16 @@ 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; }; } uint8_t PanasonicHeatpumpClimate::setClimateMode(const climate::ClimateMode mode, uint8_t byte) { uint8_t newByte = byte; switch (this->id_) { - case ClimateIds::CONF_CLIMATE_TANK: - newByte = newByte & 0b11001111; - switch (mode) { - case climate::CLIMATE_MODE_OFF: - return newByte + 0b010000; - case climate::CLIMATE_MODE_HEAT: - return newByte + 0b100000; - default: - return 0; - }; case ClimateIds::CONF_CLIMATE_ZONE1: case ClimateIds::CONF_CLIMATE_ZONE2: newByte = newByte & 0b11110000; diff --git a/components/panasonic_heatpump/climate/panasonic_heatpump_climate.h b/components/panasonic_heatpump/climate/panasonic_heatpump_climate.h index 5c505e3..eed84c8 100644 --- a/components/panasonic_heatpump/climate/panasonic_heatpump_climate.h +++ b/components/panasonic_heatpump/climate/panasonic_heatpump_climate.h @@ -9,7 +9,6 @@ namespace esphome { namespace panasonic_heatpump { enum ClimateIds : uint8_t { - CONF_CLIMATE_TANK, CONF_CLIMATE_ZONE1, CONF_CLIMATE_ZONE2, }; 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..5d893c0 --- /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_TANK = "tank" + +TYPES = [ + CONF_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_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..c0b8585 --- /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_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_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_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_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..fe09d39 --- /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_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/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..a584887 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_TANK, + TYPES, + ) + + assert CONFIG_SCHEMA is not None + assert CONF_TANK == "tank" + assert CONF_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."""