Script python - Hayward Smartbox 2

Bonjour tout le monde,

Venant juste de faire l’acquisition du module wifi smartbox 2 pour ma pompe à chaleur Hayward, j’ai cherché comment faire pour la connecter dans Jeedom.

J’ai testé bien évidemment le plugin mais sans succès.

J’ai utilisé PCAPdroid sur mon tel pour avoir l’URL utilisé par l’appli.

Du coup après de longue recherche sur le net j’ai réussi à trouver les endpoints correspondant et j’en ai fait un petit script python.

Avec ce dernier j’arrive à récupérer:

  • Consigne
  • Température de l’eau en entrée de la pompe
  • Température de l’eau en sortie de la pompe
  • Mode
  • Mode silencieux
  • Etat

J’arrive également à piloter le on/off, le mode silencieux, ainsi que la consigne.

Je n’ai pas trouvé de doc expliquant comment utiliser ces appels mais je suis tombé sur des issues d’un projet github qui en parle.

Voici mon script a utiliser via le plugin script:

#!/usr/bin/env python3

import requests
import hashlib
import json
import argparse
import time

BASE_URL = "https://cloud.linked-go.com:449/crmservice/api/app"

POWER_MAP = {
    "0": "Off",
    "1": "On"
}

MODE_MAP = {
    "1": "Chauffage",
    "0": "Refroidissement",
    "2": "Auto"
}

LABEL_MAP = {
    "Power": "Power",
    "Mode": "Mode",
    "R02": "Consigne",
    "T02": "Temp_eau_entree",
    "T03": "Temp_eau_sortie",
    "Manual-mute": "Mode_silencieux"
}

STATUS_CODES = ["Power", "Mode", "Manual-mute"]
TEMP_CODES = ["R02", "T02", "T03"]
DATA_CODES = STATUS_CODES + TEMP_CODES

class LinkedGoClient:
    def __init__(self, username, password):
        self.username = username
        self.password = password
        self.session = requests.Session()
        self.token = None

    def md5(self, text):
        return hashlib.md5(text.encode()).hexdigest()

    def headers(self):
        return {
            "x-token": self.token,
            "Content-Type": "application/json"
        }

    def login(self):
        url = f"{BASE_URL}/user/login?lang=en"

        payload = {
            "password": self.md5(self.password),
            "loginSource": "IOS",
            "areaCode": "en",
            "appId": "16",
            "type": "2",
            "userName": self.username
        }

        r = self.session.post(url, json=payload)
        r.raise_for_status()

        data = r.json()

        if "objectResult" not in data or "x-token" not in data["objectResult"]:
            raise Exception(f"Login failed: {data}")

        self.token = data["objectResult"]["x-token"]

    def get_devices(self):
        url = f"{BASE_URL}/device/deviceList?lang=en"

        r = self.session.post(url, headers=self.headers())
        r.raise_for_status()

        data = r.json()
        devices = data["objectResult"]

        if isinstance(devices, str):
            devices = json.loads(devices)

        return devices

    def get_data(self, device_code, codes):
        url = f"{BASE_URL}/device/getDataByCode?lang=en"

        payload = {
            "deviceCode": device_code,
            "appId": "16",
            "protocalCodes": codes
        }

        r = self.session.post(url, json=payload, headers=self.headers())
        r.raise_for_status()

        return r.json()["objectResult"]

    def control(self, device_code, protocol_code, value):
        url = f"{BASE_URL}/device/control?lang=en"

        payload = {
            "param": [
                {
                    "deviceCode": device_code,
                    "protocolCode": protocol_code,
                    "value": value
                }
            ]
        }

        r = self.session.post(url, json=payload, headers=self.headers())
        r.raise_for_status()

        print(f"Commande envoyée : {protocol_code} = {value}")

    def set_power(self, device_code, enabled: bool):
        self.control(device_code, "Power", "1" if enabled else "0")

    def set_temperature(self, device_code, temp: float):
        if not (15 <= temp <= 35):
            raise ValueError("Température invalide (15-35°C)")

        value = f"{temp:.1f}".rstrip("0").rstrip(".")
        self.control(device_code, "R02", value)

    def set_silent(self, device_code, enabled: bool):
        self.control(device_code, "Manual-mute", "1" if enabled else "0")

    def decode_value(self, code, value):
        if code == "Power":
            return POWER_MAP.get(value, value)

        if code == "Mode":
            return MODE_MAP.get(value, value)

        if code == "Manual-mute":
            return POWER_MAP.get(value, value)

        return value

    def get_label(self, code):
        return LABEL_MAP.get(code, code)


