Added climate entities.

Renamed 'raw_topics' into 'traits_settings'.
This commit is contained in:
ElVit 2025-06-20 13:11:50 +02:00
parent d8b26aad78
commit 472d36bf72
10 changed files with 384 additions and 59 deletions

View File

@ -11,6 +11,7 @@ DEPENDENCIES = ['uart']
CONF_PANASONIC_HEATPUMP_ID = "panasonic_heatpump"
CONF_UART_CLIENT = "uart_client_id"
CONF_LOG_UART_MSG = "log_uart_msg"
CONF_COOL_MODE = "cool_mode"
panasonic_heatpump_ns = cg.esphome_ns.namespace('panasonic_heatpump')
@ -23,6 +24,7 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_UART_CLIENT): cv.use_id(uart.UARTComponent),
cv.Optional(CONF_LOG_UART_MSG, default=False): cv.boolean,
cv.Optional(CONF_COOL_MODE, default=False): cv.boolean,
}
)
.extend(cv.polling_component_schema("3s"))
@ -39,3 +41,4 @@ async def to_code(config):
cg.add(var.set_uart_client(uart_client))
cg.add(var.set_log_uart_msg(config[CONF_LOG_UART_MSG]))
cg.add(var.set_cool_mode(config[CONF_COOL_MODE]))

View File

@ -0,0 +1,70 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate
from esphome.components.climate import (
ClimateMode,
ClimatePreset,
)
from esphome.const import (
CONF_ID,
CONF_SUPPORTED_MODES,
CONF_SUPPORTED_PRESETS,
CONF_CUSTOM_PRESETS,
CONF_VISUAL,
CONF_MIN_TEMPERATURE,
CONF_MAX_TEMPERATURE,
CONF_TEMPERATURE_STEP,
)
from .. import CONF_PANASONIC_HEATPUMP_ID, PanasonicHeatpumpComponent, panasonic_heatpump_ns
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,
]
PanasonicHeatpumpClimate = panasonic_heatpump_ns.class_("PanasonicHeatpumpClimate", climate.Climate, cg.Component)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_PANASONIC_HEATPUMP_ID): cv.use_id(PanasonicHeatpumpComponent),
cv.Optional(CONF_COOL_MODE, default=False): cv.boolean,
cv.Optional(CONF_CLIMATE_TANK): climate.climate_schema(PanasonicHeatpumpClimate)
.extend({
cv.Optional(CONF_MIN_TEMPERATURE, default=20.0): cv.float_range(min=-5.0, max=20.0),
cv.Optional(CONF_MAX_TEMPERATURE, default=65.0): cv.float_range(min=5.0, max=75.0),
}
),
cv.Optional(CONF_CLIMATE_ZONE1): climate.climate_schema(PanasonicHeatpumpClimate)
.extend({
cv.Optional(CONF_MIN_TEMPERATURE, default=-5.0): cv.float_range(min=-5.0, max=20.0),
cv.Optional(CONF_MAX_TEMPERATURE, default=5.0): cv.float_range(min=5.0, max=75.0),
}
),
cv.Optional(CONF_CLIMATE_ZONE2): climate.climate_schema(PanasonicHeatpumpClimate)
.extend({
cv.Optional(CONF_MIN_TEMPERATURE, default=-5.0): cv.float_range(min=-5.0, max=20.0),
cv.Optional(CONF_MAX_TEMPERATURE, default=5.0): cv.float_range(min=5.0, max=75.0),
}
),
}
).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 climate.new_climate(child_config)
await cg.register_component(var, 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_cool_mode(config[CONF_COOL_MODE]))
cg.add(parent.add_climate(var))

View File

