feat(gree-ir): wlasny komponent ESPHome dla klimy Gree

Natywne komponenty gree/coolix nie dzialaly - zla suma kontrolna.
Protokol zreverse-engineerowany z 22 ramek IR nagranych z pilota:
- bajt0 = tryb+power: cool 0x19, heat 0x4C, fan 0x1B, off 0x44
- bajt1 = temperatura jako (temp-16)
- checksum = (lo(b0..b3)+hi(b4..b6)+0x0A)&0xF w gornym nibblu b7
- timingi: hdr 9030/-4460, mark 677, space0 553, space1 1630, gap 19960

Termostat Off/Cool/Heat/Fan, zakres 16-30C, HW: Wemos D1 Mini + Grove IR.
This commit is contained in:
oskar 2026-07-22 16:35:47 +02:00
parent 423f871486
commit f1dbdf514d
5 changed files with 124 additions and 11 deletions

View file

@ -0,0 +1,5 @@
# Gitignore settings for ESPHome
# This is an example and may include too much for your use-case.
# You can modify this file to suit your needs.
/.esphome/
/secrets.yaml

View file

@ -0,0 +1,21 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import climate, remote_transmitter
CONF_TRANSMITTER_ID = "transmitter_id"
gree_ir_ns = cg.esphome_ns.namespace("gree_ir")
GreeIR = gree_ir_ns.class_("GreeIR", climate.Climate, cg.Component)
CONFIG_SCHEMA = climate.climate_schema(GreeIR).extend({
cv.Required(CONF_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
}).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = await climate.new_climate(config)
await cg.register_component(var, config)
tx = await cg.get_variable(config[CONF_TRANSMITTER_ID])
cg.add(var.set_transmitter(tx))

View file

@ -0,0 +1,85 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/remote_transmitter/remote_transmitter.h"
namespace esphome {
namespace gree_ir {
class GreeIR : public climate::Climate, public Component {
public:
void set_transmitter(remote_transmitter::RemoteTransmitterComponent *tx) {
this->transmitter_ = tx;
}
void setup() override {
this->mode = climate::CLIMATE_MODE_OFF;
this->target_temperature = 24;
this->publish_state();
}
climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits();
traits.set_supported_modes({
climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_COOL,
climate::CLIMATE_MODE_HEAT,
climate::CLIMATE_MODE_FAN_ONLY,
});
traits.set_visual_min_temperature(16);
traits.set_visual_max_temperature(30);
traits.set_visual_temperature_step(1);
return traits;
}
void control(const climate::ClimateCall &call) override {
if (call.get_mode().has_value())
this->mode = *call.get_mode();
if (call.get_target_temperature().has_value())
this->target_temperature = *call.get_target_temperature();
this->transmit_state_();
this->publish_state();
}
protected:
remote_transmitter::RemoteTransmitterComponent *transmitter_{nullptr};
void transmit_state_() {
uint8_t b[8] = {0, 0, 0, 0, 0, 0, 0, 0};
uint8_t mode_byte, b2, b1_0;
switch (this->mode) {
case climate::CLIMATE_MODE_COOL: mode_byte = 0x19; b2 = 0x60; b1_0 = 0x06; break;
case climate::CLIMATE_MODE_HEAT: mode_byte = 0x4C; b2 = 0x40; b1_0 = 0x01; break;
case climate::CLIMATE_MODE_FAN_ONLY: mode_byte = 0x1B; b2 = 0x60; b1_0 = 0x02; break;
default: mode_byte = 0x44; b2 = 0x00; b1_0 = 0x01; break;
}
int temp = (int) this->target_temperature;
if (temp < 16) temp = 16;
if (temp > 30) temp = 30;
uint8_t t = (this->mode == climate::CLIMATE_MODE_OFF) ? 0x0A : (uint8_t)(temp - 16);
b[0] = mode_byte; b[1] = t; b[2] = b2; b[3] = 0x50;
b[4] = b1_0; b[5] = 0x00; b[6] = 0x00;
uint8_t cs = ((b[0] & 0xF) + (b[1] & 0xF) + (b[2] & 0xF) + (b[3] & 0xF) +
((b[4] >> 4) & 0xF) + ((b[5] >> 4) & 0xF) + ((b[6] >> 4) & 0xF) + 0x0A) & 0xF;
b[7] = (cs << 4) & 0xF0;
auto transmit = this->transmitter_->transmit();
auto *data = transmit.get_data();
data->set_carrier_frequency(38000);
const int MK = 677, S0 = 553, S1 = 1630, GAP = 19960;
data->mark(9030); data->space(4460);
for (int by = 0; by < 4; by++)
for (int bit = 0; bit < 8; bit++) { data->mark(MK); data->space(((b[by] >> bit) & 1) ? S1 : S0); }
const int foot[3] = {0, 1, 0};
for (int i = 0; i < 3; i++) { data->mark(MK); data->space(foot[i] ? S1 : S0); }
data->mark(MK); data->space(GAP);
for (int by = 4; by < 8; by++)
for (int bit = 0; bit < 8; bit++) { data->mark(MK); data->space(((b[by] >> bit) & 1) ? S1 : S0); }
data->mark(MK);
transmit.perform();
}
};
} // namespace gree_ir
} // namespace

View file

@ -1,31 +1,33 @@
esphome: esphome:
name: gree-ir-blaster name: gree-ir-blaster
friendly_name: Gree IR Blaster friendly_name: Gree IR Blaster
esp8266: esp8266:
board: d1_mini board: d1_mini
external_components:
- source:
type: local
path: components
wifi: wifi:
ssid: !secret wifi_ssid ssid: !secret wifi_ssid
password: !secret wifi_password password: !secret wifi_password
ap: ap:
ssid: "Gree-IR Fallback" ssid: "Gree-IR Fallback"
password: !secret fallback_password password: !secret fallback_password
logger: logger:
api: api:
ota: ota:
platform: esphome platform: esphome
remote_transmitter: remote_transmitter:
id: tx
pin: D5 pin: D5
carrier_duty_percent: 50% carrier_duty_percent: 50%
remote_receiver:
pin:
number: D6
inverted: true
mode:
input: true
pullup: true
dump: all
climate: climate:
- platform: gree - platform: gree_ir
name: "Klimatyzacja" name: "Klimatyzacja"
id: gree_ac transmitter_id: tx
model: yan