[tuto] Récupération des données d'energie via le cloud d'un ECU APSSystems

Bonjour à tous,

J’utilise le très bon plugin de @Noyax37 : plugin-apsystemssunspec.
Cependant, le Modbus ne remonte pas les données des T.O.R. de mesure, donc pas possible d’avoir :

  • production,
  • export,
  • import,
  • consommation.

:light_bulb: Pour compléter, je récupère ces infos directement depuis le site EMA APSystems, via l’API fournie :
:backhand_index_pointing_right: Apsystems OpenAPI User Manual (PDF)


:warning: Remarques importantes

:one: API gratuite = limitée à 1000 appels/mois (~30/jour) → impossible de faire du temps réel.

:two: Il vous faut un compte EMA APSystems actif
:backhand_index_pointing_right: apsystemsema


:magnifying_glass_tilted_left: Données récupérées

Scenario 1 - A lancer tous les jours à 23h59

  • Energie produite(kWh)
  • Energie consommée (kWh)
  • Energie importée (kWh)
  • Energie exportée (kWh)

Par :

  • par heure
  • par jour
  • par mois
  • par an

Précision : on récupère toujours tout l’historique de la période en cours (par ex. toutes les journées du mois actuel).

Scenario 2 - A lancer toutes les 20min de 8h à 20h afin de ne pas exploser le nb de jetons de l’API

  • Puissance produite (W)
  • Puissance consommée (W)
  • Puissance importée-exporté (W)
    Fréquence des données : 5min
    Cron à utiliser : */20 8-20 * * *


:hammer_and_wrench: Mise en place

:white_check_mark: :one: Récupérer vos identifiants API
Dans l’interface EMA :

  • appId → Settings > OpenAPI Service
  • appSecret → Settings > OpenAPI Service
  • sid → Settings > Account Detail > Personal Info > OTHER > SID
  • eid → Settings > Account Detail > ECU Info > ECU ID

:white_check_mark: :two: Créer un équipement (plugin Virtual)
Notez son ID : $equipementId (les commandes se créeront toutes seules).
(ca peut- etre le meme équipement pour les 2 scenarios

:white_check_mark: :three: Créer un scénario Jeedom (à lancer chaque jour à 23:59)
Copiez-collez les blocs code ci-dessous.
Attention, scripts PHP intégralement réalisé avec chatGPT


:package: Scénarios

👉 Scenario 1 : Données d'energie 1X/jour à 23h59 - Cliquez pour déplier le scénario
$debugMode = 0;

$equipementId = 1162;
$sid = 'XXXX';
$eid = 'XXXX';
$appId = 'XXXX';
$appSecret = 'XXXX';
$baseUrl = 'https://api.apsystemsema.com:9282';

$eqLogic = eqLogic::byId($equipementId);
if (!is_object($eqLogic)) {
    $scenario->setLog('Équipement non trouvé');
    return;
}

function checkOrCreateCmd($logicalId, $name, $eqLogic, $scenario, $debugMode) {
    $cmd = cmd::byEqLogicIdAndLogicalId($eqLogic->getId(), $logicalId);
    if (!is_object($cmd)) {
        $cmdSameName = cmd::byEqLogicIdCmdName($eqLogic->getId(), $name);
        if (is_object($cmdSameName)) $name .= ' (auto)';
        $cmd = new cmd();
        $cmd->setLogicalId($logicalId);
        $cmd->setEqLogic_id($eqLogic->getId());
        $cmd->setType('info');
        $cmd->setSubType('numeric');
        $cmd->setIsHistorized(1);
        $cmd->setUnite('kWh');
        $cmd->setName($name);
        $cmd->save();
        if ($debugMode) $scenario->setLog("Commande créée : $name ($logicalId)");
    }
    return $cmd;
}

function safeEvent($cmd, $value, $timestamp, &$counter, &$summary, $scenario, $debugMode) {
    if (is_null($value)) {
        if ($debugMode) $scenario->setLog("Ignoré null pour {$cmd->getName()} @$timestamp");
        return;
    }
    $newValue = round((float)$value, 3);
    $datetime = date('Y-m-d H:i:s', $timestamp);
    $existing = history::byCmdIdAtDatetime($cmd->getId(), $datetime);
    if (is_object($existing) && round((float)$existing->getValue(), 3) === $newValue) {
        if ($debugMode) $scenario->setLog("Déjà en base, pas de mise à jour : {$cmd->getName()} @$datetime");
        return;
    }
    if ($debugMode) $scenario->setLog("🔧 {$cmd->getName()} → $newValue @$datetime");
    $cmd->event($newValue, $datetime);
    $summary[] = "{$cmd->getName()} = $newValue @$datetime";
    $counter++;
}

function apiRequest($path, $appId, $appSecret, $baseUrl, $scenario, $debugMode) {
    $urlSegments = explode('/', strtok($path, '?'));
    $lastSegment = end($urlSegments);
    $timestamp = round(microtime(true) * 1000);
    $nonce = strtolower(bin2hex(random_bytes(16)));
    $stringToSign = implode('/', [$timestamp, $nonce, $appId, $lastSegment, 'GET', 'HmacSHA256']);
    $signature = base64_encode(hash_hmac('sha256', $stringToSign, $appSecret, true));
    $headers = ['X-CA-AppId: ' . $appId, 'X-CA-Timestamp: ' . $timestamp, 'X-CA-Nonce: ' . $nonce, 'X-CA-Signature-Method: HmacSHA256', 'X-CA-Signature: ' . $signature];
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $baseUrl . $path);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($httpCode !== 200) {
        $scenario->setLog("$path (HTTP $httpCode)");
        return null;
    }
    if ($debugMode) $scenario->setLog("$path → $response");
    return json_decode($response, true);
}

