[TUTORIEL] Deye + RS485/Ethernet + Modbus + Jeedom

# [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.

5 « J'aime »