@ -0,0 +1,227 @@
#include "panasonic_heatpump_climate.h"
#include "esphome/core/log.h"
namespace esphome
{
namespace panasonic_heatpump
{
static const char *const TAG = "panasonic_heatpump.climate";
void PanasonicHeatpumpClimate::dump_config()
{
LOG_CLIMATE("", "Panasonic Heatpump Climate", this);
}
climate::ClimateTraits PanasonicHeatpumpClimate::traits()
{
auto traits = climate::ClimateTraits();
//traits.set_supports_action(true);
traits.set_supports_current_temperature(true);
if (this->cool_mode_ && (this->id_ == ClimateIds::CONF_CLIMATE_ZONE1 || this->id_ == ClimateIds::CONF_CLIMATE_ZONE2))
{
traits.set_supports_two_point_target_temperature(true);
this->supported_modes_.insert(climate::CLIMATE_MODE_COOL);
this->supported_modes_.insert(climate::CLIMATE_MODE_AUTO);
}
traits.set_supported_modes(this->supported_modes_);
traits.set_visual_min_temperature(this->min_temperature_);
traits.set_visual_max_temperature(this->max_temperature_);
traits.set_visual_temperature_step(this->temperature_step_);
return traits;
}
void PanasonicHeatpumpClimate::control(const climate::ClimateCall &call)
{
if (call.get_mode().has_value())
{
int byte6 = this->parent_->getResponseByte(6);
if (byte6 >= 0)
{
climate::ClimateMode new_mode = *call.get_mode();
uint8_t newByte6 = this->setClimateMode(new_mode, (uint8_t)byte6);
this->parent_->set_command_byte(newByte6, 6);
}
}
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;
case ClimateIds::CONF_CLIMATE_ZONE2:
this->parent_->set_command_byte(PanasonicCommand::setPlus128(new_temp), 40); // set7
break;
};
}
if (call.get_target_temperature_high().has_value())
{
float new_temp = *call.get_target_temperature_high();
switch (this->id_)
{
case ClimateIds::CONF_CLIMATE_ZONE1:
this->parent_->set_command_byte(PanasonicCommand::setPlus128(new_temp), 38); // set5
break;
case ClimateIds::CONF_CLIMATE_ZONE2:
this->parent_->set_command_byte(PanasonicCommand::setPlus128(new_temp), 40); // set7
break;
};
}
if (call.get_target_temperature_low().has_value())
{
float new_temp = *call.get_target_temperature_low();
switch (this->id_)
{
case ClimateIds::CONF_CLIMATE_ZONE1:
this->parent_->set_command_byte(PanasonicCommand::setPlus128(new_temp), 39); // set6
break;
case ClimateIds::CONF_CLIMATE_ZONE2:
this->parent_->set_command_byte(PanasonicCommand::setPlus128(new_temp), 41); // set8
break;
};
}
this->publish_state();
this->keep_state_ = 2;
}
void PanasonicHeatpumpClimate::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_high;
float new_target_temp_low;
float new_current_temp;
new_mode = this->getClimateMode(data[6]); // set9
switch (this->id_)
{
case ClimateIds::CONF_CLIMATE_TANK:
new_target_temp_high = PanasonicDecode::getByteMinus128(data[42]); // set11
new_current_temp = PanasonicDecode::getByteMinus128(data[141]); // top10
break;
case ClimateIds::CONF_CLIMATE_ZONE1:
new_target_temp_high = PanasonicDecode::getByteMinus128(data[38]); // set5
new_target_temp_low = PanasonicDecode::getByteMinus128(data[39]); // set6
new_current_temp = PanasonicDecode::getByteMinus128(data[139]); // top56
break;
case ClimateIds::CONF_CLIMATE_ZONE2:
new_target_temp_high = PanasonicDecode::getByteMinus128(data[40]); // set7
new_target_temp_low = PanasonicDecode::getByteMinus128(data[41]); // set8
new_current_temp = PanasonicDecode::getByteMinus128(data[140]); // top57
break;
default: return;
};
if (!this->get_traits().get_supports_two_point_target_temperature() &&
this->mode == new_mode &&
this->target_temperature == new_target_temp_high &&
this->current_temperature == new_current_temp) return;
if (this->get_traits().get_supports_two_point_target_temperature() &&
this->mode == new_mode &&
this->target_temperature_high == new_target_temp_high &&
this->target_temperature_low == new_target_temp_low &&
this->current_temperature == new_current_temp) return;
if (new_mode != 255) this->mode = (climate::ClimateMode) new_mode;
//this->action = climate::CLIMATE_ACTION_IDLE;
if (this->get_traits().get_supports_two_point_target_temperature())
{
this->target_temperature_high = new_target_temp_high;
this->target_temperature_low = new_target_temp_low;
}
else
{
this->target_temperature = new_target_temp_high;
}
this->current_temperature = new_current_temp;
this->publish_state();
}
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))
{
case 0b0001:
return climate::CLIMATE_MODE_OFF;
case 0b0010:
return climate::CLIMATE_MODE_HEAT;
case 0b0011:
return climate::CLIMATE_MODE_COOL;
case 0b1000:
return climate::CLIMATE_MODE_AUTO;
case 0b1001:
return climate::CLIMATE_MODE_AUTO; // 0x19 = auto-heat
case 0b1010:
return climate::CLIMATE_MODE_AUTO; // 0x1A = auto-cool
default: return 255;
};
default: return 255;
};
}
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;
switch (mode)
{
case climate::CLIMATE_MODE_OFF:
return newByte + 0b0001;
case climate::CLIMATE_MODE_HEAT:
return newByte + 0b0010;
case climate::CLIMATE_MODE_COOL:
return newByte + 0b0011;
case climate::CLIMATE_MODE_AUTO:
return newByte + 0b1000;
default: return 0;
};
default: return 0;
};
}
} // namespace panasonic_heatpump
} // namespace esphome

