Merge branch 'dev' into cc1101-modulation-names-progmem

This commit is contained in:
J. Nick Koston
2026-10-13 19:16:58 -10:00
committed by GitHub
9 changed files with 125 additions and 85 deletions
+1
View File
@@ -47,6 +47,7 @@ CanMode = esp32_can_ns.enum("CanMode")
CAN_MODES = {
"NORMAL": CanMode.CAN_MODE_NORMAL,
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
"NO_ACK": CanMode.CAN_MODE_NO_ACK,
}
# Currently the driver only supports a subset of the bit rates defined in canbus
+5 -2
View File
@@ -79,10 +79,13 @@ bool ESP32Can::setup_internal() {
}
// Select TWAI mode based on configuration
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
twai_mode_t twai_mode = TWAI_MODE_NORMAL;
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
twai_mode = TWAI_MODE_LISTEN_ONLY;
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
} else if (this->mode_ == CAN_MODE_NO_ACK) {
twai_mode = TWAI_MODE_NO_ACK;
ESP_LOGI(TAG, "CAN bus configured in NO_ACK mode (transmit without requiring ACK)");
}
twai_general_config_t g_config =
+1
View File
@@ -12,6 +12,7 @@ namespace esphome::esp32_can {
enum CanMode : uint8_t {
CAN_MODE_NORMAL = 0,
CAN_MODE_LISTEN_ONLY = 1,
CAN_MODE_NO_ACK = 2,
};
class ESP32Can final : public canbus::Canbus {
+2 -2
View File
@@ -126,8 +126,8 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config: ConfigType) -> None:
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_protocol(config[CONF_PROTOCOL]))
tag = heatpumpir_ns.ProtocolTag.template(config[CONF_PROTOCOL])
var = await climate_ir.new_climate_ir(config, tag())
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
+68 -64
View File
@@ -5,7 +5,6 @@
#if defined(USE_ARDUINO) || defined(USE_ESP32)
#include <cmath>
#include <map>
#include <IRSender.h>
#include <HeatpumpIRFactory.h>
#include "esphome/components/remote_base/remote_base.h"
@@ -45,71 +44,76 @@ class IRSenderESPHome : public IRSender {
ESPHOME_LOG_TAG(TAG, "heatpumpir.climate");
const std::map<Protocol, std::function<HeatpumpIR *()>> PROTOCOL_CONSTRUCTOR_MAP = {
{PROTOCOL_AUX, []() { return new AUXHeatpumpIR(); }}, // NOLINT
{PROTOCOL_BALLU, []() { return new BalluHeatpumpIR(); }}, // NOLINT
{PROTOCOL_CARRIER_MCA, []() { return new CarrierMCAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_CARRIER_NQV, []() { return new CarrierNQVHeatpumpIR(); }}, // NOLINT
{PROTOCOL_DAIKIN_ARC417, []() { return new DaikinHeatpumpARC417IR(); }}, // NOLINT
{PROTOCOL_DAIKIN_ARC480, []() { return new DaikinHeatpumpARC480A14IR(); }}, // NOLINT
{PROTOCOL_DAIKIN, []() { return new DaikinHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ELECTROLUXYAL, []() { return new ElectroluxYALHeatpumpIR(); }}, // NOLINT
{PROTOCOL_FUEGO, []() { return new FuegoHeatpumpIR(); }}, // NOLINT
{PROTOCOL_FUJITSU_AWYZ, []() { return new FujitsuHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREE, []() { return new GreeGenericHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAA, []() { return new GreeYAAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAN, []() { return new GreeYANHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAC, []() { return new GreeYACHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYT, []() { return new GreeYTHeatpumpIR(); }}, // NOLINT
{PROTOCOL_GREEYAP, []() { return new GreeYAPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HISENSE_AUD, []() { return new HisenseHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HITACHI, []() { return new HitachiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_HYUNDAI, []() { return new HyundaiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_IVT, []() { return new IVTHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MIDEA, []() { return new MideaHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FA, []() { return new MitsubishiFAHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FD, []() { return new MitsubishiFDHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_FE, []() { return new MitsubishiFEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_FDTC, []() { return new MitsubishiHeavyFDTCHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZJ, []() { return new MitsubishiHeavyZJHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZM, []() { return new MitsubishiHeavyZMHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_HEAVY_ZMP, []() { return new MitsubishiHeavyZMPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_KJ, []() { return new MitsubishiKJHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_MSC, []() { return new MitsubishiMSCHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_MSY, []() { return new MitsubishiMSYHeatpumpIR(); }}, // NOLINT
{PROTOCOL_MITSUBISHI_SEZ, []() { return new MitsubishiSEZKDXXHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_CKP, []() { return new PanasonicCKPHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_DKE, []() { return new PanasonicDKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_EKE, []() { return new PanasonicEKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_JKE, []() { return new PanasonicJKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_LKE, []() { return new PanasonicLKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_NKE, []() { return new PanasonicNKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_AQV, []() { return new SamsungAQVHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_FJM, []() { return new SamsungFJMHeatpumpIR(); }}, // NOLINT
{PROTOCOL_SHARP, []() { return new SharpHeatpumpIR(); }}, // NOLINT
{PROTOCOL_TOSHIBA_DAISEIKAI, []() { return new ToshibaDaiseikaiHeatpumpIR(); }}, // NOLINT
{PROTOCOL_TOSHIBA, []() { return new ToshibaHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ZHLT01, []() { return new ZHLT01HeatpumpIR(); }}, // NOLINT
{PROTOCOL_NIBE, []() { return new NibeHeatpumpIR(); }}, // NOLINT
{PROTOCOL_QLIMA_1, []() { return new Qlima1HeatpumpIR(); }}, // NOLINT
{PROTOCOL_QLIMA_2, []() { return new Qlima2HeatpumpIR(); }}, // NOLINT
{PROTOCOL_SAMSUNG_AQV12MSAN, []() { return new SamsungAQV12MSANHeatpumpIR(); }}, // NOLINT
{PROTOCOL_ZHJG01, []() { return new ZHJG01HeatpumpIR(); }}, // NOLINT
{PROTOCOL_AIRWAY, []() { return new AIRWAYHeatpumpIR(); }}, // NOLINT
{PROTOCOL_BGH_AUD, []() { return new BGHHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PANASONIC_ALTDKE, []() { return new PanasonicAltDKEHeatpumpIR(); }}, // NOLINT
{PROTOCOL_PHILCO_PHS32, []() { return new PhilcoPHS32HeatpumpIR(); }}, // NOLINT
{PROTOCOL_VAILLANTVAI8, []() { return new VaillantHeatpumpIR(); }}, // NOLINT
{PROTOCOL_R51M, []() { return new R51MHeatpumpIR(); }}, // NOLINT
};
template<Protocol P> struct ProtocolClass;
template<Protocol P> HeatpumpIR *create_heatpump_ir() {
return new typename ProtocolClass<P>::type(); // NOLINT
}
// Codegen calls only the configured protocols, so the linker drops the other classes
#define HEATPUMPIR_PROTOCOL(protocol, cls) \
template<> struct ProtocolClass<protocol> { using type = cls; }; \
template HeatpumpIR *create_heatpump_ir<protocol>();
HEATPUMPIR_PROTOCOL(PROTOCOL_AUX, AUXHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_BALLU, BalluHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_MCA, CarrierMCAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_CARRIER_NQV, CarrierNQVHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC417, DaikinHeatpumpARC417IR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN_ARC480, DaikinHeatpumpARC480A14IR)
HEATPUMPIR_PROTOCOL(PROTOCOL_DAIKIN, DaikinHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ELECTROLUXYAL, ElectroluxYALHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_FUEGO, FuegoHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_FUJITSU_AWYZ, FujitsuHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREE, GreeGenericHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAA, GreeYAAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAN, GreeYANHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAC, GreeYACHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYT, GreeYTHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_GREEYAP, GreeYAPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HISENSE_AUD, HisenseHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HITACHI, HitachiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_HYUNDAI, HyundaiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_IVT, IVTHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MIDEA, MideaHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FA, MitsubishiFAHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FD, MitsubishiFDHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_FE, MitsubishiFEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_FDTC, MitsubishiHeavyFDTCHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZJ, MitsubishiHeavyZJHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZM, MitsubishiHeavyZMHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_HEAVY_ZMP, MitsubishiHeavyZMPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_KJ, MitsubishiKJHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSC, MitsubishiMSCHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_MSY, MitsubishiMSYHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_MITSUBISHI_SEZ, MitsubishiSEZKDXXHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_CKP, PanasonicCKPHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_DKE, PanasonicDKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_EKE, PanasonicEKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_JKE, PanasonicJKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_LKE, PanasonicLKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_NKE, PanasonicNKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV, SamsungAQVHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_FJM, SamsungFJMHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SHARP, SharpHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA_DAISEIKAI, ToshibaDaiseikaiHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_TOSHIBA, ToshibaHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHLT01, ZHLT01HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_NIBE, NibeHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_1, Qlima1HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_QLIMA_2, Qlima2HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_SAMSUNG_AQV12MSAN, SamsungAQV12MSANHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_ZHJG01, ZHJG01HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_AIRWAY, AIRWAYHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_BGH_AUD, BGHHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PANASONIC_ALTDKE, PanasonicAltDKEHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_PHILCO_PHS32, PhilcoPHS32HeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_VAILLANTVAI8, VaillantHeatpumpIR)
HEATPUMPIR_PROTOCOL(PROTOCOL_R51M, R51MHeatpumpIR)
#undef HEATPUMPIR_PROTOCOL
void HeatpumpIRClimate::setup() {
auto protocol_constructor = PROTOCOL_CONSTRUCTOR_MAP.find(protocol_);
if (protocol_constructor == PROTOCOL_CONSTRUCTOR_MAP.end()) {
ESP_LOGE(TAG, "Invalid protocol");
return;
}
this->heatpump_ir_ = protocol_constructor->second();
climate_ir::ClimateIR::setup();
if (this->sensor_) {
this->sensor_->add_on_state_callback([this](float state) {
+20 -12
View File
@@ -89,25 +89,21 @@ enum VerticalDirection {
VERTICAL_DIRECTION_DOWN = 5,
};
/// Creates the HeatpumpIR library class for a protocol; instantiated in heatpumpir.cpp.
template<Protocol P> HeatpumpIR *create_heatpump_ir();
/// Selects the protocol at compile time so the enum and the library object always match.
template<Protocol P> struct ProtocolTag {};
// Temperature
const float TEMP_MIN = 0; // Celsius
const float TEMP_MAX = 100; // Celsius
class HeatpumpIRClimate final : public climate_ir::ClimateIR {
public:
HeatpumpIRClimate()
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
climate::CLIMATE_FAN_AUTO},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
template<Protocol P>
explicit HeatpumpIRClimate(ProtocolTag<P> /*tag*/) : HeatpumpIRClimate(P, create_heatpump_ir<P>()) {}
void setup() override;
void set_protocol(Protocol protocol) {
this->protocol_ = protocol;
if (this->has_fan_byte_presets_()) {
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
}
}
void set_horizontal_default(HorizontalDirection horizontal_direction) {
this->default_horizontal_direction_ = horizontal_direction;
}
@@ -119,6 +115,18 @@ class HeatpumpIRClimate final : public climate_ir::ClimateIR {
void set_min_temperature(float temperature) { this->min_temperature_ = temperature; }
protected:
HeatpumpIRClimate(Protocol protocol, HeatpumpIR *heatpump_ir)
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
climate::CLIMATE_FAN_AUTO},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}),
heatpump_ir_(heatpump_ir),
protocol_(protocol) {
if (this->has_fan_byte_presets_()) {
this->presets_ = {climate::CLIMATE_PRESET_NONE, climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_BOOST};
}
}
HeatpumpIR *heatpump_ir_;
void control(const climate::ClimateCall &call) override;
/// Transmit via IR the state of this climate controller.
+10 -4
View File
@@ -1120,8 +1120,12 @@ def _load_yaml_internal(fname: Path, *, track_document_range: bool = True) -> An
res = parse_yaml(fname, f_handle, track_document_range=track_document_range)
except (UnicodeDecodeError, OSError) as err:
raise EsphomeError(f"Error reading file {fname}: {err}") from err
# Top-level !include returns a deferred IncludeFile; resolve it so
# callers always receive the final content.
return res
def _resolve_include(res: Any) -> Any:
# A document that is just `!include` parses to a deferred IncludeFile; resolve it so
# every parse_yaml caller, including the VS Code and ACE loaders, gets the final content.
if isinstance(res, IncludeFile):
res = res.load() if res.should_load() else {}
return res
@@ -1147,7 +1151,7 @@ def parse_yaml(
# tracked mode; reject the combination instead of half-applying it.
raise ValueError("track_document_range=False requires the default yaml_loader")
try:
return _load_yaml_internal_with_type(
res = _load_yaml_internal_with_type(
ESPHomeLoader,
file_name,
file_handle,
@@ -1159,13 +1163,15 @@ def parse_yaml(
# readable exceptions
# Rewind the stream so we can try again
file_handle.seek(0, 0)
return _load_yaml_internal_with_type(
res = _load_yaml_internal_with_type(
ESPHomePurePythonLoader,
file_name,
file_handle,
yaml_loader,
track_document_range=track_document_range,
)
# Resolved after the fallback so a failed first parse never loads the include twice
return _resolve_include(res)
def _load_yaml_internal_with_type(
+1 -1
View File
@@ -18,7 +18,7 @@ canbus:
tx_pin: ${tx_pin}
can_id: 4
bit_rate: 50kbps
mode: NORMAL
mode: NO_ACK
on_frame:
- can_id: 500
then:
+17
View File
@@ -954,6 +954,23 @@ def test_yaml_merge_chain_include_resolves() -> None:
assert config["base"]["existing"] == "value"
def test_parse_yaml_custom_loader_resolves_root_include() -> None:
"""A document that is only `!include` is resolved for a custom loader too."""
parent = Path("/fake/main.yaml")
loader_calls: list[Path] = []
def custom_loader(fname: Path) -> dict[str, int]:
loader_calls.append(fname)
return {"x": 1}
config = yaml_util.parse_yaml(
parent, io.StringIO("!include inner.yaml\n"), custom_loader
)
assert config == {"x": 1}
assert loader_calls == [Path("/fake/inner.yaml")]
def test_yaml_merge_chain_include_depth_exceeded() -> None:
"""Chain includes in merge keys exceeding depth limit raise a clear error."""
parent = Path("/fake/main.yaml")