Initial commit: Hitachi RAR-6NE1 climate via ESP32/ESPHome

Reverse engineering del protocollo IR (HITACHI_AC 28 byte) del telecomando
RAR-6NE1 e custom component ESPHome bidirezionale (TX + RX) per Home Assistant.

- esphome/: custom component hitachi_rar6ne1 (climate_ir::ClimateIR) + config
- src/: firmware Arduino di cattura IR con web UI (strumento di diagnostica)
- README.md: documentazione completa (protocollo, decode, checksum, gotcha)
- Segreti esclusi dal versionamento (vedi *.example e .gitignore)
- Licenza GPL-3.0

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Francesco Zanin
2026-06-30 15:50:26 +02:00
commit 1a9eb82d0f
15 changed files with 1813 additions and 0 deletions

232
README.md Normal file
View File

@@ -0,0 +1,232 @@
# Hitachi RAR-6NE1 — Reverse Engineering protocollo IR per ESPHome
## Obiettivo
Decodificare il protocollo IR del telecomando Hitachi **RAR-6NE1** (compatibile con unità interne RAK-15QPE/18RPE/25RPE/35RPE/42RPE/50RPE) per realizzare un climate control via ESP32 + ESPHome.
## Stato attuale
> **2026-06-30 — Implementazione ESPHome realizzata e funzionante in bidirezionale.**
> Il condizionatore si comanda da Home Assistant (TX) e lo stato si sincronizza
> quando si usa il telecomando fisico (RX). Vedi la sezione **[Implementazione
> ESPHome (fase finale)](#implementazione-esphome-fase-finale)** in fondo per
> architettura, file e i "gotcha" risolti.
Reverse engineering **sostanzialmente completo** sul lato protocollo. Mappatura solida di tutti i campi principali (power, mode, temp, fan, swing, silent, powerful) e **algoritmo di checksum determinato e verificato al 100%** — coincide esattamente con `IRHitachiAc::calcChecksum()` della libreria IRremoteESP8266: il telecomando RAR-6NE1 è quindi compatibile byte-per-byte e bit-per-bit con quella classe, non solo strutturalmente. Possiamo generare comandi custom validi da zero.
L'ipotesi "frame periodico di temperatura ambiente dal telecomando" (sensore IR lato trasmettitore) è stata **verificata e scartata**: con la web UI di monitoraggio (vedi sotto) si è osservato un periodo di **~80 minuti di silenzio totale a telecomando fermo, con unità accesa**, e tutti i frame ricevuti sono risultati essere comandi da pressione tasto. Il telecomando **non invia trasmissioni spontanee periodiche**.
L'implementazione finale ESPHome **non** ha riusato la classe `IRHitachiAc`: è un
custom component che estende `climate_ir::ClimateIR` e costruisce i 28 byte +
checksum a mano (più portabile, nessuna dipendenza da IRremoteESP8266). Dettagli
nella sezione *Implementazione ESPHome (fase finale)* in fondo.
## File in questa cartella
| File | Scopo |
|---|---|
| `platformio.ini` + `src/main.cpp` | Progetto PlatformIO. Lo sketch cattura i segnali IR (ricevitore su **GPIO27**, ESP32 DevKitC) e li **logga in una web UI** accessibile in WiFi, con timestamp NTP, decodifica a 28 byte on-device e verifica checksum. Pensato per lasciare l'ESP vicino al condizionatore e osservare i frame nel tempo (vedi sezione "Monitor web"). Logga anche su seriale (formato `IRrecvDumpV2`) se collegato via USB. |
| `src/secrets.h` | Credenziali WiFi + hostname mDNS + timezone per lo sketch PlatformIO (formato C, da compilare). **Distinto da `secrets.yaml`.** |
| `hitachi-ir-dump.yaml` | Config ESPHome alternativa per dump raw via `remote_receiver` (`dump: all`). **Nota**: ESPHome non ha un decoder Hitachi generico in `remote_base` (il decode Hitachi esiste solo dentro i climate component `hitachi_ac344`/`hitachi_ac424`), quindi questo file produce solo raw non interpretato — usare il progetto PlatformIO per il riconoscimento automatico del protocollo. |
| `secrets.yaml` | Credenziali WiFi per la config ESPHome (placeholder, da compilare). |
## Monitor web (osservazione frame nel tempo)
Lo sketch `src/main.cpp` espone una web UI per registrare ogni trama IR ricevuta — utile per lasciare l'ESP32 vicino al condizionatore e verificare, ad esempio, se il telecomando (che ha un sensore di temperatura integrato) invia frame periodici "spontanei" all'unità.
**Setup:**
1. Compila `src/secrets.h` con SSID/password della rete WiFi (la stessa del PC da cui interroghi l'endpoint).
2. `pio run -t upload` con ESP collegato via USB, poi sposta l'ESP vicino al condizionatore (alimentato).
3. Apri `http://hitachi-ir.local/` (mDNS) o `http://<IP>/` (l'IP è stampato sul seriale al boot).
**Endpoint:**
| Endpoint | Risposta |
|---|---|
| `GET /` | Pagina HTML con tabella auto-refresh (ogni 3s): orario, Δ dal frame precedente, bits, rawlen, checksum OK/BAD, campi decodificati (power/mode/temp/fan/swing/silent/powerful/tasto) e i 28 byte. |
| `GET /api/captures` | JSON riassuntivo di tutte le catture nel ring buffer (ultime 30) + uptime/totale/IP. Per polling automatico. |
| `GET /api/raw?i=N` | Array raw in microsecondi della cattura con sequenza `N` (per ri-analizzare frame anomali che non decodificano a 28 byte). |
| `GET /api/clear` | Svuota il buffer. |
Il ring buffer tiene le ultime 30 catture in RAM (si perde al reset). Ogni cattura conserva anche il raw grezzo (fino a 460 valori) così i frame di lunghezza/struttura inattesa non vanno persi.
## Perché non è un protocollo "già supportato"
- Verificato su esphome.io: i soli climate component Hitachi nativi sono `hitachi_ac344` e `hitachi_ac424`.
- Verificato su `ir_Hitachi.h` (libreria IRremoteESP8266): i modelli di telecomando documentati per ciascuna variante (AC1, AC2, AC3, AC264, AC296, AC344, AC424) **non includono RAR-6NE1** né le sue varianti (RAR-6N1/6N2/6N3/6NE4/6N5).
- Empiricamente: lo sketch `IRrecvDumpV2`, che prova il decode con tutti i protocolli noti, riconosce solo *alcune* trame (es. lo stato OFF) come `HITACHI_AC` valido; la maggior parte risulta `UNKNOWN`.
## Metodo di decode (validato)
Le trame, una volta corrette per un bug di cattura ricorrente, risultano essere il protocollo **`HITACHI_AC` standard a 28 byte (224 bit)** — la classe più "base" della libreria (distinta da AC264/296/344/424) — con codifica a distanza:
- Header: mark ~3300-3400µs, space ~1700µs
- Bit: mark costante ~400µs, space `526µs`→bit `0`, `1296µs`→bit `1`
- 28 byte totali, MSB-first per byte, ordine byte diretto (nessuna inversione)
### Bug di cattura da correggere sempre
Molti dump (in particolare il primo dopo un periodo di idle) **perdono il mark iniziale lungo dell'header** (~3300-3400µs), registrando come primo valore solo la sua space (~1700µs). Risultato: l'array raw appare "shiftato" di una posizione.
**Come riconoscerlo:** guarda il primo valore del raw array.
- Se è **> 3000µs** → header completo, **scarta i primi 2 valori** (mark+space header) prima di accoppiare il resto.
- Se è **< 2000µs** (tipicamente ~1650-1720) → header tagliato, **scarta solo il primo valore**.
Poi accoppia i valori restanti `(mark, space)`: bit = `1` se `space > 800`, altrimenti `0`. Bit-pack MSB-first in byte, 28 byte totali.
Questo metodo è stato calibrato e validato contro una trama riconosciuta nativamente come `HITACHI_AC` dalla libreria (stato OFF, con checksum verificato dalla libreria stessa) — non è una supposizione.
## Mappatura campi (stato a 28 byte, indice da 0)
| Campo | Byte | Valori osservati | Note |
|---|---|---|---|
| Power | `[17]` bit0 | `0x00`=OFF, `0x01`=ON | Confermato, coincide col layout documentato in `ir_Hitachi.h` |
| Mode | `[10]` | Cool=`0x20`, Fan-only=`0x30`, Auto=`0x40`, Dry=`0xA0`, Heat=`0xC0` | Confermato con test isolato (solo mode cambiato) |
| Temp | `[11]` | 26°C=`0x2C`, 25°C=`0x4C` | Azzerato a `0x01` in Fan-only (temp non applicabile) |
| Fan speed | `[13]` | `0x40`=1/4, `0xC0`=2/4, `0x20`=3/4, `0xA0`=4/4, `0x80`=Auto | Confermato con test isolato (sequenza 3→2→1→Auto→4→3). Dry forza automaticamente `0xC0`. |
| Checksum | `[27]` | Cambia ad ogni trasmissione | **Algoritmo determinato e verificato su tutti i 18 stati raccolti finora (match perfetto, nessuna eccezione)** — è esattamente `IRHitachiAc::calcChecksum()` della libreria IRremoteESP8266 (`ir_Hitachi.cpp:173-178`). Vedi sezione dedicata sotto. |
| Swing Verticale | `[14]` | `0xE0`=ON, `0x60`=OFF | Confermato con test isolato (sequenza V+H→solo H→OFF→solo V→V+H) |
| Swing Orizzontale | `[15]` | `0xE0`=ON, `0x60`=OFF | Confermato con lo stesso test |
| Silent | `[26]` bit4 (`0x10`) | `0x10`=ON, `0x00`=OFF | Confermato con test isolato ON→OFF |
| Powerful | `[25]` bit2 (`0x04`) | `0x34`=ON, `0x30`=OFF | Confermato con test isolato ON→OFF |
| `[9]` | — | `0xE0` (idle/solo mode), `0xC0` (tasto power on/off), `0xB0` (tasto swing V), `0x70` (tasto swing H), `0x30` (tasto fan speed), `0x38` (tasto Silent), `0xA8` (tasto Powerful) | **Non è uno stato persistente**: codifica il *tasto/categoria appena premuto* sul telecomando, non il risultato — resta identico tra ON e OFF dello stesso tasto (es. `0x38` sia per Silent ON che OFF, `0xC0` sia per power ON che OFF). Confermato su 15+ trasmissioni isolate (swing, fan, silent, powerful, power). |
## Algoritmo di checksum (confermato)
Coincide esattamente con la classe `IRHitachiAc` standard della libreria IRremoteESP8266 (`ir_Hitachi.cpp`, righe 173-178). Verificato in Python su tutti gli 18 stati raccolti (baseline mode/temp/power, swing, fan speed) — match perfetto al 100%, nessuna eccezione:
```python
def reverse_bits(x, nbits=8):
r = 0
for i in range(nbits):
r |= ((x >> i) & 1) << (nbits - 1 - i)
return r
def calc_checksum(state): # state = 28 byte, l'ultimo e' il checksum stesso
s = 62
for b in state[:-1]: # primi 27 byte
s = (s - reverse_bits(b, 8)) & 0xFF
return reverse_bits(s, 8)
```
Implicazione: possiamo generare comandi custom mai catturati (es. combinazioni nuove di mode/temp/fan/swing) calcolando il checksum con questa formula, senza dover fare solo replay di raw catturati. Probabile che la classe `IRHitachiAc` esistente nella libreria possa essere riusata direttamente (o con un wrapper minimale) per l'implementazione ESPHome finale, anziché scrivere un protocollo custom da zero — da verificare.
### Stati di riferimento catturati (esadecimale, 28 byte)
```
OFF : 80 08 0C 02 FD 80 7F 88 48 C0 20 2C 00 20 E0 E0 00 00 00 00 00 00 00 01 80 30 00 40
ON (26C cool, fan 3/4): 80 08 0C 02 FD 80 7F 88 48 C0 20 2C 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 41
Cool 25°C : 80 08 0C 02 FD 80 7F 88 48 90 20 4C 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 7E
Fan only : 80 08 0C 02 FD 80 7F 88 48 E0 30 01 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 54
Auto : 80 08 0C 02 FD 80 7F 88 48 E0 40 2C 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 01
Heat : 80 08 0C 02 FD 80 7F 88 48 E0 C0 2C 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 FE
Dry : 80 08 0C 02 FD 80 7F 88 48 E0 A0 2C 00 C0 E0 E0 00 01 00 00 00 00 00 01 80 30 00 7E
Cool (return): 80 08 0C 02 FD 80 7F 88 48 E0 20 2C 00 20 E0 E0 00 01 00 00 00 00 00 01 80 30 00 7E
Fan 2/4 : 80 08 0C 02 FD 80 7F 88 48 30 20 2C 00 C0 E0 E0 00 01 00 00 00 00 00 01 80 30 00 5E
```
## Come riprodurre/continuare il lavoro
1. **Hardware**: ESP32 DevKitC + ricevitore IR (TSOP38xx/VS1838B) su GPIO27, 3.3V.
2. **Cattura**: `pio run -t upload -t monitor` da questa cartella (richiede PlatformIO). Puntare il telecomando e premere un tasto.
3. **Decode manuale**: usare il metodo sopra descritto (vedi anche script Python di calibrazione usati in sessione, non salvati in questa cartella — ricostruibili facilmente dal metodo descritto).
4. **Verifica checksum**: aprire `ir_Hitachi.cpp` su github.com/crankyoldgit/IRremoteESP8266 (classe `IRHitachiAc`, funzione `calcChecksum()`) e confrontare con gli stati di riferimento sopra.
## Prossimi passi
1. ~~**Implementazione ESPHome finale**~~**FATTA** (2026-06-30) — custom component
`climate_ir`, vedi sezione *Implementazione ESPHome (fase finale)*. Resta solo lo
sweep interattivo completo dei comandi in TX da confermare sul campo.
2. (Opzionale) Test temperatura impostata su più valori per confermare la formula `tempC = reverseBits(byte[11]) / 2` su tutto il range (1632°C), finora derivata da 25/26°C.
### Verificato e chiuso
- **Frame spontanei / sensore di temperatura del telecomando**: ipotesi **scartata**. Monitoraggio via web UI con ~80 min di silenzio totale a telecomando fermo (unità accesa); tutti i frame ricevuti sono comandi da pressione tasto. Il RAR-6NE1 non trasmette periodicamente.
- **byte[9]**: confermato = codice del tasto premuto (non stato persistente).
## Riferimenti
- https://esphome.io/components/climate/climate_ir — protocolli IR climate supportati da ESPHome
- https://github.com/crankyoldgit/IRremoteESP8266 — `src/ir_Hitachi.h` / `ir_Hitachi.cpp`
- Nota progetto su Obsidian: `projects/hitachi-ir-esphome.md`
---
## Implementazione ESPHome (fase finale)
Realizzata il **2026-06-30**. Sostituisce il monitor Arduino (`src/main.cpp`, che
resta come strumento di cattura/diagnostica) con un'integrazione ESPHome nativa che
espone il condizionatore come entità `climate` in Home Assistant, **bidirezionale**.
### Architettura
Custom **external component** che estende `climate_ir::ClimateIR`. Non riusa la
classe `IRHitachiAc` della libreria: costruisce i 28 byte e il checksum a mano
(stesso algoritmo verificato) ed emette/decodifica via i componenti nativi ESPHome
`remote_transmitter`/`remote_receiver` (RMT). Vantaggi: nessuna dipendenza esterna,
entità climate completa, TX **e** RX (sync col telecomando fisico).
### File (`esphome/`)
```
esphome/
├── hitachi-ir.yaml # config: wifi/api/ota + TX GPIO25 + RX GPIO27 + climate
├── secrets.yaml # template wifi
└── components/hitachi_rar6ne1/
├── __init__.py · climate.py # registrazione platform (API ESPHome 2025+/2026.x)
├── hitachi_rar6ne1.h # classe, costanti timing, mappa fan/swing/preset
└── hitachi_rar6ne1.cpp # build 28 byte + checksum + transmit_state + on_receive
```
Importazione: `hitachi-ir.yaml` e la cartella `components/` nella stessa directory di
config ESPHome (es. `/config/`), poi build/flash dall'add-on ESPHome di HA (primo
flash USB, poi OTA).
### Hardware
- **RX**: ricevitore IR (TSOP/VS1838B) su **GPIO27** (invertito, pull-up).
- **TX**: LED IR + driver a transistor su **GPIO25**`GPIO25 →[1k]→ base NPN`;
`3V3 →[100R]→ LED IR(A→K) → collector`; `emitter → GND`. Portante 38 kHz.
### Mappatura campi → 28 byte (in `build_state_`)
Identica alla tabella sopra: mode `[10]`, temp `[11]`=`reverseBits(°C×2)`, fan `[13]`
(Quiet/Low/Med/High→1/2/3/4 + Auto), swing V `[14]`/H `[15]` (`0xE0`/`0x60`), power
`[17]` bit0, preset **Eco→Silent** `[26]` bit4, **Boost→Powerful** `[25]` bit2,
checksum `[27]`. `byte[9]` (codice tasto) impostato a `0xE0` via la costante
`kButtonCode` — non critico (vedi gotcha).
### Gotcha risolti (utili per progetti AC simili su ESPHome/ESP32)
1. **API `climate_ir` dipende dalla versione.** Su 2024.12.x serviva la costante
`CLIMATE_IR_WITH_RECEIVER_SCHEMA` + `register_climate_ir`; dalla 2025.x in poi
gli helper a funzione `climate_ir_with_receiver_schema(cls)` + `new_climate_ir`.
Il `climate.py` attuale usa la forma moderna (testato su **2026.6.3**).
2. **`external_components: path` deve puntare alla cartella *contenitore***
(`components`), non alla cartella del componente (`components/hitachi_rar6ne1`):
altrimenti `climate.py` viene scambiato per il core `climate`*import circolare*
(`partially initialized module 'esphome.components.climate'`).
3. **RX troncato.** Il frame Hitachi è 224 bit ≈ 226 simboli RMT, ma l'ESP32
classico ne cattura di default ~192 → frame tagliato, decode fallito (nessun
`RX ok`). Fix: `rmt_symbols: 384` + `receive_symbols: 384` sul `remote_receiver`
(su questa versione di ESPHome; il vecchio `memory_blocks` non esiste più).
4. **`byte[9]` non è critico.** L'unità accetta il frame full-state a prescindere:
in TX un OFF con `byte[9]=0xC0` ha spento il condizionatore; in RX il telecomando
reale ha inviato `byte[9]=0x10` (= il default di `IRHitachiAc::stateReset`).
5. Tenere `dump: all` **spento** in esercizio: con frame lunghi blocca il loop
~200 ms (`remote_receiver took a long time`). Riattivare solo per debug raw.
### Stato di verifica (2026-06-30)
-**TX**: comando OFF dall'ESP → condizionatore spento (emettitore e accettazione
frame provati sul campo).
-**RX**: telecomando fisico → frame completo decodificato, checksum valido,
entità `climate` in HA aggiornata (mode/temp/fan/swing letti correttamente).
- ✅ Build/validazione su ESPHome 2026.6.3; fix RMT e path confermati dai log.
-**Da confermare sul campo**: sweep completo dei comandi *dalla card HA* in TX
(tutti i modi, range temp, tutte le fan, swing V/H/Both, preset Eco/Boost). Il
protocollo è interamente validato, quindi atteso funzionante; manca solo la prova
esaustiva interattiva.
### Edge case & robustezza (2026-06-30, sera)
- **Range temperatura** dell'entità ristretto a **1830 °C** (`kMinTempC`/`kMaxTempC`
in `hitachi_rar6ne1.h`), scelta conservativa. ⚑ *Flag aperto*: il range realmente
supportato dal RAR-6NE1 è da verificare agli estremi (16/32) — se accettati si può
riallargare.
- **Preset ↔ ventola accoppiati** (override `control()` in `hitachi_rar6ne1.cpp`):
come il telecomando reale, attivando Eco/Silent la ventola va al minimo (Quiet) e
Boost/Powerful al massimo (High); cambiando la ventola si esce dal preset. Evita
stati incoerenti in HA (es. Boost + Quiet).
- **Setpoint in modalità Auto**: confermato **assoluto** come in Cool (nessuna
modifica necessaria).
- **OFF**: confermato funzionante in **ogni** modo (`byte[9]=0xE0`).
-**In sospeso**: semantica della temperatura in **Dry**`byte[11]` reale da
catturare dal telecomando (per ora si invia il setpoint come in Cool).