View File

@ -0,0 +1,45 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/climate/climate.h"
#include "../panasonic_heatpump.h"
#include "../decode.h"
#include "../commands.h"
namespace esphome
{
namespace panasonic_heatpump
{
enum ClimateIds : uint8_t
{
CONF_CLIMATE_TANK,
CONF_CLIMATE_ZONE1,
CONF_CLIMATE_ZONE2,
};
class PanasonicHeatpumpClimate : public climate::Climate, public Component,
public Parented<PanasonicHeatpumpComponent>, public PanasonicHeatpumpEntity
{
public:
PanasonicHeatpumpClimate() = default;
void dump_config() override;
climate::ClimateTraits traits() override;
void publish_new_state(const std::vector<uint8_t>& data) override;
void set_cool_mode(bool value) { this->cool_mode_ = value; }
void set_min_temperature(float value) { this->min_temperature_ = value; }
void set_max_temperature(float value) { this->max_temperature_ = value; }
protected:
void control(const climate::ClimateCall &call) override;
uint8_t getClimateMode(const uint8_t input);
uint8_t setClimateMode(const climate::ClimateMode mode, const uint8_t byte);
bool cool_mode_ { false };
float min_temperature_ { -5.0 };
float max_temperature_ { 5.0 };
float temperature_step_ { 0.5 };
std::set<climate::ClimateMode> supported_modes_ { climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT };
};
} // namespace panasonic_heatpump
} // namespace esphome

View File