class HeatPump:
    def __init__(self, client, device):
        self.client = client
        self.device = device
        self.code = device["deviceCode"]
        self.name = device.get("deviceName", "Unknown")

    def get_status(self):
        data = self.client.get_data(self.code, DATA_CODES)
        result = {}

        for i in data:
            code = i["code"]
            value = i["value"]

            decoded = self.client.decode_value(code, value)
            label = self.client.get_label(code)

            if code in TEMP_CODES:
                result[label] = float(value)
            else:
                result[label] = decoded

        return result

    def on(self):
        self.client.set_power(self.code, True)

    def off(self):
        self.client.set_power(self.code, False)

    def set_temp(self, temp):
        self.client.set_temperature(self.code, temp)

    def silent(self, enabled=True):
        self.client.set_silent(self.code, enabled)


def write_status_json(pumps):
    results = {}

    for pump in pumps:
        results[pump.code] = pump.get_status()

    with open("/var/www/html/plugins/script/data/hayward_pac.json", "w") as f:
        json.dump(results, f, indent=2)

def parse_args():
    parser = argparse.ArgumentParser(description="Contrôle PAC Hayward")

    parser.add_argument("action", choices=["status", "on", "off", "temp", "silent"])
    parser.add_argument("value", nargs="?", help="Valeur (température ou on/off)")

    parser.add_argument("--device", help="Nom de l'appareil")

    return parser.parse_args()


if __name__ == "__main__":

    args = parse_args()

    client = LinkedGoClient("adresse_mail", "password")
    client.login()

    devices = client.get_devices()
    pumps = [HeatPump(client, d) for d in devices]

    if not pumps:
        print("Aucun appareil trouvé")
        exit(1)

    if args.action == "status":
        write_status_json(pumps)

    pump = pumps[0]

    if args.device:
        pump = next((p for p in pumps if p.name == args.device), pump)

    elif args.action == "on":
        pump.on()
        print("Pompe allumée")
        time.sleep(2)
        write_status_json(pumps)

    elif args.action == "off":
        pump.off()
        print("Pompe éteinte")
        time.sleep(2)
        write_status_json(pumps)

    elif args.action == "temp":
        if not args.value:
            print("Une température est attendue")
            exit(1)

        temp = float(args.value)
        pump.set_temp(temp)
        print(f"Température réglée à {temp}°C")
        time.sleep(2)
        write_status_json(pumps)

    elif args.action == "silent":
        if args.value not in ["on", "off"]:
            print("Valeur attendue: on/off")
            exit(1)

        enabled = args.value == "on"
        pump.silent(enabled)
        print(f"Mode silencieux {'activé' if enabled else 'désactivé'}")
        time.sleep(2)
        write_status_json(pumps)

Voici un exemple des commandes que j’ai crée dans le plugin script.

Ce n’est pas parfait mais ca me permet de piloter ma PAC avec le surplus solaire.

Ce qu’il faut savoir:

  • Ces URLS sont compatibles avec plusieurs modèles wifi (pas que Hayward).
  • Les codes que je demande correspondent à mon besoin, vous pouvez en ajouter en fouillant le net pour trouver ce qu’il vous faut.
  • Il faut éditer le script pour y mettre votre adresse mail ainsi que votre password. Ce que j’ai fait j’ai mis mes identifiants et j’ai crée un second compte en partageant mon appareil pour que je puisse rester connecter sur l’appli mobile. Sinon j’étais déco a chaque appel du script.
  • Dans la capture du dessus j’ai crée un commande pour rafraichir le status. une fois lancé cela va générer un fichier json dans le répertoire: plugins/script/data/hayward_pac.json afin de pouvoir le parser facilement avec le plugin script.

Voici un aperçu de mon fichier json:

Le 1er champ que j’ai masqué correspond à l’ID de mon équipement (smartbox 2).

En espérant que cela puisse aider quelqu’un.

Peut-être que ces endpoints ainsi que l’authentification pourrait être reporter dans le plugin.

Liens utilisés:

En vous souhaitant une bonne journée.

Zerat9w