Le Pattern Bridge : du Legacy au Moderne¶
Ce document explique comment le backend Go (essensys-server-backend) comble l'obsolescence du client materiel BP_MQX_ETH en implementant un Anti-Corruption Layer (ACL) — une couche de traduction qui isole le domaine moderne du protocole legacy.
1. Le Probleme¶
Le firmware BP_MQX_ETH communique via un protocole HTTP non-standard, des JSON malformes, et un modele de donnees opaque (tableau d'octets). Aucun client moderne (navigateur, IA, Home Assistant) ne peut dialoguer directement avec le materiel.
❌ Incompatible directement
┌──────────────┐ ┌──────────────┐
│ React SPA │ ────── JSON standard ─────── ✗ ──→ │ BP_MQX_ETH │
│ Home Asst. │ ────── MQTT ──────────────── ✗ ──→ │ (Coldfire) │
│ OpenClaw IA │ ────── MCP/SSE ───────────── ✗ ──→ │ │
└──────────────┘ └──────────────┘
2. L'Anti-Corruption Layer¶
Le backend Go se positionne comme traducteur bilateral :
┌──────────────┐ ┌───────────────────┐ ┌──────────────┐
│ React SPA │ │ Backend Go │ │ BP_MQX_ETH │
│ Home Asst. │ ◄─────► │ (ACL Bridge) │ ◄────► │ (Coldfire) │
│ OpenClaw IA │ │ │ │ │
└──────────────┘ └───────────────────┘ └──────────────┘
API REST standard Traduction + Buffer Protocole legacy
JSON RFC 8259 Normalisation JSON JSON malformes
WebSocket/SSE Single-packet TCP Polling 2s
MQTT Discovery Queue Redis Port 80 fixe
MCP Protocol Bloc complet 605-622 Table d'echange brute
3. Les Composants du Bridge¶
3.1 Normalisation JSON (internal/api/json_normalizer.go)¶
Le firmware envoie du JSON invalide. Le normalizer corrige les trames avant le parsing Go :
Entree firmware : {version:"1.0",ek:[{k:613,v:"1"},{k:607,v:"0"}]}
↓
Normalizer Go : {"version":"1.0","ek":[{"k":613,"v":"1"},{"k":607,"v":"0"}]}
↓
json.Unmarshal : OK ✓
Transformations effectuees : - Ajout de guillemets autour des cles non-quotees - Preservation des valeurs deja entre guillemets - Gestion des imbrications (tableaux, objets)
3.2 Legacy HTTP Server (internal/api/legacy_http_server.go)¶
Le serveur HTTP dedie au firmware implemente toutes les quirks :
| Quirk | Implementation |
|---|---|
| Port 80 obligatoire | Le serveur ecoute sur :80 en mode production |
Content-Type: application/json ;charset=UTF-8 |
Header custom avec espace avant ; |
| Code 201 pour POST | w.WriteHeader(http.StatusCreated) |
_de67f en premiere position |
Champ fixe dans la structure de reponse, serialise en premier |
| Single-packet TCP | Reponse assemblée en memoire puis ecrite en un seul Write() |
3.3 Admin HTTP Server (internal/api/admin_http_server.go)¶
En parallele, un second serveur HTTP expose une API REST standard pour les clients modernes :
| Aspect | Legacy Server | Admin Server |
|---|---|---|
| Port | 80 | Configurable (ex: 3001) |
| JSON | Normalise a l'entree | Standard |
| Content-Type | Avec espace | Standard |
| Code POST | 201 | 200 |
| Authentification | Aucune (LAN) | Bearer token |
3.4 Action Service (internal/core/action_service.go)¶
Le service central qui traduit les intentions utilisateur en ordres firmware :
Intention utilisateur : "Allumer le salon"
↓
Frontend traduit en : {index: 619, bit: 1}
↓
ActionService.AddAction()
↓
GenerateCompleteBlock(605-622) : genere le bloc complet avec OR bitwise
↓
Redis essensys:global:actions : stocke la commande
↓
GET /api/myactions (polling firmware) : le firmware recupere l'ordre
↓
POST /api/done/{guid} : le firmware acquitte
Expansion du Bloc Complet¶
La methode GenerateCompleteBlock() prend les indices fournis par le client et genere un bloc atomique de 18 indices (605-622) :
func (s *ActionService) GenerateCompleteBlock(params []ActionParam) []ActionParam {
block := make(map[int]int)
for i := 605; i <= 622; i++ {
block[i] = 0 // Initialiser tout a 0
}
for _, p := range params {
if p.Key >= 605 && p.Key <= 622 {
block[p.Key] = p.Value
}
}
// Convertir en slice triee + ajouter trigger 590
result := []ActionParam{{Key: 590, Value: 1}}
for i := 605; i <= 622; i++ {
result = append(result, ActionParam{Key: i, Value: block[i]})
}
return result
}
Fusion Bitwise (OR)¶
Quand plusieurs commandes arrivent pendant le meme cycle de polling :
func (s *ActionService) MergeActions(existing, incoming []ActionParam) []ActionParam {
merged := make(map[int]int)
for _, p := range existing {
merged[p.Key] = p.Value
}
for _, p := range incoming {
merged[p.Key] |= p.Value // OR bitwise
}
return toSlice(merged)
}
3.5 Store Abstraction (internal/data/store.go)¶
Le pattern Repository isole l'acces aux donnees :
type Store interface {
GetExchangeTable(clientID string) (map[int]int, error)
SetExchangeValue(clientID string, index, value int) error
GetPendingActions(clientID string) ([]Action, error)
AckAction(clientID, guid string) error
GetClientInfo(clientID string) (*ClientInfo, error)
}
Implementations : - RedisStore : Production (Raspberry Pi) - MemoryStore : Tests unitaires et mode development - PostgresStore : Support Portal (gestion des machines/gateways)
4. Les 4 Points d'Entree des Ordres¶
Le backend accepte des commandes depuis 4 sources differentes, toutes convergent vers ActionService.AddAction() :
┌─────────────────────┐
Frontend Web ──── POST /api ────►│ │
│ │
MQTT (HA) ─── CommandHandler ───►│ ActionService │───► Redis ───► Firmware
│ .AddAction() │
MCP (IA) ──── Tool send_order ──►│ .GenerateComplete │
│ Block() │
Control Plane ─ PUT /api/redis ─►│ │
└─────────────────────┘
| Source | Mecanisme | Normalisation | Bloc complet |
|---|---|---|---|
| Frontend React | POST /api/admin/inject ou /api/web/actions |
Oui | Oui (automatique) |
| Home Assistant | Commande MQTT → CommandHandler |
Oui | Oui (automatique) |
| OpenClaw / IA | Outil MCP send_order |
Oui | Oui (auto dans le MCP server) |
| Control Plane | PUT /api/redis/exchange/{client}/{index} |
Non (ecriture directe) | Non |
Particularite du Control Plane¶
Le Control Plane ecrit directement dans Redis, contournant ActionService. C'est une porte de secours administrative pour injecter ou corriger des valeurs sans passer par le cycle action/acquittement.
5. Pont MQTT vers Home Assistant¶
Le backend publie l'etat de la table d'echange sur des topics MQTT structures pour le mecanisme de discovery de Home Assistant :
homeassistant/switch/essensys/salon1/config → auto-configuration HA
homeassistant/switch/essensys/salon1/state → ON/OFF
homeassistant/switch/essensys/salon1/set → commande depuis HA
Quand Home Assistant publie sur le topic /set, le backend :
1. Recoit la commande via le subscriber MQTT
2. Traduit le nom symbolique ("salon1") en indice de la table (619, bit 1)
3. Appelle ActionService.AddAction() avec le bloc complet
4. Le firmware recupere l'ordre lors du prochain polling
6. Le MCP : Interface IA¶
Le Model Context Protocol expose le systeme comme un ensemble d'outils utilisables par un agent IA :
Agent IA : "Quelle est la temperature de consigne du salon ?"
↓
Outil MCP : read_exchange_value(index=349)
↓
Redis : Tb_Echange[349] = 0x11
↓
MCP decode : mode automatique, consigne CONFORT
↓
Agent IA : "Le salon est en mode automatique, consigne CONFORT"
Le MCP server est un binaire Go independant (cmd/mcp-server/main.go) qui se connecte au meme Redis que le backend. Il implemente l'auto-expansion du bloc complet quand un agent IA envoie un ordre.
7. Le Frontend : Traduction Visuelle¶
Le frontend React traduit les indices numeriques opaques en interface utilisateur intuitive :
Indice 619 bit 1 → Bouton "Salon 1" avec icone ampoule
Indice 349 = 0x01 → Slider "Zone Jour" position "CONFORT"
Indice 625 = 0x07 → 3 volets salon coches dans l'interface
Indice 11 bit 0 = 1 → Badge rouge "ALARME" dans le header
Le frontend ne contient aucune logique metier. Il ne sait pas ce que signifie "indice 619 bit 1" au-dela de ce qui est configure dans ses fichiers de mapping. Si un nouveau dispositif est ajoute a la table d'echange, le frontend doit etre mis a jour manuellement.
8. Flux de Donnees Complet¶
Du Bouton au Relais¶
sequenceDiagram
participant User as Utilisateur
participant FE as Frontend React
participant BE as Backend Go
participant Redis as Redis
participant FW as BP_MQX_ETH
participant BA as Boitier Auxiliaire
User->>FE: Clic "Allumer Salon 1"
FE->>BE: POST /api/admin/inject {index: 619, bit: 1}
BE->>BE: GenerateCompleteBlock(605-622)
BE->>BE: MergeActions (OR bitwise)
BE->>Redis: RPUSH essensys:global:actions {guid, params}
Note over FW: Polling toutes les 2 sec
FW->>BE: GET /api/myactions
BE->>Redis: LPOP essensys:global:actions
BE-->>FW: 200 OK + {_de67f:null, actions:[...]}
FW->>FW: Ecrit indices dans Tb_Echange[]
FW->>BA: Transmet ordre via I2C
BA->>BA: Active le relais physique
FW->>BE: POST /api/done/{guid}
BE-->>FW: 201 Created
BE->>Redis: Supprime l'action acquittee
De l'Alerte au WhatsApp¶
sequenceDiagram
participant FW as BP_MQX_ETH
participant BE as Backend Go
participant Prom as Prometheus
participant AM as Alertmanager
participant OC as OpenClaw
participant WA as WhatsApp
FW->>BE: POST /api/mystatus (alerte bit actif)
BE->>BE: Expose metrique essensys_alert{type="fuite"}
Prom->>BE: Scrape /metrics
Prom->>Prom: Evalue regle d'alerte
Prom->>AM: Fire alert
AM->>OC: POST /hooks/agent (Bearer token)
OC->>OC: Reformule via OpenAI GPT-4o-mini
OC->>WA: Envoie message clair en francais
Note over WA: "⚠️ Alerte fuite lave-linge detectee. Verifiez l'arrivee d'eau."
9. Resilience et Degradation Gracieuse¶
Le systeme est concu pour fonctionner meme en cas de panne partielle :
| Composant en panne | Impact | Mitigation |
|---|---|---|
| Redis | Plus de queue d'actions, perte de cache | Le firmware continue de fonctionner en autonome |
| Backend Go | Plus de communication avec le firmware | L'ecran tactile local reste operationnel |
| MCP / OpenClaw | Plus d'interaction IA | Le frontend web reste fonctionnel |
| Prometheus / Alertmanager | Plus d'alertes proactives | Le firmware continue de signaler les alertes sur l'ecran |
| Internet (Traefik) | Plus d'acces distant | L'acces LAN via Nginx reste operationnel |
| Nginx | Plus d'acces web LAN | L'ecran tactile sur le materiel reste operationnel |
Le firmware BP_MQX_ETH est autonome : meme sans serveur, il continue de gerer le chauffage, l'alarme et les scenarios localement.
References Sources¶
- Normalisation JSON :
essensys-server-backend/internal/api/json_normalizer.go - Serveur legacy :
essensys-server-backend/internal/api/legacy_http_server.go - Serveur admin :
essensys-server-backend/internal/api/admin_http_server.go - Action Service :
essensys-server-backend/internal/core/action_service.go - Store interface :
essensys-server-backend/internal/data/store.go - MCP Server :
essensys-server-backend/cmd/mcp-server/main.go - Pont MQTT :
essensys-server-backend/internal/mqtt/ - Documentation legacy :
client-essensys-legacy/docs/protocol/