# [TUTORIEL] Deye + RS485/Ethernet + Modbus + Jeedom
## Superviser et piloter les 6 créneaux batterie depuis Jeedom
> **Projet personnel partagé avec la communauté Jeedom**
> L’objectif de ce tutoriel est de montrer comment connecter un onduleur Deye à Jeedom via son port Modbus RS485, en utilisant un convertisseur RS485 → Ethernet, puis comment exploiter cette communication pour lire les informations batterie et piloter les 6 créneaux de programmation.
-–
## 1. Présentation
Je cherchais au départ simplement à récupérer les informations de mon Deye dans Jeedom. Une fois la communication Modbus établie, le projet a évolué vers un pilotage bidirectionnel : Jeedom ne se contente plus de lire les données, il peut également modifier plusieurs paramètres des créneaux batterie.
Le système permet notamment de :
- récupérer le SOC batterie ;
- récupérer la tension batterie ;
- récupérer la puissance batterie et déterminer charge/décharge ;
- lire les six horaires ;
- lire les six valeurs de puissance ;
- lire les six tensions programmées ;
- lire l’état de la charge réseau de chaque créneau ;
- modifier les horaires ;
- modifier les tensions ;
- modifier les puissances ;
- activer / désactiver la charge réseau pour chaque créneau ;
- modifier le courant de charge réseau ;
- centraliser toutes les informations dans un dashboard Jeedom.
Le développement présenté ici repose sur un script Python exécuté par le plugin Script de Jeedom et sur plusieurs widgets HTML/JavaScript personnalisés.
## 2. Architecture
ONDULEUR DEYE → Port Modbus RS485 → RS485 → HF2221 → RS485 → Ethernet → Ethernet / TCP → RESEAU LAN → TCP/IP → JEEDOM → plugin Script
Le convertisseur RS485/Ethernet fonctionne ici comme un pont transparent : le script construit une trame Modbus RTU, l’envoie en TCP au convertisseur, puis le convertisseur la transmet sur le bus RS485.
## 3. Matériel
### Onduleur
Le projet a été développé autour d’un Deye
### Convertisseur
Le convertisseur utilisé pendant le développement est un HF2221 RS485 → Ethernet****.
### Jeedom
Jeedom peut être installé sur un Raspberry Pi, mini-PC, serveur ou machine virtuelle. Dans mon cas, Jeedom tourne sur une VM Synology.
## 4. Câblage RS485
Le point important est d’utiliser le port Modbus du Deye et non un autre port RS485.
Le câblage à reproduire est :
DEYE CONVERTISSEUR RS485
Pin RS485 B → B
Pin RS485 A → A
GND → GND
## 5. Paramètres du convertisseur
Configurer le port série du convertisseur avec les paramètres utilisés par le projet :
Baudrate : 9600
Data bits : 8
Parity : None
Stop bits : 1
Le Deye utilisé dans le développement est adressé avec :
Slave ID : 1
Le convertisseur possède ensuite une adresse IP sur le réseau local et un port TCP.
**À adapter impérativement à votre installation.**
# 6. Installation du script Python
Le script est installé ici :
/var/www/html/plugins/script/data/deye_modbus.py
Le plugin Script de Jeedom permet ensuite d’appeler le programme avec différents arguments.
Donner les droits d’exécution :
sudo chmod +x /var/www/html/plugins/script/data/deye_modbus.py
# 7. Script Python complet
> **À modifier avant utilisation :** renseigner `DEYE_IP` avec l’adresse IP de votre convertisseur et adapter `DEYE_PORT` si nécessaire.
\`\`\`python
import socket
import sys
import json
\# ==========================================================
\# CONFIGURATION
\# ==========================================================
DEYE_IP = « 192.168.X.X »
DEYE_PORT = 10002
SLAVE = 1
\# ==========================================================
\# CRC16 MODBUS
\# ==========================================================
def crc16(data):
crc = 0xFFFF
for b in data:
crc = crc ^ b
for i in range(8):
if crc & 1:
crc = (crc >> 1) ^ 0xA001
else:
crc = crc >> 1
lo = crc & 0xFF
hi = (crc >> 8) & 0xFF
return lo, hi
\# ==========================================================
\# LECTURE D’UN REGISTRE
\# ==========================================================
def read_reg(addr):
body = bytes((
SLAVE,
3,
(addr >> 8) & 0xFF,
addr & 0xFF,
0,
1
))
lo, hi = crc16(body)
frame = body + bytes((lo, hi))
s = socket.socket()
s.settimeout(3)
s.connect((DEYE_IP, DEYE_PORT))
s.send(frame)
data = s.recv(50)
s.close()
if len(data) >= 5:
val = (data\[3\] << 8) + data\[4\]
return val
return None
\# ==========================================================
\# LECTURE REGISTRE SIGNE
\# ==========================================================
def read_reg_signed(addr):
val = read_reg(addr)
if val is None:
return None
if val > 32767:
val = val - 65536
return val
\# ==========================================================
\# LECTURE DE PLUSIEURS REGISTRES
\# ==========================================================
def read_regs(addr, count):
body = bytes((
SLAVE,
3,
(addr >> 8) & 0xFF,
addr & 0xFF,
(count >> 8) & 0xFF,
count & 0xFF
))
lo, hi = crc16(body)
frame = body + bytes((lo, hi))
s = socket.socket()
s.settimeout(3)
s.connect((DEYE_IP, DEYE_PORT))
s.send(frame)
data = s.recv(5 + count \* 2 + 10)
s.close()
values = \[\]
if len(data) >= 3 + count \* 2:
for i in range(count):
idx = 3 + i \* 2
val = (data\[idx\] << 8) + data\[idx + 1\]
values.append(val)
return values
\# ==========================================================
\# ECRITURE D’UN REGISTRE
\# ==========================================================
def write_reg(addr, value):
body = bytes((
SLAVE,
16,
(addr >> 8) & 0xFF,
addr & 0xFF,
0,
1,
2,
(value >> 8) & 0xFF,
value & 0xFF
))
lo, hi = crc16(body)
frame = body + bytes((lo, hi))
s = socket.socket()
s.settimeout(3)
s.connect((DEYE_IP, DEYE_PORT))
s.send(frame)
data = s.recv(50)
s.close()
return data
\# ==========================================================
\# CALCUL DE L’ADRESSE D’UN CRENEAU
\# ==========================================================
def slot_register(base, slot):
return base + (slot - 1)
\# ==========================================================
\# ACTIVER / DESACTIVER CHARGE RESEAU
\# ==========================================================
def set_grid_charge(slot, enable):
addr = slot_register(274, slot)
current = read_reg(addr)
if current is None:
print(« ERREUR lecture registre », addr)
return
if enable == 1:
new_value = current | 1
else:
new_value = current & 0xFFFE
write_reg(addr, new_value)
print(read_reg(addr))
\# ==========================================================
\# TOGGLE CHARGE RESEAU
\# ==========================================================
def toggle_grid_charge(slot):
addr = slot_register(274, slot)
current = read_reg(addr)
if current is None:
print(« ERREUR lecture registre », addr)
return
if current & 1:
new_value = current & 0xFFFE
result = 0
else:
new_value = current | 1
result = 1
write_reg(addr, new_value)
print(result)
\# ==========================================================
\# ARGUMENTS
\# ==========================================================
if len(sys.argv) < 2:
print(« ERREUR mode manquant »)
sys.exit(1)
mode = sys.argv\[1\]
\# ==========================================================
\# MODE READ
\# ==========================================================
if mode == « read »:
if len(sys.argv) < 3:
print(« ERREUR adresse manquante »)
sys.exit(1)
addr = int(sys.argv\[2\])
print(read_reg_signed(addr))
\# ==========================================================
\# MODE WRITE
\# ==========================================================
if mode == « write »:
if len(sys.argv) < 4:
print(« ERREUR arguments manquants »)
sys.exit(1)
addr = int(sys.argv\[2\])
value = int(sys.argv\[3\])
write_reg(addr, value)
print(read_reg(addr))
\# ==========================================================
\# MODE GRIDCHARGE
\# ==========================================================
if mode == « gridcharge »:
if len(sys.argv) < 4:
print(« ERREUR arguments manquants »)
sys.exit(1)
slot = int(sys.argv\[2\])
enable = int(sys.argv\[3\])
set_grid_charge(slot, enable)
\# ==========================================================
\# MODE GRIDCHARGETOGGLE
\# ==========================================================
if mode == « gridchargetoggle »:
if len(sys.argv) < 3:
print(« ERREUR créneau manquant »)
sys.exit(1)
slot = int(sys.argv\[2\])
toggle_grid_charge(slot)
\# ==========================================================
\# MODE VOLTAGE
\# ==========================================================
if mode == « voltage »:
if len(sys.argv) < 4:
print(« ERREUR arguments manquants »)
sys.exit(1)
slot = int(sys.argv\[2\])
value = float(sys.argv\[3\])
addr = slot_register(262, slot)
write_reg(addr, int(round(value \* 100)))
import time
time.sleep(0.3)
print(read_reg(addr))
\# ==========================================================
\# MODE HOUR
\# ==========================================================
if mode == « hour »:
if len(sys.argv) < 4 or « : » not in sys.argv\[3\]:
print(« ERREUR format heure invalide (attendu HH:MM) »)
sys.exit(1)
slot = int(sys.argv\[2\])
time_str = sys.argv\[3\]
h, m = time_str.split(« : »)
h = int(h)
m = int(m)
addr = slot_register(250, slot)
write_reg(addr, h \* 100 + m)
print(read_reg(addr))
\# ==========================================================
\# MODE POWER
\# ==========================================================
if mode == « power »:
if len(sys.argv) < 4:
print(« ERREUR arguments manquants »)
sys.exit(1)
slot = int(sys.argv\[2\])
value = int(sys.argv\[3\])
addr = slot_register(256, slot)
write_reg(addr, value)
print(read_reg(addr))
\# ==========================================================
\# MODE GRIDCHARGEAMP
\# ==========================================================
if mode == « gridchargeamp »:
if len(sys.argv) < 3:
print(« ERREUR valeur manquante »)
sys.exit(1)
value = int(sys.argv\[2\])
write_reg(230, value)
print(read_reg(230))
\# ==========================================================
\# MODE SCANALL
\# ==========================================================
if mode == « scanall »:
vals = read_regs(200, 100)
if len(vals) < 100:
print(json.dumps({« error »: « lecture incomplete »}))
sys.exit(1)
result = {}
\# Six horaires
for slot in range(1, 7):
result\[« hour » + str(slot)\] = vals\[slot - 1 + 50\]
result\[« power » + str(slot)\] = vals\[slot + 55\]
result\[« voltage » + str(slot)\] = vals\[slot + 61\]
result\[« gridcharge » + str(slot)\] = vals\[slot + 73\] & 1
# Courant de charge réseau
result[« gridchargeamp »] = vals[30]
# Batterie
result[« battery_soc »] = read_reg(184)
result[« battery_voltage »] = read_reg(183)
result[« battery_power »] = read_reg_signed(190)
# Toutes les valeurs 200 → 299
for i, v in enumerate(vals):
addr = 200 + i
result[« reg_ » + str(addr)] = v
print(json.dumps(result))
```
Ce script reprend la version finale fournie pendant le développement : fonctions de lecture/écriture Modbus, gestion des six créneaux, charge réseau, lecture groupée `scanall` et lecture des trois valeurs batterie.
-–
# 8. Tester la communication avant Jeedom
Je conseille fortement de tester le script depuis le terminal avant de créer les commandes Jeedom.
### Tester le SOC
python /var/www/html/plugins/script/data/deye_modbus.py read 184
python /var/www/html/plugins/script/data/deye_modbus.py read 184
### Tester la tension batterie
python /var/www/html/plugins/script/data/deye_modbus.py read 183
Le registre est retourné en valeur brute. Il faudra ensuite appliquer le facteur d’échelle prévu par le registre.
### Tester la puissance batterie
python /var/www/html/plugins/script/data/deye_modbus.py read 190
Cette valeur est traitée comme une valeur signée par le script.
# 9. Le mode `scanall`
C’est l’une des fonctions les plus pratiques du script.
python /var/www/html/plugins/script/data/deye_modbus.py scanall
Le programme lit les registres **200 à 299** en une seule opération puis construit un JSON contenant :
- les 6 horaires ;
- les 6 puissances ;
- les 6 tensions ;
- les 6 états de charge réseau ;
- le courant de charge réseau ;
- le SOC ;
- la tension batterie ;
- la puissance batterie ;
- l’ensemble des valeurs brutes `reg_200` à `reg_299`.
Cette dernière partie est intéressante pour explorer d’autres registres et comprendre progressivement la table de communication.
# 10. Tableau des registres utilisés
| Fonction | Registre | Traitement |
|---|---|---|
| Heure créneau 1 | 250 | HHMM |
| Heure créneau 1 | 250 | HHMM |
| Heure créneau 2 | 251 | HHMM |
| Heure créneau 3 | 252 | HHMM |
| Heure créneau 4 | 253 | HHMM |
| Heure créneau 5 | 254 | HHMM |
| Heure créneau 6 | 255 | HHMM |
| Puissance créneau 1 | 256 | valeur brute |
| Puissance créneau 2 | 257 | valeur brute |
| Puissance créneau 3 | 258 | valeur brute |
| Puissance créneau 4 | 259 | valeur brute |
| Puissance créneau 5 | 260 | valeur brute |
| Puissance créneau 6 | 261 | valeur brute |
| Tension créneau 1 | 262 | ×100 |
| Tension créneau 2 | 263 | ×100 |
| Tension créneau 3 | 264 | ×100 |
| Tension créneau 4 | 265 | ×100 |
| Tension créneau 5 | 266 | ×100 |
| Tension créneau 6 | 267 | ×100 |
| Courant charge réseau | 230 | valeur brute |
| Charge réseau créneau 1 | 274 | bit 0 |
| Charge réseau créneau 2 | 275 | bit 0 |
| Charge réseau créneau 3 | 276 | bit 0 |
| Charge réseau créneau 4 | 277 | bit 0 |
| Charge réseau créneau 5 | 278 | bit 0 |
| Charge réseau créneau 6 | 279 | bit 0 |
| SOC batterie | 184 | valeur brute |
| Tension batterie | 183 | ×100 |
| Puissance batterie | 190 | signé |
La logique d’adressage des créneaux est centralisée dans `slot_register(base, slot)`, ce qui évite de coder séparément les six cas.
# 11. Comprendre les écritures
## Modifier l’heure
Exemple pour le créneau 1 :
python /var/www/html/plugins/script/data/deye_modbus.py hour 1 15:05
Le script convertit automatiquement `15:05` en `1505` avant d’écrire le registre associé.
## Modifier la tension
python /var/www/html/plugins/script/data/deye_modbus.py voltage 1 54.4
Le script multiplie automatiquement la tension par 100 avant écriture.
## Modifier la puissance
python /var/www/html/plugins/script/data/deye_modbus.py power 1 3000
## Activer la charge réseau
python /var/www/html/plugins/script/data/deye_modbus.py gridcharge 1 1
## Désactiver
python /var/www/html/plugins/script/data/deye_modbus.py gridcharge 1 0
## Inverser l’état
python /var/www/html/plugins/script/data/deye_modbus.py gridchargetoggle 1
Le `toggle` lit d’abord le registre et modifie uniquement le bit 0, afin de ne pas écraser les autres bits du registre.
# 12. Création des commandes dans Jeedom
Le principe est de créer des commandes du plugin **Script** qui appellent le fichier Python.
Exemples :
### SOC
python /var/www/html/plugins/script/data/deye_modbus.py read 184
### Tension batterie
python /var/www/html/plugins/script/data/deye_modbus.py read 183
### Puissance batterie
python /var/www/html/plugins/script/data/deye_modbus.py read 190
### Créneau 1 – heure
python /var/www/html/plugins/script/data/deye_modbus.py hour 1 #value#
### Créneau 1 – tension
python /var/www/html/plugins/script/data/deye_modbus.py voltage 1 #value#
### Créneau 1 – puissance
python /var/www/html/plugins/script/data/deye_modbus.py power 1 #value#
### Créneau 1 – charge réseau
python /var/www/html/plugins/script/data/deye_modbus.py gridcharge 1 #value#
### Créneau 1 – toggle
python /var/www/html/plugins/script/data/deye_modbus.py gridchargetoggle 1
Il suffit de remplacer `1` par `2`, `3`, `4`, `5` ou `6` pour les cinq autres créneaux.
> **Remarque :** les noms exacts des commandes Jeedom et leur type Info/Action dépendent de la structure de votre équipement. Le présent tutoriel donne la logique de commande utilisée par le projet.
-–
# 13. Le dashboard personnalisé
Une fois la partie Modbus fonctionnelle, le projet a été complété par plusieurs widgets personnalisés pour avoir une interface lisible et compacte.
Les fichiers développés sont :
cmd.action.slider.time_picker.html
cmd.info.numeric.battery_gauge.html
cmd.info.numeric.power_flow.html
cmd.info.numeric.voltage_badge.html
# 14. Widget SOC batterie
Fichier :
cmd.info.numeric.battery_gauge.html
Il affiche le SOC sous forme de batterie horizontale et change de niveau/couleur en fonction du pourcentage.
Le widget final utilise un pôle de batterie à gauche et une animation d’alerte lorsque le SOC est faible.
Le code suivant correspond à la version finale développée :
<div class="cmd cmd-widget #history#" data-type="info" data-subtype="numeric" data-template="badge" data-cmd_id="#id#" data-cmd_uid="#uid#" data-version="#version#" data-eqLogic_id="#eqLogic_id#">
<div class="title #hide_name#">
<div class="cmdName">#name_display#</div>
</div>
<div class="deye-battery-container">
<div class="deye-battery-terminal"></div>
<div class="deye-battery">
<div class="deye-juice"></div>
<div class="deye-bolt">⚡</div>
</div>
<div class="deye-battery-level">--%</div>
</div>
</div>
<style>
:root {
--deye-alert: #f44336;
--deye-inter: #ff9800;
--deye-ok: #4CAF50;
--deye-shell: #888;
--deye-width: 70px;
--deye-height: 34px;
}
.deye-battery-container {
width: calc(var(--deye-width) + 16px);
display: flex;
align-items: center;
position: relative;
}
.deye-battery-terminal {
width: 6px;
height: 14px;
background: var(--deye-shell);
border-radius: 3px 0 0 3px;
margin-right: -2px;
z-index: 1;
}
.deye-battery {
width: var(--deye-width);
height: var(--deye-height);
border: 2.5px solid var(--deye-shell);
border-radius: 5px;
padding: 3px;
box-sizing: border-box;
position: relative;
background: #0f0f0f;
overflow: hidden;
box-shadow: inset 0 0 4px rgba(0,0,0,0.6);
}
.deye-juice {
height: 100%;
border-radius: 2px;
transition: width 0.6s ease, background 0.6s ease;
}
.deye-juice-alert {
background: linear-gradient(90deg, #8e0000, var(--deye-alert));
animation: deye-blink 1.2s ease-in-out infinite;
}
.deye-juice-inter {
background: linear-gradient(90deg, #b35900, var(--deye-inter));
}
.deye-juice-ok {
background: linear-gradient(90deg, #1b5e20, var(--deye-ok));
}
@keyframes deye-blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.deye-battery-level {
position: absolute;
top: 0;
left: 6px;
width: var(--deye-width);
height: 100%;
font-size: 12px;
font-weight: bold;
line-height: var(--deye-height);
text-align: center;
color: #fff;
text-shadow: 0 1px 2px rgba(0,0,0,0.8);
pointer-events: none;
}
</style>
<script>
jeedom.cmd.update['#id#'] = function(_options) {
var cmd = $('.cmd[data-cmd_id=#id#]');
var val = parseFloat(_options.display_value);
if (isNaN(val)) return;
cmd.find('.deye-juice').css('width', (val <= 0 ? 5 : val) + '%');
cmd.find('.deye-battery-level').text(Math.round(val) + '%');
var juice = cmd.find('.deye-juice');
juice.removeClass('deye-juice-alert deye-juice-inter deye-juice-ok');
if (val <= 20) {
juice.addClass('deye-juice-alert');
} else if (val <= 59) {
juice.addClass('deye-juice-inter');
} else {
juice.addClass('deye-juice-ok');
}
};
jeedom.cmd.update['#id#']({display_value:'#state#'});
</script>
Ce widget utilise jeedom.cmd.update, car la commande concernée est une commande Info native.
# 15. Widget puissance batterie
Fichier :
cmd.info.numeric.power_flow.html
L’idée est d’afficher directement le sens de circulation :
↑ 0.09 kW (charge)
ou :
↓ 2.50 kW (décharge)
Le widget utilise une zone morte de ±20 W afin d’éviter les basculements permanents lorsque la puissance oscille autour de zéro.
\`\`\`html
<div class="cmd cmd-widget #history#" data-type="info" data-subtype="numeric" data-template="badge" data-cmd_id="#id#" data-cmd_uid="#uid#" data-version="#version#" data-eqLogic_id="#eqLogic_id#">
#name_display#
→
– W
</div>
<s
.deye-power-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: #1e1e1
.deye-power-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: #1e1e1e;
border: 1px solid #555;
border-radius: 12px;
padding: 4px 12px;
font-weight: bold;
font-size: 14px;
color: #fff;
transition: border-color 0.4s, box-shadow 0.4s;
}
.deye-power-badge.deye-charging {
border-color: #4CAF50;
box-shadow: 0 0 8px rgba(76,175,80,0.4);
}
.deye-power-badge.deye-discharging {
border-color: #ff9800;
box-shadow: 0 0 8px rgba(255,152,0,0.4);
}
.deye-power-badge.deye-idle {
border-color: #666;
}
.deye-power-arrow {
font-size: 16px;
display: inline-block;
}
.deye-charging .deye-power-arrow {
color: #4CAF50;
animation: deye-arrow-up 1.2s ease-in-out infinite@keyframes
}
.deye-discharging .deye-power-arrow {
color:@keyframes#ff9800;
animat@keyframeson: deye-arrow-down 1.2s ease-in-out infinite;
}
@keyframes deye-arrow-up {
0%, 100% { transform: transla@keyframeseY(0); opacity: 1; }
50%@keyframes{ transform: translateY(-3px); opacity: 0.6; }
}
@keyframes deye-arrow-down {
0%, 100% { transform: translateY(0); opacity: 1; }
50% { transform: translateY(3px); opacity: 0.6; }
}
er-#id#');if (!badge) return;
var val = parseFloat(\_o
jeedom.cmd.update\[‹ #id# ›\] = function(\_options) {
var badge = document.getElementById(‹ deye-power-#id# ›);
if (!badge) return;
var val = parseFloat(\_options.display_value);
if (isNaN(val)) return;
var arrow = badge.querySelector(‹ .deye-power-arrow ›);
var valueEl = badge.querySelector(‹ .deye-power-value ›);
var absVal = Math.abs(val);
var kw = (absVal / 1000).toFixed(2);
badge.classList.remove(‹ deye-charging ›, ‹ deye-discharging ›, ‹ deye-idle ›);
if (val < -20) {
badge.classList.add(‹ deye-charging ›);
arrow.textContent = ‹ ↑ ›;
valueEl.textContent = kw + ’ kW (charge)‹ ;
} else if (val > 20) {
badge.classList.add(‹ deye-discharging ›);
arrow.textContent = ‹ ↓ ›;
valueEl.textContent = kw + ’ kW (décharge) ›;
} else {
badge.classList.add(‹ deye-idle ›);
arrow.textContent = ‹ → ›;
valueEl.textContent = ‹ 0.00 kW ›;
}
};
jeedom.cmd.update\[‹ #id# ›\]({display_value:‹ #state# ›});
fois une valeur déjà convertie en volts et une valeur brute multipliée par 100.
\`\`\`html
<div class="cmd cmd-widget #history#" data-type="info" data-subtype="numeric" data-template="badge" data-cmd_id="#id#" data-cmd_uid="#uid#" data-version="#version#" data-eqLogic_id="#eqLogic_id#">
#name_display#
🔋
– V
</div>
<style><style>
.deye-volt-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: #1e1e1e;
border: 1px solid #2196F3;
border-radius: 12px;
padding: 4px 12px;
font-weight: bol<style>
.deye-volt-badge {
display: inline-flex;
align-items: center;
gap: 6px;
background: #1e1e1e;
border: 1px solid #2196F3;
border-radius: 12px;
padding: 4px 12px;
font-weight: bold;
font-size: 14px;
color: #fff;
box-shadow: 0 0 6px rgba(33,150,243,0.25);
}
.deye-volt-icon {
font-size: 14px;
}
</style>al > 200 ? (val / 100<script>
m.cmd.update\[‹ #id# ›\] = function(\_options) {
var cmd = $(‹ .cmd\[data-cmd_id=#id#\] ›);
var val = parseFloat(\_options.display_value);
if (isNaN(val)) return;
var displayVal = (val > 200 ? (val / 100).toFixed(2) : val.toFixed(2));
cmd.find(‹ .deye-volt-value ›).text(displayVal + ’ V’);
};
jeedom.cmd.update\[‹ #id# ›\]({display_value:‹ #state# ›});
cmd.action.slider.time_picker.html
Le widget permet de sélectionner :
HH : MM
avec :
- heures de 00 à 23 ;
- minutes de 00 à 55 par pas de 5 minutes.
Les sélecteurs sont volontairement disposés sur la même ligne afin d’obtenir une commande compacte.
\`\`\`html
<script type="<script type="text/javascript">
if (typeof window.deyeRegisterSync !== 'function') {
window.deyeSyncCallbacks = {};
window.deyeRegisterSync = function(syncId, key, callback) {
if (!window.deyeSyncCallbacks\[syncId\]) {
w<script type="text/javascript">
if (typeof window.deyeRegisterSync !== 'function') {
window.deyeSyncCallbacks = {};
window.deyeRegisterSync = function(syncId, key, callback) {
if (!window.deyeSyncCallbacks\[syncId\]) {
window.deyeSyncCallbacks\[syncId\] = {};
var existing = jeedom.cmd.update\[syncId\];
jeedom.cmd.update\[syncId\] = function(\_options) {
if (typeof existing === 'function') existing(\_options);
var data;
try {
data = JSON.parse(\_options.value);
} catch(e) {
return;
}
for (var k in window.deyeSyncCallbacks\[syncId\]) {
window.deyeSyncCallbacks\[syncId\]\[k\](data);
}
};
}
window.deyeSyncCallbacks\[syncId\]\[key\] = callback;
};
}
</script>0,0.25); »>
hour-#id# » data-slot=« #slot# »
style=« font-size:14px; padding:2px 4px; border-radius:6px; border:none; background:transparent; color:#fff; cursor:pointer; text-align:right; -webkit-appearance:none; appearance:none; »
onchange=« deyeTimeExecute(‹ #id# ›) »>
:
<select id=« minute-#id# »
style=« font-size:14px; padding:2px 4px; border-radius:6px; border:none; background:transparent; color:#fff; cursor:pointer; -webkit-appearance:none; appearance:none; »
onchange=« deyeTimeExecute(‹ #id# ›) »>
</div>
<script type="text/<script type="text/javascript">
function deyeTimeInit(id) {
var hourSelect = document.getElementById('hour-' + id);
var minuteSelect = document.getElementById('minute-' + id);
if (!hourSelect || !minuteSelect) return;
if (hourSelect.options.length === 0) {
<script type="text/javascript">
function deyeTimeInit(id) {
var hourSelect = document.getElementById('hour-' + id);
var minuteSelect = document.getElementById('minute-' + id);
if (!hourSelect || !minuteSelect) return;
if (hourSelect.options.length === 0) {
for (var h = 0; h <= 23; h++) {
var opt = document.createElement('option');
var val = (h < 10 ? '0' : '') + h;
opt.value = val;
opt.text = val;
hourSelect.appendChild(opt);
}
}
if (minuteSelect.options.length === 0) {
for (var m = 0; m <= 55; m += 5) {
var opt2 = document.createElement('option');
var val2 = (m < 10 ? '0' : '') + m;
opt2.value = val2;
opt2.text = val2;
minuteSelect.appendChild(opt2);
}
}
}
window.deyeTimeExecute = function(id) {
var hourSelect = document.getElementById('hour-' + id);
var minuteSelect = document.getElementById('minute-' + id);
if (!hourSelect || !minuteSelect) return;
var h = hourSelect.value;
var m = minuteSelect.value;
var payload = JSON.stringify({slider: h + ':' + m});
jeedom.cmd.execute({id: id, value: payload});
}
window.deyeTimeSetValue = function(id, currentValue) {
var hourSelect = document.getElementById('hour-' + id);
var minuteSelect = document.getElementById('minute-' + id);
if (!hourSelect || !minuteSelect) return;
if (currentValue && currentValue.indexOf(':') > -1) {
var parts = currentValue.split(':');
hourSelect.value = parts\[0\];
minuteSelect.value = parts\[1\];
}
}
deyeTimeInit('#id#');
deyeTimeSetValue('#id#', '#state#');
window.deyeRegisterSync('4537', 'time-#id#', function(data) {
var hourSelect = document.getElementById('hour-#id#');
if (!hourSelect) return;
var slot = hourSelect.getAttribute('data-slot');
var val = data\['hour' + slot\];
if (val !== undefined) {
var h = Math.floor(val / 100);
var m = val % 100;
var hh = (h < 10 ? '0' : '') + h;
var mm = (m < 10 ? '0' : '') + m;
deyeTimeSetValue('#id#', hh + ':' + mm);
}
});
</script>un appel effectué alors que le DOM du widget n’était pas encore complètement construit.ction et d’affichage ont donc été sécurisées avec un contrôle d’existence :
\`\`\`javascript
window.deyeVoltDisplay = function(id) {
var slider = document.getElementById(‹ volt- › + id);
var display = document.getElementById(‹ voltval- › + id);
if (!slider || !display) return;
display.innerText = parseFloat(slider.value).toFixed(1) + ’ V’;
deyeUpdateVoltTrack(id);
}
window.deyeVoltExecute = function(id) {
var slider = document.getElementById(‹ volt- › + id);
if (!slider) return;
var val = parseFloat(slider.value).toFixed(1);
var payload = JSON.stringify({slider: val});
jeedom.cmd.execute({id: id, value: payload});
}
function deyeUpdateVoltTrack(id) {
var slider = document.getElementById(‹ volt- › + id);
if (!slider) return;
var pct = ((slider.value - slider.min) / (slider.max - slider.min)) \* 100;
slider.style.background =
'linear-gradient(to right, #2196F3 0%, #2196F3 ’ + pct + '%, #444249 ’ + pct + ‹ %, #444249 100%) ›;
}
> **Nota :** le fichier source fourni ne contient pas l’intégralité du `voltage_slider.html`, uniquement les trois fonctions finales de correction. Le code ci-dessus est donc à intégrer dans le fichier complet déjà utilisé dans votre installation, et non à considérer comme le fichier complet du widget.
-–
# 19. Synchronisation des widgets
Le widget horaire met en place un petit mécanisme de synchronisation JavaScript partagé entre plusieurs instances.
Le principe est de créer une table de callbacks :
\`\`\`javascript
window.deyeSyncCallbacks
\`\`\`
puis d’intercepter la mise à jour d’une commande Jeedom pour redistribuer le JSON aux widgets concernés.
Cette méthode permet de conserver les six widgets synchronisés avec le résultat de `scanall`.
# 20. Exemple de dashboard final
L’idée finale est d’avoir une interface de ce type :
Le dashboard final permet donc de voir immédiatement :
- l’état de charge ;
- la tension batterie ;
- le sens et la puissance charge/décharge ;
- les six horaires ;
- les six tensions ;
- l’état de charge réseau de chaque période.
-–
# 21. Dépannage
## Le script ne répond pas
Vérifier dans cet ordre :
1. l’adresse IP du convertisseur ;
2. le port TCP ;
3. la liaison Ethernet ;
4. le mode transparent du convertisseur ;
5. les paramètres 9600 / 8N1 ;
6. l’adresse Modbus `SLAVE` ;
7. le câblage A/B.
## `TabError`
Remplacer entièrement le contenu du fichier Python et éviter tout mélange d’indentation.
## Les widgets affichent `– V`
Vérifier la console JavaScript du navigateur et les protections `if (!slider) return;` dans le widget tension. Le problème rencontré pendant le développement venait d’un appel alors que l’élément HTML n’existait pas encore.
## Les valeurs ne se mettent pas à jour
Vérifier que les commandes Info utilisent bien `jeedom.cmd.update` et que les commandes Action personnalisées utilisent le mécanisme de synchronisation correspondant.
-–
# 22. Sécurité et précautions
Le script peut écrire dans l’onduleur. Il faut donc rester prudent avec les valeurs envoyées,
Les limites doivent rester compatibles avec l’installation réelle, la batterie, le BMS et les protections.
Le fait qu’un registre soit modifiable ne signifie pas que toutes les valeurs sont sûres dans toutes les installations.
-–
# 23. Conclusion
Cette intégration permet de transformer Jeedom en véritable interface locale de supervision et de pilotage du Deye.