$cmds = [];
foreach ([
    'produced_hourly' => 'Energie produite horaire', 'consumed_hourly' => 'Energie consommé horaire', 'exported_hourly' => 'Energie exporté horaire', 'imported_hourly' => 'Energie immporté horaire',
    'produced_daily' => 'Energie produite jour', 'consumed_daily' => 'Energie consommé jour', 'exported_daily' => 'Energie exporté jour', 'imported_daily' => 'Energie immporté jour',
    'produced_monthly' => 'Energie produite mois', 'consumed_monthly' => 'Energie consommé mois', 'exported_monthly' => 'Energie exporté mois', 'imported_monthly' => 'Energie immporté mois',
    'produced_yearly' => 'Energie produite année', 'consumed_yearly' => 'Energie consommé année', 'exported_yearly' => 'Energie exporté année', 'imported_yearly' => 'Energie importé année'
] as $id => $name) {
    $cmds[$id] = checkOrCreateCmd($id, $name, $eqLogic, $scenario, $debugMode);
}

$modifCount = 0;
$summaryLog = [];
foreach (['hourly' => 'Y-m-d', 'daily' => 'Y-m', 'monthly' => 'Y', 'yearly' => ''] as $level => $format) {
    $dateParam = $format ? date($format) : '';
    $url = "/user/api/v2/systems/$sid/devices/meter/period/$eid?energy_level=$level" . ($dateParam ? "&date_range=$dateParam" : '');
    $data = apiRequest($url, $appId, $appSecret, $baseUrl, $scenario, $debugMode);
    if ($data && isset($data['data'])) {
        $times = $data['data']['time'] ?? range(0, count($data['data']['produced']) -1);
        foreach ($times as $i => $time) {
            $produced = $data['data']['produced'][$i];
            $consumed = $data['data']['consumed'][$i];
            $imported = $data['data']['imported'][$i];
            $exported = $data['data']['exported'][$i];
            if ($level === 'hourly') $timestamp = strtotime(date('Y-m-d') . " $time:00:00");
            elseif ($level === 'daily') $timestamp = strtotime(date('Y-m') . '-' . str_pad($time, 2, '0', STR_PAD_LEFT) . ' 00:00:00');
            elseif ($level === 'monthly') $timestamp = strtotime(date('Y') . '-' . str_pad($time, 2, '0', STR_PAD_LEFT) . '-01 00:00:00');
            else $timestamp = strtotime($time . '-01-01 00:00:00');
            safeEvent($cmds['produced_' . $level], $produced, $timestamp, $modifCount, $summaryLog, $scenario, $debugMode);
            safeEvent($cmds['consumed_' . $level], $consumed, $timestamp, $modifCount, $summaryLog, $scenario, $debugMode);
            safeEvent($cmds['imported_' . $level], $imported, $timestamp, $modifCount, $summaryLog, $scenario, $debugMode);
            safeEvent($cmds['exported_' . $level], $exported, $timestamp, $modifCount, $summaryLog, $scenario, $debugMode);
        }
    }
}

$scenario->setLog("Total valeurs mises à jour : $modifCount");
if ($debugMode && count($summaryLog)) {
    $scenario->setLog("📝 Résumé des mises à jour :");
    foreach ($summaryLog as $line) {
        $scenario->setLog("   → $line");
    }
}
👉 Scenario 2 : Données de puissance 1X/20min de 8h à 20h - Cliquez pour déplier le scénario complet
// Script Jeedom pour récupérer et historiser les puissances minutely sans doublon + résumé final
$debugMode = 0; // Mettre 1 pour debug complet, 0 pour réduire logs

