Merge pull request #22 from ElVit/heatpump_water_heater
Add water_heater to panasonic_heatpump
This commit is contained in:
commit
385c4b5c2b
|
|
@ -124,9 +124,8 @@ void PanasonicHeatpumpClimate::publish_new_state(const std::vector<uint8_t>& 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;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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_) {
|
||||
|
|
|
|||
|
|
@ -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<PanasonicHeatpumpEntity*> binary_sensors_;
|
||||
std::vector<PanasonicHeatpumpEntity*> climates_;
|
||||
std::vector<PanasonicHeatpumpEntity*> water_heaters_;
|
||||
std::vector<PanasonicHeatpumpEntity*> numbers_;
|
||||
std::vector<PanasonicHeatpumpEntity*> selects_;
|
||||
std::vector<PanasonicHeatpumpEntity*> sensors_;
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
@ -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<uint8_t>& 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
|
||||
|
|
@ -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 <set>
|
||||
|
||||
namespace esphome {
|
||||
namespace panasonic_heatpump {
|
||||
enum WaterHeaterIds : uint8_t {
|
||||
CONF_HEATER_TANK,
|
||||
};
|
||||
|
||||
class PanasonicHeatpumpWaterHeater : public water_heater::WaterHeater,
|
||||
public Parented<PanasonicHeatpumpComponent>,
|
||||
public PanasonicHeatpumpEntity {
|
||||
public:
|
||||
PanasonicHeatpumpWaterHeater() = default;
|
||||
void dump_config() override;
|
||||
water_heater::WaterHeaterTraits traits() override;
|
||||
void publish_new_state(const std::vector<uint8_t>& 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
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue