Reboot a distance

Bonjour,

Je possède un EcoDevices 3 avec le dernier firmware (Version IHM :1.2.0 et Version du firmware:1.3.0). Il communique avec Home Assistant.

Est-il possible de redémarrer l’ED3 à distance par une commande HTTP/REST, une API ou une autre méthode réseau, sans couper physiquement son alimentation ?

Si oui, pourriez-vous me communiquer la commande ou la documentation correspondante ?

Mon objectif est de redémarrer automatiquement l’ED3 ne renvoie plus de valeur vers home assistant (ex : si la valeur du tarif Linky ne correspond plus à HEURE PLEINE ou HEURE CREUSE)

Merci pour votre aide.

Bonsoir @Cocof ,

Oui, bien sûr, vous pouvez redémarrer à distance votre ED3.. Il suffit de vous connecter à l’IP de votre ED3, d’entrer votre mdp puis sur la roue dentée (paramètres), système, vous avez 4 boutons et le quatrième est « Redémarrer »

Bonne soirée

Bonjour @Cocof

Oui c’est possible, il faut utiliser cet utilitaire (pour un accès distant depuis le net il faut un vpn)

Bonjour,
je pense que ça ne répond pas aux besoins :wink:

@cocof : l’api ne permet pas de rebooter l’ECO3, c’est une fonctionnalité qui avait été demandée pendant les bétatests, mais non implémentée à ma connaissance.
A part couper l’alim par un relais, je ne vois pas comment faire autrement.
Bonne journée

bjr

en effet je veux automatiser via mon home assistant ce reboot. c’est pour cela qu’il me faut une commande via le reseau

bonjour la commande semble etre POST http://192.168.0.91:5080/api/system/reboot?

mais avec le logon et le mot de passe. Il faudrait les mettre ou dans la commande ? (ou l’apikey ?)

il faut passer par le soft pour l’instant