$equipementId = 1162;
$sid = 'XXXXX';
$eid = 'XXXXX';
$appId = 'XXXXX';
$appSecret = 'XXXXX';
$baseUrl = 'https://api.apsystemsema.com:9282';

$eqLogic = eqLogic::byId($equipementId);
if (!is_object($eqLogic)) {
    $scenario->setLog('Equipement non trouvé');
    return;
}

function apiRequest($path, $appId, $appSecret, $baseUrl, $debugMode, $scenario) {
    $urlSegments = explode('/', strtok($path, '?'));
    $lastSegment = end($urlSegments);
    $timestamp = round(microtime(true) * 1000);
    $nonce = strtolower(bin2hex(random_bytes(16)));
    $stringToSign = implode('/', [$timestamp, $nonce, $appId, $lastSegment, 'GET', 'HmacSHA256']);
    $signature = base64_encode(hash_hmac('sha256', $stringToSign, $appSecret, true));
    $headers = ['X-CA-AppId: ' . $appId, 'X-CA-Timestamp: ' . $timestamp, 'X-CA-Nonce: ' . $nonce, 'X-CA-Signature-Method: HmacSHA256', 'X-CA-Signature: ' . $signature];
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $baseUrl . $path);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($httpCode !== 200) {
        $scenario->setLog("[apiRequest] Erreur $httpCode : $response");
        return null;
    }
    if ($debugMode) $scenario->setLog("[apiRequest] Réponse $path → $response");
    return json_decode($response, true);
}

function checkOrCreateCmd($logicalId, $name, $eqLogic) {
    $cmd = cmd::byEqLogicIdAndLogicalId($eqLogic->getId(), $logicalId);
    if (!is_object($cmd)) {
        $cmd = new cmd();
        $cmd->setLogicalId($logicalId);
        $cmd->setEqLogic_id($eqLogic->getId());
        $cmd->setType('info');
        $cmd->setSubType('numeric');
        $cmd->setName($name);
        $cmd->setIsHistorized(1);
        $cmd->setUnite('W');
        $cmd->save();
    }
    return $cmd;
}

function saveIfDifferent($cmd, $value, $timestamp, &$count, $scenario) {
    $datetime = date('Y-m-d H:i:s', $timestamp);
    $existing = history::byCmdIdAtDatetime($cmd->getId(), $datetime);
    $lastValue = is_object($existing) ? floatval($existing->getValue()) : null;
    $newValue = round((float)$value, 2);
    if ($lastValue !== $newValue) {
        $scenario->setLog("[SAVE] {$cmd->getName()} $datetime → $newValue W");
        $cmd->event($newValue, $datetime);
        $count++;
    }
}

$today = date('Y-m-d');
$path = "/user/api/v2/systems/$sid/devices/meter/period/$eid?energy_level=minutely&date_range=$today";
$data = apiRequest($path, $appId, $appSecret, $baseUrl, $debugMode, $scenario);

$updates = 0;

if ($data && isset($data['data']['time'])) {
    $times = $data['data']['time'];
    $powers = $data['data']['power'];

    $cmdProduced = checkOrCreateCmd('produced_minutely', 'Puissance produite', $eqLogic);
    $cmdConsumed = checkOrCreateCmd('consumed_minutely', 'Puissance consommee', $eqLogic);
    $cmdImported = checkOrCreateCmd('imported_exported_minutely', 'Puissance importee-exportee', $eqLogic);

    foreach ($times as $i => $timeStr) {
        $timestamp = strtotime("$today $timeStr:00");
        if (isset($powers['produced'][$i])) saveIfDifferent($cmdProduced, $powers['produced'][$i], $timestamp, $updates, $scenario);
        if (isset($powers['consumed'][$i])) saveIfDifferent($cmdConsumed, $powers['consumed'][$i], $timestamp, $updates, $scenario);
        if (isset($powers['imported_exported'][$i])) saveIfDifferent($cmdImported, $powers['imported_exported'][$i], $timestamp, $updates, $scenario);
    }
    $scenario->setLog("Résumé : $updates mises à jour effectuées");
} else {
    $scenario->setLog('[ERREUR] Aucune donnée minutely trouvée ou format inattendu.');
}
---

Les commandes sont ensuite disponibles pour en faire ce que vous voulez …
SI vous modifiez le script, n’hesitez pas à partager

Norbert

Bonjour Norbert,

Bon, je vais me lancer le premier dans la question des nuls.
Jeedom 4.4.19
Je pense avoir tout bien fait, mais à l’enregistrement du scénario.

Argh, je vais recoller le scénario. D’ici à ce qu’un caractère foireux soit dedans !!