@ -404,76 +404,42 @@ namespace esphome
this->publish_state(new_state);
}
bool PanasonicHeatpumpNumber::set_traits(std::map<std::string, int>& raw_topics)
bool PanasonicHeatpumpNumber::set_traits(std::map<std::string, int>& traits_settings)
{
if (raw_topics.empty()) return false;
if (traits_settings.empty()) return false;
switch (this->id_)
{
case NumberIds::CONF_SET5:
{
if (this->traits.get_min_value() != -5.0 && raw_topics["top76"] == 0)
case NumberIds::CONF_SET5: // Set Z1 Heat Request Temperature
case NumberIds::CONF_SET7: // Set Z2 Heat Request Temperature
if (this->traits.get_min_value() != -5.0 && traits_settings["heating_mode"] == 0)
{
this->traits.set_min_value(-5.0);
this->traits.set_max_value(5.0);
return true;
}
if (this->traits.get_min_value() != 20.0 && raw_topics["top76"] == 1)
if (this->traits.get_min_value() != 20.0 && traits_settings["heating_mode"] == 1)
{
this->traits.set_min_value(20.0);
this->traits.set_max_value(60.0);
return true;
}
break;
}
case NumberIds::CONF_SET6:
{
if (this->traits.get_min_value() != -5.0 && raw_topics["top81"] == 0)
case NumberIds::CONF_SET6: // Set Z1 Cool Request Temperature
case NumberIds::CONF_SET8: // Set Z2 Cool Request Temperature
if (this->traits.get_min_value() != -5.0 && traits_settings["cooling_mode"] == 0)
{
this->traits.set_min_value(-5.0);
this->traits.set_max_value(5.0);
return true;
}
if (this->traits.get_min_value() != 20.0 && raw_topics["top81"] == 1)
if (this->traits.get_min_value() != 20.0 && traits_settings["cooling_mode"] == 1)
{
this->traits.set_min_value(20.0);
this->traits.set_max_value(60.0);
return true;
}
break;
}
case NumberIds::CONF_SET7:
{
if (this->traits.get_min_value() != -5.0 && raw_topics["top76"] == 0)
{
this->traits.set_min_value(-5.0);
this->traits.set_max_value(5.0);
return true;
}
if (this->traits.get_min_value() != 20.0 && raw_topics["top76"] == 1)
{
this->traits.set_min_value(20.0);
this->traits.set_max_value(60.0);
return true;
}
break;
}
case NumberIds::CONF_SET8:
{
if (this->traits.get_min_value() != -5.0 && raw_topics["top81"] == 0)
{
this->traits.set_min_value(-5.0);
this->traits.set_max_value(5.0);
return true;
}
if (this->traits.get_min_value() != 20.0 && raw_topics["top81"] == 1)
{
this->traits.set_min_value(20.0);
this->traits.set_max_value(60.0);
return true;
}
break;
}
};
return false;

View File

@ -54,7 +54,7 @@ namespace esphome
PanasonicHeatpumpNumber() = default;
void dump_config() override;
void publish_new_state(const std::vector<uint8_t>& data) override;
bool set_traits(std::map<std::string, int>& raw_topics) override;
bool set_traits(std::map<std::string, int>& traits_settings) override;
protected:
void control(float value) override;

View File

@ -53,7 +53,7 @@ namespace esphome
{
for (auto *entity : this->numbers_)
{
this->traits_changed_ = entity->set_traits(this->raw_topics_) ? true : this->traits_changed_;
this->traits_changed_ = entity->set_traits(this->traits_settings_) ? true : this->traits_changed_;
}
this->loop_state_ = LoopState::SET_SELECT_TRAITS;
break;
@ -62,7 +62,7 @@ namespace esphome
{
for (auto *entity : this->selects_)
{
this->traits_changed_ = entity->set_traits(this->raw_topics_) ? true : this->traits_changed_;
this->traits_changed_ = entity->set_traits(this->traits_settings_) ? true : this->traits_changed_;
}
this->loop_state_ = LoopState::PUBLISH_SENSOR;
break;
@ -118,6 +118,15 @@ namespace esphome
{
entity->publish_new_state(this->heatpump_message_);
}
this->loop_state_ = LoopState::PUBLISH_CLIMATE;
break;
}
case LoopState::PUBLISH_CLIMATE:
{
for (auto *entity : this->climates_)
{
entity->publish_new_state(this->heatpump_message_);
}
this->loop_state_ = LoopState::SEND_REQUEST;
break;
}
@ -323,9 +332,9 @@ namespace esphome
this->last_response_count_ = this->current_response_count_;
// Save some topic values that are needed for setting traits
raw_topics_["top76"] = PanasonicDecode::getBit7and8(data[28]); // Heating Mode
raw_topics_["top81"] = PanasonicDecode::getBit5and6(data[28]); // Cooling Mode
raw_topics_["top120"] = PanasonicDecode::getBit5and6(data[23]); // Heat Cool Control
traits_settings_["heating_mode"] = PanasonicDecode::getBit7and8(data[28]); // top76
traits_settings_["cooling_mode"] = PanasonicDecode::getBit5and6(data[28]); // top81
traits_settings_["cool_mode_configured"] = this->cool_mode_;
return true;
}

View File

@ -28,6 +28,7 @@ namespace esphome
PUBLISH_NUMBER,
PUBLISH_SELECT,
PUBLISH_SWITCH,
PUBLISH_CLIMATE,
SEND_REQUEST,
READ_REQUEST,
RESTART_LOOP
@ -46,7 +47,7 @@ namespace esphome
public:
virtual void set_id(const int id) { id_ = id; }
virtual void publish_new_state(const std::vector<uint8_t>& data) = 0;
virtual bool set_traits(std::map<std::string, int>& raw_topics) { return false; }
virtual bool set_traits(std::map<std::string, int>& traits_settings) { return false; }
protected:
int id_ { -1 };
@ -67,7 +68,8 @@ namespace esphome
void loop() override;
// option functions
void set_uart_client(uart::UARTComponent* uart) { this->uart_client_ = uart; }
void set_log_uart_msg(bool enable) { this->log_uart_msg_ = enable; }
void set_log_uart_msg(bool active) { this->log_uart_msg_ = active; }
void set_cool_mode(bool active) { this->cool_mode_ = active; }
// uart message variables to use in lambda functions
int getResponseByte(const int index);
// command functions
@ -77,6 +79,7 @@ namespace esphome
void set_command_curve(const uint8_t value, const uint8_t index);
// entity functions
void add_binary_sensor(PanasonicHeatpumpEntity *binary_sensor) { binary_sensors_.push_back(binary_sensor); }
void add_climate(PanasonicHeatpumpEntity *climate) { climates_.push_back(climate); }
void add_number(PanasonicHeatpumpEntity *number) { numbers_.push_back(number); }
void add_select(PanasonicHeatpumpEntity *select) { selects_.push_back(select); }
void add_sensor(PanasonicHeatpumpEntity *sensor) { sensors_.push_back(sensor); }
@ -87,12 +90,13 @@ namespace esphome
// options variables
uart::UARTComponent* uart_client_ { nullptr };
bool log_uart_msg_ { false };
bool cool_mode_ { false };
// uart message variables
std::vector<uint8_t> heatpump_message_;
std::vector<uint8_t> response_message_;
std::vector<uint8_t> request_message_;
std::vector<uint8_t> command_message_;
std::map<std::string, int> raw_topics_;
std::map<std::string, int> traits_settings_;
uint8_t payload_length_;
uint8_t byte_;
uint8_t current_response_count_ { 0 };
@ -104,6 +108,7 @@ namespace esphome
uint8_t traits_update_counter_ { 0 };
// entity vectors
std::vector<PanasonicHeatpumpEntity *> binary_sensors_;
std::vector<PanasonicHeatpumpEntity *> climates_;
std::vector<PanasonicHeatpumpEntity *> numbers_;
std::vector<PanasonicHeatpumpEntity *> selects_;
std::vector<PanasonicHeatpumpEntity *> sensors_;

View File

@ -122,23 +122,23 @@ namespace esphome
this->publish_state(new_state);
}
bool PanasonicHeatpumpSelect::set_traits(std::map<std::string, int>& raw_topics)
bool PanasonicHeatpumpSelect::set_traits(std::map<std::string, int>& traits_settings)
{
if (raw_topics.empty()) return false;
if (traits_settings.empty()) return false;
switch (this->id_)
{
case SelectIds::CONF_SET9:
{
if (this->traits.get_options().size() != 3 && raw_topics["top120"] == 0)
if (this->traits.get_options().size() != 3 && traits_settings["cool_mode_configured"] == 0)
{
auto options = std::vector<std::string>(PanasonicDecode::OperationMode + 1, PanasonicDecode::OperationMode + 4);
this->traits.set_options(options);
return true;
}
if (this->traits.get_options().size() != 10 && raw_topics["top120"] != 0)
if (this->traits.get_options().size() != 7 && traits_settings["cool_mode_configured"] == 1)
{
auto options = std::vector<std::string>(PanasonicDecode::OperationMode + 1, std::end(PanasonicDecode::OperationMode));
auto options = std::vector<std::string>(PanasonicDecode::OperationMode + 1, PanasonicDecode::OperationMode + 8);
this->traits.set_options(options);
return true;
}

View File

@ -28,7 +28,7 @@ namespace esphome
PanasonicHeatpumpSelect() = default;
void dump_config() override;
void publish_new_state(const std::vector<uint8_t>& data) override;
bool set_traits(std::map<std::string, int>& raw_topics) override;
bool set_traits(std::map<std::string, int>& traits_settings) override;
protected:
void control(const std::string &value) override;