mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 02:02:51 +00:00
[heatpumpir] Add ZMP IR receiver (#17099)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick@koston.org> Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
co-authored by
pre-commit-ci-lite[bot]
J. Nick Koston
J. Nick Koston
parent
e92d239996
commit
af76f19ab4
@@ -1,5 +1,7 @@
|
||||
#include "heatpumpir.h"
|
||||
|
||||
#include "receiver_mitsubishi_heavy.h"
|
||||
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include <cmath>
|
||||
@@ -246,6 +248,23 @@ void HeatpumpIRClimate::transmit_state() {
|
||||
swing_h_cmd);
|
||||
}
|
||||
|
||||
bool HeatpumpIRClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
switch (this->protocol_) {
|
||||
case PROTOCOL_MITSUBISHI_HEAVY_ZMP: {
|
||||
uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH];
|
||||
if (!decode_mitsubishi_heavy_frame(data, frame, *this))
|
||||
return false;
|
||||
decode_mitsubishi_heavy_zmp(frame, *this);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false; // no decoder for this protocol
|
||||
}
|
||||
this->target_temperature = clamp(this->target_temperature, this->min_temperature_, this->max_temperature_);
|
||||
this->publish_state();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::heatpumpir
|
||||
|
||||
#endif
|
||||
|
||||
@@ -102,7 +102,14 @@ class HeatpumpIRClimate final : public climate_ir::ClimateIR {
|
||||
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||
void setup() override;
|
||||
void set_protocol(Protocol protocol) { this->protocol_ = protocol; }
|
||||
void set_protocol(Protocol protocol) {
|
||||
this->protocol_ = protocol;
|
||||
if (protocol == PROTOCOL_MITSUBISHI_HEAVY_ZMP) {
|
||||
this->presets_.insert(climate::CLIMATE_PRESET_NONE);
|
||||
this->presets_.insert(climate::CLIMATE_PRESET_ECO);
|
||||
this->presets_.insert(climate::CLIMATE_PRESET_BOOST);
|
||||
}
|
||||
}
|
||||
void set_horizontal_default(HorizontalDirection horizontal_direction) {
|
||||
this->default_horizontal_direction_ = horizontal_direction;
|
||||
}
|
||||
@@ -117,6 +124,8 @@ class HeatpumpIRClimate final : public climate_ir::ClimateIR {
|
||||
HeatpumpIR *heatpump_ir_;
|
||||
/// Transmit via IR the state of this climate controller.
|
||||
void transmit_state() override;
|
||||
/// Handle received IR data from the remote control.
|
||||
bool on_receive(remote_base::RemoteReceiveData data) override;
|
||||
Protocol protocol_;
|
||||
HorizontalDirection default_horizontal_direction_;
|
||||
VerticalDirection default_vertical_direction_;
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "receiver_mitsubishi_heavy.h"
|
||||
#include "heatpumpir.h"
|
||||
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include <HeatpumpIRFactory.h>
|
||||
|
||||
namespace esphome::heatpumpir {
|
||||
|
||||
static constexpr uint8_t FRAME_PREFIX[5] = {0x52, 0xAE, 0xC3, 0x26, 0xD9};
|
||||
static constexpr uint8_t XOR_CHECKSUM_EXPECTED = 0xFF;
|
||||
static constexpr uint8_t PREFIX_LENGTH = 5;
|
||||
static constexpr uint8_t MODE_TEMP_BYTE = 9;
|
||||
static constexpr uint8_t MODE_MASK = 0x07;
|
||||
static constexpr uint8_t TEMP_MASK = 0x0F;
|
||||
static constexpr uint8_t TEMPERATURE_BASE = 17;
|
||||
static constexpr uint8_t TEMPERATURE_SHIFT = 4;
|
||||
|
||||
static bool read_raw_bytes(remote_base::RemoteReceiveData &data, uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH]) {
|
||||
if (!data.expect_item(MITSUBISHI_HEAVY_HDR_MARK, MITSUBISHI_HEAVY_HDR_SPACE))
|
||||
return false;
|
||||
|
||||
for (uint8_t i = 0; i < MITSUBISHI_HEAVY_FRAME_LENGTH; i++) {
|
||||
uint8_t byte = 0;
|
||||
for (uint8_t bit = 0; bit < 8; bit++) {
|
||||
if (data.expect_item(MITSUBISHI_HEAVY_BIT_MARK, MITSUBISHI_HEAVY_ONE_SPACE)) {
|
||||
byte |= 1 << bit;
|
||||
} else if (!data.expect_item(MITSUBISHI_HEAVY_BIT_MARK, MITSUBISHI_HEAVY_ZERO_SPACE)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
frame[i] = byte;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool validate_prefix(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH]) {
|
||||
for (uint8_t i = 0; i < PREFIX_LENGTH; i++) {
|
||||
if (frame[i] != FRAME_PREFIX[i])
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool validate_checksums(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH]) {
|
||||
for (uint8_t i = PREFIX_LENGTH; i < MITSUBISHI_HEAVY_FRAME_LENGTH; i += 2) {
|
||||
if ((uint8_t) (frame[i] ^ frame[i + 1]) != XOR_CHECKSUM_EXPECTED)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool decode_mode_field(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH], climate::ClimateMode &mode) {
|
||||
if (frame[MODE_TEMP_BYTE] & MITSUBISHI_HEAVY_MODE_OFF) {
|
||||
mode = climate::CLIMATE_MODE_OFF;
|
||||
return true;
|
||||
}
|
||||
switch (frame[MODE_TEMP_BYTE] & MODE_MASK) {
|
||||
case MITSUBISHI_HEAVY_MODE_AUTO:
|
||||
mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
return true;
|
||||
case MITSUBISHI_HEAVY_MODE_HEAT:
|
||||
mode = climate::CLIMATE_MODE_HEAT;
|
||||
return true;
|
||||
case MITSUBISHI_HEAVY_MODE_COOL:
|
||||
mode = climate::CLIMATE_MODE_COOL;
|
||||
return true;
|
||||
case MITSUBISHI_HEAVY_MODE_DRY:
|
||||
mode = climate::CLIMATE_MODE_DRY;
|
||||
return true;
|
||||
case MITSUBISHI_HEAVY_MODE_FAN:
|
||||
mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static float decode_temperature_field(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH]) {
|
||||
uint8_t temp_nibble = frame[MODE_TEMP_BYTE] >> TEMPERATURE_SHIFT;
|
||||
uint8_t inverted_nibble = ~temp_nibble;
|
||||
uint8_t temp_offset = inverted_nibble & TEMP_MASK;
|
||||
return TEMPERATURE_BASE + temp_offset;
|
||||
}
|
||||
|
||||
static bool apply_climate_state(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH], HeatpumpIRClimate &climate) {
|
||||
climate::ClimateMode mode;
|
||||
if (!decode_mode_field(frame, mode))
|
||||
return false;
|
||||
climate.mode = mode;
|
||||
climate.target_temperature = decode_temperature_field(frame);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool decode_mitsubishi_heavy_frame(remote_base::RemoteReceiveData &data, uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH],
|
||||
HeatpumpIRClimate &climate) {
|
||||
if (!read_raw_bytes(data, frame))
|
||||
return false;
|
||||
if (!validate_prefix(frame))
|
||||
return false;
|
||||
if (!validate_checksums(frame))
|
||||
return false;
|
||||
return apply_climate_state(frame, climate);
|
||||
}
|
||||
|
||||
} // namespace esphome::heatpumpir
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/remote_base/remote_base.h"
|
||||
|
||||
namespace esphome::heatpumpir {
|
||||
|
||||
class HeatpumpIRClimate;
|
||||
|
||||
/// Bytes in a Mitsubishi Heavy IR frame, prefix included
|
||||
inline constexpr uint8_t MITSUBISHI_HEAVY_FRAME_LENGTH = 11;
|
||||
|
||||
/// Read a Mitsubishi Heavy frame into `frame`, check its prefix and inverted byte pairs, and apply the
|
||||
/// fields every variant shares (mode and target temperature). Returns false when no valid frame was read.
|
||||
bool decode_mitsubishi_heavy_frame(remote_base::RemoteReceiveData &data, uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH],
|
||||
HeatpumpIRClimate &climate);
|
||||
|
||||
/// Apply the ZMP-specific fields (fan, preset, swing) of a validated frame.
|
||||
void decode_mitsubishi_heavy_zmp(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH], HeatpumpIRClimate &climate);
|
||||
|
||||
} // namespace esphome::heatpumpir
|
||||
@@ -0,0 +1,63 @@
|
||||
#include "receiver_mitsubishi_heavy.h"
|
||||
#include "heatpumpir.h"
|
||||
|
||||
#if defined(USE_ARDUINO) || defined(USE_ESP32)
|
||||
|
||||
#include <HeatpumpIRFactory.h>
|
||||
|
||||
namespace esphome::heatpumpir {
|
||||
|
||||
// Fan speed sits in bits 7:5 of byte 7; the vertical swing code spans bit 1 of byte 5 and bits 4:3 of byte 7
|
||||
static constexpr uint8_t BYTE_SWING_H = 5;
|
||||
static constexpr uint8_t BYTE_FAN = 7;
|
||||
static constexpr uint8_t FAN_SPEED_MASK = 0xE0;
|
||||
static constexpr uint8_t VS_MASK_BYTE5 = 0x02;
|
||||
static constexpr uint8_t VS_MASK_BYTE7 = 0x18;
|
||||
// The library has no horizontal swing for ZMP and always sends MITSUBISHI_HEAVY_ZMP_HS_STOP (0xCC) in byte 5.
|
||||
// Captures of the remote show 0x5C while it swings; the mask leaves out the vertical swing and clean bits.
|
||||
static constexpr uint8_t HORIZONTAL_SWING_MASK = 0xDC;
|
||||
static constexpr uint8_t HORIZONTAL_SWING_ACTIVE = 0x5C;
|
||||
|
||||
void decode_mitsubishi_heavy_zmp(const uint8_t frame[MITSUBISHI_HEAVY_FRAME_LENGTH], HeatpumpIRClimate &climate) {
|
||||
uint8_t fan = frame[BYTE_FAN] & FAN_SPEED_MASK;
|
||||
climate.preset = climate::CLIMATE_PRESET_NONE;
|
||||
switch (fan) {
|
||||
case MITSUBISHI_HEAVY_ZMP_FAN_AUTO:
|
||||
climate.fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
case MITSUBISHI_HEAVY_ZMP_FAN1:
|
||||
climate.fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
case MITSUBISHI_HEAVY_ZMP_FAN2:
|
||||
climate.fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
break;
|
||||
case MITSUBISHI_HEAVY_ZMP_FAN3:
|
||||
climate.fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
case MITSUBISHI_HEAVY_ZMP_HIPOWER:
|
||||
climate.preset = climate::CLIMATE_PRESET_BOOST;
|
||||
break; // the remote keeps the fan mode and restores it when the preset ends
|
||||
case MITSUBISHI_HEAVY_ZMP_ECONO:
|
||||
climate.preset = climate::CLIMATE_PRESET_ECO;
|
||||
break;
|
||||
}
|
||||
|
||||
uint8_t swing_h = frame[BYTE_SWING_H] & HORIZONTAL_SWING_MASK;
|
||||
uint8_t swing_v = (frame[BYTE_SWING_H] & VS_MASK_BYTE5) | (frame[BYTE_FAN] & VS_MASK_BYTE7);
|
||||
bool h_swing = (swing_h == HORIZONTAL_SWING_ACTIVE);
|
||||
bool v_swing = (swing_v == MITSUBISHI_HEAVY_ZMP_VS_SWING);
|
||||
|
||||
if (h_swing && v_swing) {
|
||||
climate.swing_mode = climate::CLIMATE_SWING_BOTH;
|
||||
} else if (h_swing) {
|
||||
climate.swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
||||
} else if (v_swing) {
|
||||
climate.swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||
} else {
|
||||
climate.swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::heatpumpir
|
||||
|
||||
#endif
|
||||
@@ -7,6 +7,15 @@ climate:
|
||||
min_temperature: 18
|
||||
max_temperature: 30
|
||||
transmitter_id: xmitr
|
||||
- platform: heatpumpir
|
||||
protocol: mitsubishi_heavy_zmp
|
||||
horizontal_default: left
|
||||
vertical_default: up
|
||||
name: HeatpumpIR Climate Mitsubishi ZMP
|
||||
min_temperature: 18
|
||||
max_temperature: 30
|
||||
transmitter_id: xmitr
|
||||
receiver_id: rcvr
|
||||
- platform: heatpumpir
|
||||
protocol: daikin
|
||||
horizontal_default: mleft
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/bk72xx-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/bk72xx-ard.yaml
|
||||
heatpumpir: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-ard.yaml
|
||||
heatpumpir: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml
|
||||
heatpumpir: !include common.yaml
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
packages:
|
||||
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml
|
||||
heatpumpir: !include common.yaml
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
# Common remote_receiver configuration for BK72XX Arduino tests
|
||||
# Provides a shared remote receiver that all components can use
|
||||
# Components will auto-use this receiver if they don't specify receiver_id
|
||||
|
||||
substitutions:
|
||||
remote_receiver_pin: GPIO17
|
||||
|
||||
remote_receiver:
|
||||
id: rcvr
|
||||
pin: ${remote_receiver_pin}
|
||||
dump: all
|
||||
tolerance: 25%
|
||||
Reference in New Issue
Block a user