Oui le bouton copier.
A l’importation du template idem.

C’est étonnant d’avoir une erreur alors que le scénario n’est pas exécuté. Et même en désactivant le bloc code il refuse et fait la même erreur.

D’après Mistral Ai (oui je suis franchouillard) c’est qu’on essaie de mettre une valeur dans un champ qui attend un entier
Error code : 22007
L’erreur peut survenir lorsque vous essayez d’insérer une valeur incorrecte dans un champ qui attend un entier. Par exemple, une chaîne de caractères vide ou un caractère non numérique peut causer cette erreur

L’erreur que vous rencontrez est due à l’insertion d’une valeur de chaîne de caractères incorrecte dans la colonne expression de la table scenarioExpression de votre base de données Jeedom. Plus précisément, l’erreur indique que la valeur contient des caractères non valides, probablement des emojis ou des caractères spéciaux, qui ne sont pas supportés par le jeu de caractères actuel de votre base de données.

Non pareil avec ce bloc code même en mettant mes valeurs

c’est normal la clé à molette, je connais pas

Supprime là !

Eh ben voilà :grinning:

Je voulais bien faire avec de petits icônes :unamused:

Les infos remontent bien, vous pouvez y aller les gars :wave:t5:
Merci qui, merci Norbert :sweat_smile:
Il ne reste plus qu’a Noyax d’intégrer ça dans son plugin en stat pour satisfaire les gars qui lui ont déjà demandé.
Mais bon, ça nécessite l’api alors qu’il voulait rester en local dans le plugin, donc pas sur que ça le branche.

Je connais au moins 2 gars ici qui seraient intéressés :sweat_smile:

Tu penseras à changer ton appsecret que tu as publiée :see_no_evil:

Bonjour @rennais35000 ,

J’ai modifié légèrement le premier scenario (pour avoir des noms de commande plus uniformes). Tu peux supprimer toutes les commandes de l’équipement pour tout recréer (pas de perte de données tant que tu restes sur le même mois)

J’ai rajouté un scenario pour récupérer les données de puissance et reproduire le joli graphique qu’on trouve sur l’app ou le site APSystems

Norbert

Bonjour Norbert,
J’ai mis à jour et enlevé les 2 icones pour que ça s’enregistre.
Par contre le deuxième scénario ne me fait pas de graphe.
Est ce des fonctions de la 4.5 ?

[2025-07-14 19:05:51][SCENARIO] -- Début : Scenario lance manuellement.
[2025-07-14 19:05:51][SCENARIO] - Exécution du sous-élément de type [action] : action
[2025-07-14 19:05:51][SCENARIO] Exécution d'un bloc élément : 2143
[2025-07-14 19:05:51][SCENARIO] - Exécution du sous-élément de type [condition] : if #time# > 0800 && #time# < 2000
[2025-07-14 19:05:51][SCENARIO] Evaluation de la condition : [1905 > 0800 && 1905 < 2000] = Vrai
[2025-07-14 19:05:51][SCENARIO] - Exécution du sous-élément de type [action] : then
[2025-07-14 19:05:51][SCENARIO] Exécution d'un bloc élément : 2144
[2025-07-14 19:05:51][SCENARIO] - Exécution du sous-élément de type [action] : code
[2025-07-14 19:05:51][SCENARIO] Exécution d'un bloc code
[2025-07-14 19:05:51][SCENARIO] [SAVE] Puissance produite 2025-07-14 18:59:00 → 1779.6 W
[2025-07-14 19:05:51][SCENARIO] [SAVE] Puissance consommee 2025-07-14 18:59:00 → 328.5 W
[2025-07-14 19:05:52][SCENARIO] [SAVE] Puissance importee-exportee 2025-07-14 18:59:00 → -1451.1 W
[2025-07-14 19:05:52][SCENARIO] [SAVE] Puissance produite 2025-07-14 19:04:00 → 1016.67 W
[2025-07-14 19:05:52][SCENARIO] [SAVE] Puissance consommee 2025-07-14 19:04:00 → -31.07 W
[2025-07-14 19:05:52][SCENARIO] [SAVE] Puissance importee-exportee 2025-07-14 19:04:00 → -1047.74 W
[2025-07-14 19:05:52][SCENARIO] Résumé : 6 mises à jour effectuées
[2025-07-14 19:05:52][SCENARIO] Fin correcte du scénario

Non, c’est une vue

Du coup je suis sensé retrouver une vue dans le menu vue ?

Je crois que c’est plutôt à toi de la créer.
On ne peut pas importer des objets type vue dans jeedom.

Non, c’est à toi de la créer. Une zone de type graphe avec les trois commandes puissances dans le graphe

Norbert