Merci. Ce que ton soft fait, ha doit bien savoir le faire. Je vais voir avec Claude ou autre je teste demain matin ( besoin d’une double ou triple authentification d’après ce que j’ai compris, mais ça semble faisable.

c’est du html, tu télécharge et tu peux examiner le code et sit u veux juste la partie reboot il suffit d’isoler le bloc de code qui le fait et de voir avec Claude Code pour le mettre en script dans HA

Pour les infos concernant le reboot, les voici (il faut que les champs IP et APIKEY soient remplis) :

Le reboot de l’EcoDevices 3 utilise un mécanisme d’authentification en 3 étapes :

Étape 1 — Challenge : POST /api/auth/secure/root avec body {} → le serveur génère et retourne une secureKey aléatoire à usage unique (ex: WqMFIrEWuqtK0MKi).

Étape 2 — Login chiffré : le deck chiffre {"login":"root","passwd":"root"} en AES-ECB avec la secureKey comme clé (parsée en UTF-8), puis envoie le résultat en secureContent via POST /api/auth/login/root?secure=true → le serveur retourne un AuthToken de session.

Étape 3 — Reboot : POST /api/system/reboot?AuthToken=xxx avec body {} → le serveur redémarre en ~1 seconde.

Points clés découverts par rétro-ingénierie :

  • L’auth utilise AuthToken (session) et non ApiKey (statique)
  • Le chiffrement est AES-ECB — identifié en capturant les échanges F12 et en testant les modes de déchiffrement
  • Le mot de passe ne transite jamais en clair
  • La secureKey est à usage unique — elle expire immédiatement après l’étape 2

Hello ca fonctionne :slight_smile: tuto :

creation du fichier ecodevices_reboot.py dans le repertoire config de home assistant :



import json
import re
import sys
import base64
import urllib.parse
import urllib.request
from pathlib import Path

from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.padding import PKCS7


SECRETS_FILE = Path("/config/secrets.yaml")


def read_secret(name):
    text = SECRETS_FILE.read_text(encoding="utf-8")

    match = re.search(
        r"(?m)^" + re.escape(name) + r"\s*:\s*(.*?)\s*$",
        text,
    )

    if not match:
        raise RuntimeError(f"Clé absente de secrets.yaml : {name}")

    value = match.group(1).strip()

    if (
        len(value) >= 2
        and value[0] == value[-1]
        and value[0] in "'\""
    ):
        value = value[1:-1]

    return value


def post(base_url, path, data):
    request = urllib.request.Request(
        base_url.rstrip("/") + path,
        data=json.dumps(
            data,
            ensure_ascii=False,
        ).encode("utf-8"),
        headers={"Content-Type": "application/json"},
        method="POST",
    )

    with urllib.request.urlopen(request, timeout=10) as response:
        body = response.read().decode("utf-8")

        if not body.strip():
            return {}

        return json.loads(body)


def authenticate(base_url, login, password):
    encoded_login = urllib.parse.quote(login, safe="")

    # Étape 1 : récupération de la clé de chiffrement
    challenge = post(
        base_url,
        f"/api/auth/secure/{encoded_login}",
        {},
    )

    secure_key = challenge["secureKey"].encode("utf-8")

    if len(secure_key) not in (16, 24, 32):
        raise RuntimeError(
            f"Longueur inattendue de secureKey : "
            f"{len(secure_key)} octets"
        )

    # Étape 2 : chiffrement des identifiants selon l'API GCE
    payload = json.dumps(
        {
            "login": login,
            "passwd": password,
        },
        separators=(",", ":"),
        ensure_ascii=False,
    ).encode("utf-8")

    padder = PKCS7(128).padder()
    padded_payload = (
        padder.update(payload) + padder.finalize()
    )

    encryptor = Cipher(
        algorithms.AES(secure_key),
        modes.ECB(),
    ).encryptor()

    encrypted_payload = (
        encryptor.update(padded_payload)
        + encryptor.finalize()
    )

    secure_content = base64.b64encode(
        encrypted_payload
    ).decode("ascii")

    # Étape 3 : authentification
    result = post(
        base_url,
        f"/api/auth/login/{encoded_login}?secure=true",
        {"secureContent": secure_content},
    )

    token = result.get("AuthToken")

    if not token:
        raise RuntimeError(
            "Authentification refusée : AuthToken absent"
        )

    return token


def reboot():
    base_url = read_secret("ecodevices_url").rstrip("/")
    login = read_secret("ecodevices_login")
    password = read_secret("ecodevices_password")

    token = authenticate(base_url, login, password)

    # Étape 4 : envoi de la commande de redémarrage
    path = "/api/system/reboot?" + urllib.parse.urlencode(
        {"AuthToken": token}
    )

    post(base_url, path, {})

    print("Commande de redémarrage envoyée à l'ECODEVICES 3.")


if __name__ == "__main__":
    try:
        reboot()
    except Exception as err:
        print(f"Erreur : {type(err).__name__}: {err}")
        sys.exit(1)

dans le fichier configuration.yaml :

shell_command:
  reboot_ecodevices3: "/usr/local/bin/python3 /config/ecodevices_reboot.py"

dans le fichier secret.yaml :

ecodevices_login: "root"
ecodevices_password: "mot_de_passe_root"
ecodevices_url: "http://192.168.x.xx:xxxx"

il suffit après de creer une automatisation du type :

alias: Linky - Alerte si tarif invalide
description: >-
  Alerte dès la perte du tarif, puis jusqu'à deux tentatives
  de redémarrage de l'EcoDevices 3 si le problème persiste.

triggers:
  - trigger: template
    id: tarif_invalide
    value_template: >-
      {{ states('sensor.linky_tarif_en_cours') not in
         ['HEURE  PLEINE', 'HEURE  CREUSE'] }}
    for:
      minutes: 1

  - trigger: homeassistant
    event: start
    id: demarrage_ha

conditions: []

actions:
  # Après démarrage de HA, laisser les intégrations se stabiliser
  - if:
      - condition: trigger
        id: demarrage_ha
    then:
      - delay:
          minutes: 2

  # Vérifier que le tarif est toujours invalide
  - condition: template
    value_template: >-
      {{ states('sensor.linky_tarif_en_cours') not in
         ['HEURE  PLEINE', 'HEURE  CREUSE'] }}

  # Notification immédiate
  - action: notify.mobile_app_christophe
    data:
      title: "⚠️ ALERTE Linky"
      message: >-
        Perte ou valeur invalide du tarif Linky détectée.
        Valeur actuelle : {{ states('sensor.linky_tarif_en_cours') }}.
        Tentative de redémarrage de l'EcoDevices 3 en cours.

  # Première tentative de redémarrage
  - action: shell_command.reboot_ecodevices3
    continue_on_error: true

  - delay:
      minutes: 1

  # Deuxième tentative seulement si le tarif reste invalide
  - if:
      - condition: template
        value_template: >-
          {{ states('sensor.linky_tarif_en_cours') not in
             ['HEURE  PLEINE', 'HEURE  CREUSE'] }}
    then:
      - action: shell_command.reboot_ecodevices3
        continue_on_error: true

      - delay:
          minutes: 1

  # Notification finale si le problème persiste
  - if:
      - condition: template
        value_template: >-
          {{ states('sensor.linky_tarif_en_cours') not in
             ['HEURE  PLEINE', 'HEURE  CREUSE'] }}
    then:
      - action: notify.mobile_app_christophe
        data:
          title: "🚨 Linky - Tarif toujours invalide"
          message: >-
            Le tarif reste invalide après les tentatives de redémarrage
            de l'EcoDevices 3.
            Valeur actuelle : {{ states('sensor.linky_tarif_en_cours') }}.
            Vérifier l'appareil et sa communication.

mode: single
max_exceeded: silent