Projet de reverse-engineering du protocole RS485 d'une pompe à chaleur Poolex Jetline Selection 90 (fonctionne aussi sur la 95).
Le Wemos D1 Mini écoute passivement le bus RS485 entre la carte mère (PAC) et la télécommande filaire, décode les trames, et publie les valeurs sur MQTT pour Home Assistant.
✅ Fonctionnel — en production depuis mai 2026.
Ce qui fonctionne :
- Lecture des températures : eau entrée/sortie, air ambiant, serpentin (coil), gaz
- Lecture de l'état : PAC ON/OFF, chauffage actif (compresseur)
- Lecture de la consigne (setpoint) envoyée par la télécommande
- Lecture de la consigne confirmée par la PAC
- Publication MQTT sur changement + republication périodique (60s)
- Mise à jour OTA (Over The Air)
- Debug à distance via Telnet (RemoteDebug)
Ce qui reste à faire :
- Envoi de commandes à la PAC (consigne température, ON/OFF)
- Décodage complet de la partie "état machine" des trames longues
- Lecture des codes erreur
Le firmware complet tient dans un seul fichier : src/main.cpp.
┌─────────────┐ RS485 ┌──────────┐ MQTT ┌────────────────┐
│ PAC │◄──────────────►│ Wemos D1 │──────────────►│ Home Assistant │
│ Poolex │ (9600 8N1) │ (sniffer)│ │ │
└─────────────┘ └──────────┘ └────────────────┘
▲ ▲
│ RS485 │
└─────────────────────────────┘
Télécommande filaire
Topologie du bus : trois appareils partagent le même bus RS485 half-duplex :
- La carte mère PAC
- La télécommande filaire d'origine
- Le Wemos D1 Mini (sniffer passif — ne transmet jamais)
Le Wemos est câblé en parallèle avec la télécommande sur le même faisceau.
- Vitesse : 9600 baud, 8N1
- Cycle : ~190 ms (2 heartbeats par seconde)
- Séparateur inter-trames : octets
0x01(idle bus) - CPU cadencé à 160 MHz pour traitement temps réel
| Direction | En-tête | Taille | Description |
|---|---|---|---|
| remote → PAC | XX 04 FF ... |
12 B | Heartbeat télécommande (constant, ~98 ms) |
| PAC → remote | FD XX ... |
6–13 B | ACK PAC |
| PAC → remote | [flags] <état> <capteurs> |
25–36 B | Trame capteurs + état |
Les 9 derniers octets de chaque burst long (≥ 25 B) contiennent les capteurs, terminés par FF FF :
water_in_temp = ((0xFF - data[0]) >> 3) & 0x3F; // °C
air_ambient_temp = ((0xFF - data[1]) >> 1) & 0x3F; // °C
coil_temp = ((0xFF - data[2]) >> 1) & 0x3F; // °C
gas_temp = ((0xFF - data[3]) >> 1) & 0x3F; // °C
water_out_temp = ((0xFF - data[4]) >> 1) & 0x3F; // °C
// data[5] = 0xFF (constant)
active_status = ((0xFF - data[6]) >> 4) & 0x01; // 1 = PAC ON
// data[7-8] = FF FF (terminaison)La validation se fait par plage de température (5–45 °C) — pas d'en-tête fixe.
La consigne est extraite de deux sources :
- Télécommande (heartbeat 12B) : byte offset 9 →
setpoint = ((0xFF - byte) >> 1) & 0x3F - PAC (trame d'état longue ≥15B) : byte offset 2 (ou 3 si préfixe FF) →
setpoint_confirmed = ((0xFF - byte) >> 3) & 0x3F
Le dernier byte des trames d'état longues (≥15B avant le sensor tail) encode le mode :
| Byte | Mode | Valeur MQTT |
|---|---|---|
0x6C |
Chauffage seul | heat |
0x64 |
Chauffage (variante) | heat |
0x54 |
Automatique (chaud + froid) | auto |
0x5C |
Refroidissement seul | cool |
⚠️ Ces valeurs sont issues de l'analyse d'une capture.0x64apparaît lors de transitions ou après un power cycle — il est provisoirement mappé surheat.
Sur ESP8266, SoftwareSerial désactive TOUTES les interruptions pendant la réception de chaque byte (~1ms par byte à 9600 baud). Le stack WiFi (qui partage le même CPU) ne peut pas traiter les paquets entrants → ping timeout, OTA impossible, telnet inaccessible.
Paradoxalement, les publications MQTT sortantes fonctionnent (elles sont mises en file d'attente), mais tout ce qui nécessite une réponse entrante est bloqué.
-
Lecture non-bloquante : on ne lit que
PS.available()bytes, jamais dereadBytes()bloquant avec timeout. -
Mini-throttle : on attend au minimum 5 ms entre deux lectures (ou 20+ bytes disponibles) pour accumuler des bytes au lieu de lire un par un.
-
Fenêtre WiFi pendant l'idle bus : quand le bus est inactif (entre les cycles de trames), on coupe brièvement le RX SoftwareSerial (3 ms) pour laisser le stack WiFi traiter les paquets entrants.
if (bus_is_idle && (now - last_wifi_window >= 50)) {
PS.enableRx(false);
delay(3); // fenêtre WiFi
PS.enableRx(true);
}Le flag bus_is_idle passe à true quand :
- Le buffer résiduel est vide (tous les bursts ont été traités)
- Il ne reste que des octets
0x01(séparateurs) - Aucune nouvelle donnée depuis > 100 ms (timeout résiduel)
- Wemos D1 Mini (ESP8266)
- MAX485 — interface RS485
- Convertisseur 12V → 5V (min 2A)
- Couverture WiFi à l'emplacement de la PAC
- Boîtier IP67
Le câble de la PAC a 4 fils :
- Vert + Jaune : bus RS485 → A et B du MAX485
- Noir + Rouge : alimentation 12V
| Broche MAX485 | GPIO Wemos | Pin Wemos | Fonction |
|---|---|---|---|
| RO (sortie) | GPIO4 | D2 | RX données |
| DI (entrée) | GPIO5 | D1 | TX données |
| DE + RE | GPIO14 | D5 | Contrôle direction |
DE/RE sont reliés ensemble et maintenus à LOW en permanence (mode réception only).
- Premier flash : via USB de l'ordinateur
- En fonctionnement : alimentation 12V de la PAC → convertisseur 12V/5V → Wemos (pin 5V) + MAX485 (VCC)
- PlatformIO (VSCode ou CLI)
# Copier le fichier de secrets
cp src/secrets.h.sample include/secrets.h
# Éditer avec vos paramètres WiFi et MQTTRemplir dans include/secrets.h :
WIFI_SSIDWIFI_PASSWORDMQTT_BROKER_ADDR
# Compiler
pio run
# Flash via USB (premier flash)
pio run -t upload
# Flash via OTA (après le premier flash)
pio run -t upload --upload-port 10.0.0.72telnet <IP_DU_WEMOS>Utiliser les commandes RemoteDebug pour changer le niveau de log (verbose, debug, info, warning, error).
| Topic | Description | Unité |
|---|---|---|
poolheater/values/water_in_temp |
Température eau entrée | °C |
poolheater/values/water_out_temp |
Température eau sortie | °C |
poolheater/values/air_ambient_temp |
Température air ambiant | °C |
poolheater/values/coil_temp |
Température serpentin | °C |
poolheater/values/gaz_temp |
Température gaz | °C |
poolheater/values/active_status |
PAC allumée | 0/1 |
poolheater/values/heating |
Compresseur actif | 0/1 |
poolheater/values/setpoint |
Consigne demandée (télécommande) | °C |
poolheater/values/setpoint_confirmed |
Consigne confirmée (PAC) | °C |
poolheater/values/mode |
Mode de fonctionnement | heat / auto / cool |
poolheater/values/error |
Code erreur PAC ( |
none |
poolheater/values/state_raw |
Trame d'état brute (hex) | Pour analyse |
poolheater/status |
État connexion (retained) | ON |
| Topic | Description |
|---|---|
poolheater/command/probe |
Déclenche un test d'émission TX (expérimental) |
- Publication sur changement de valeur (pas de flood)
- Republication complète toutes les 60 secondes pour garantir la fraîcheur dans HA
- Reconnexion MQTT throttlée à 5 secondes entre les tentatives
Dans votre configuration.yaml :
mqtt:
sensor:
- name: "Pool water temperature"
state_topic: "poolheater/values/water_in_temp"
force_update: true
unit_of_measurement: "°C"
unique_id: "pool_hp_water_in_temp"
device_class: "temperature"
- name: "Pool water OUT temperature"
state_topic: "poolheater/values/water_out_temp"
force_update: true
unit_of_measurement: "°C"
unique_id: "pool_hp_water_out_temp"
device_class: "temperature"
- name: "Heat pump status"
state_topic: "poolheater/values/active_status"
force_update: true
unique_id: "pool_hp_status"
- name: "Heat pump heating"
state_topic: "poolheater/values/heating"
force_update: true
unique_id: "pool_hp_heating"
- name: "Heat pump ambient air temperature"
state_topic: "poolheater/values/air_ambient_temp"
force_update: true
unit_of_measurement: "°C"
unique_id: "pool_hp_air_temp"
device_class: "temperature"
- name: "Heat pump gas temperature"
state_topic: "poolheater/values/gaz_temp"
force_update: true
unit_of_measurement: "°C"
unique_id: "pool_hp_gas_temp"
device_class: "temperature"
- name: "Heat pump coil temperature"
state_topic: "poolheater/values/coil_temp"
force_update: true
unit_of_measurement: "°C"
unique_id: "pool_hp_coil_temp"
device_class: "temperature"
- name: "Heat pump setpoint"
state_topic: "poolheater/values/setpoint"
force_update: true
unit_of_measurement: "°C"
unique_id: "pool_hp_setpoint"
device_class: "temperature"
- name: "Heat pump setpoint confirmed"
state_topic: "poolheater/values/setpoint_confirmed"
force_update: true
unit_of_measurement: "°C"
unique_id: "pool_hp_setpoint_confirmed"
device_class: "temperature"
- name: "Heat pump mode"
state_topic: "poolheater/values/mode"
force_update: true
unique_id: "pool_hp_mode"
- name: "Heat pump error"
state_topic: "poolheater/values/error"
force_update: true
unique_id: "pool_hp_error"
icon: "mdi:alert-circle"| Valeur MQTT | Mode | Description |
|---|---|---|
heat |
Chauffage seul | Mode 2 — la PAC chauffe uniquement |
auto |
Automatique | Mode 1 — chauffage + refroidissement |
cool |
Refroidissement seul | Mode 0 — la PAC refroidit uniquement |
Pour visualiser l'efficience de la PAC dans Home Assistant, on utilise des template sensors qui calculent :
- Delta T : différence entre water_out et water_in (°C)
- Puissance thermique : P = débit × ΔT × 1.163 (kW)
- COP estimé : basé sur la courbe constructeur Poolex Jetline 90 en fonction de la température air
Ajouter dans configuration.yaml :
# --- Input number pour le débit PAC ---
input_number:
pool_hp_flow_rate:
name: "Débit PAC (m³/h)"
min: 1
max: 6
step: 0.1
initial: 3
unit_of_measurement: "m³/h"
icon: mdi:water-pump
# --- Template sensors pour l'efficience ---
template:
- sensor:
- name: "Pool HP Delta T"
unique_id: "pool_hp_delta_t"
unit_of_measurement: "°C"
device_class: "temperature"
state: >
{% set t_out = states('sensor.pool_water_out_tempetature') | float(0) %}
{% set t_in = states('sensor.pool_water_tempetature') | float(0) %}
{% if t_out > 0 and t_in > 0 %}
{{ (t_out - t_in) | round(1) }}
{% else %}
unknown
{% endif %}
- name: "Pool HP Puissance thermique"
unique_id: "pool_hp_thermal_power"
unit_of_measurement: "kW"
device_class: "power"
state: >
{% set t_out = states('sensor.pool_water_out_tempetature') | float(0) %}
{% set t_in = states('sensor.pool_water_tempetature') | float(0) %}
{% set flow = states('input_number.pool_hp_flow_rate') | float(3) %}
{% set delta = t_out - t_in %}
{% if t_out > 0 and t_in > 0 and delta >= 0 %}
{{ (flow * delta * 1.163) | round(2) }}
{% else %}
0
{% endif %}
- name: "Pool HP COP estimé"
unique_id: "pool_hp_cop_estimated"
unit_of_measurement: "COP"
icon: mdi:lightning-bolt-circle
state: >
{# COP estimé Poolex Jetline 90 selon température air #}
{# Courbe constructeur approximée : COP ≈ 3.0 + (T_air - 15) × 0.15 #}
{# Plafonné entre 2.5 et 7.0 #}
{% set t_air = states('sensor.heat_pump_ambient_air_temperature') | float(0) %}
{% set heating = states('sensor.heat_pump_status') | int(0) %}
{% if heating == 1 and t_air > 0 %}
{% set cop = 3.0 + (t_air - 15) * 0.15 %}
{{ [2.5, [cop, 7.0] | min] | max | round(1) }}
{% else %}
unknown
{% endif %}
- name: "Pool HP Puissance électrique estimée"
unique_id: "pool_hp_electrical_power"
unit_of_measurement: "kW"
device_class: "power"
state: >
{% set thermal = states('sensor.pool_hp_puissance_thermique') | float(0) %}
{% set cop = states('sensor.pool_hp_cop_estime') | float(0) %}
{% if cop > 0 and thermal > 0 %}
{{ (thermal / cop) | round(2) }}
{% else %}
0
{% endif %}La courbe COP est approximée à partir des données constructeur de la Jetline Selection 90 :
| Temp. air | COP estimé | Puissance thermique |
|---|---|---|
| 10°C | ~2.3 | ~5.5 kW |
| 15°C | ~3.0 | ~7.0 kW |
| 20°C | ~3.8 | ~8.5 kW |
| 26°C | ~4.6 | ~9.5 kW |
| 30°C | ~5.3 | ~10 kW |
⚠️ Le COP réel dépend aussi de la température de l'eau et de l'humidité. Pour un COP précis, il faudrait un compteur électrique (type Shelly 1PM) sur l'alimentation de la PAC.
💡 Débit PAC : Ajustez
input_number.pool_hp_flow_rateselon votre installation. La Poolex Jetline 90 recommande 1.5–3.5 m³/h à travers l'échangeur (pas le débit total de la pompe de filtration).
Le répertoire src/frame_logs/ contient des captures brutes du bus UART pour le reverse-engineering :
- Captures avec télécommande + PAC (trames 12-36 B)
motherboard_only.txt: PAC seule sans télécommande (trames 93 B commençant par0x0C)
- Point de départ : esp8266_poolstar
- Discussion protocole : issue Cyril
- Version Python
- Thread Modbus Jeedom
- Reverse Hayward : Forum Arduino
- Reverse Poolex Dreamline : Repository
Les pull requests sont les bienvenues. Pour les changements majeurs, ouvrez d'abord une issue.
