Creality Cloud 0.62 : 8 défauts rencontrés, avec correctifs

Bonjour,

Merci pour le plugin, et pour la correction du curl_getinfo($http) dans la 0.62. En le mettant en service chez moi (Jeedom 4.6.1, PHP 8, trois machines : K1 Max, i7 et Halot-Sky), je suis tombé sur les défauts ci-dessous. Tous sont corrigés en local et tournent depuis le 16/09 sans erreur. Le correctif complet contre la 0.62 est joint en fin de message.

Bloquants

1. login() n’est pas statique mais est appelée en statique.
public function login() est appelée par self::login() dans request(). En PHP 8, appeler en statique une méthode non statique lève une Error fatale : chaque requête à l’API meurt, et aucun équipement n’est jamais créé.
Correctif : public static function login(). La méthode n’utilise pas $this.

2. Une clé de télémétrie vide fait tomber toute la boucle.
Dans getDevices(), l’API values/attributes renvoie en premier un élément {"lastUpdateTs":…,"value":null,"key":""}. loadCmdFromConf("") crée alors une commande sans nom et save() lève une exception, sur le premier élément. Rien n’est mis à jour.
Correctif : ignorer une clé vide.

3. Pas de garde sur le retour de loadCmdFromConf().
Dans les deux boucles (attributes et timeseries), event() est appelé même si aucune commande n’a été obtenue.
Correctif : if (!is_object($cmd)) continue; (utile aussi avec le point 4).

Fonctionnement

4. L’option « Toutes les commandes » est sans effet (priorité d’opérateurs).
Dans loadCmdFromConf() :
if ($param_cmd === null || !is_array($param_cmd) && config::byKey('allCmd', __CLASS__))
Le && est évalué avant le ||. Une clé inconnue crée donc toujours une commande brute, option cochée ou non. Les commandes supprimées reviennent à chaque passage du cron.
Correctif : tester allCmd séparément et renvoyer null quand l’option est décochée.

5. « Synchroniser » ne rapatrie aucune valeur.
synchronize() appelle getDevices() sans true. Les imprimantes sont découvertes, mais les valeurs n’arrivent qu’au passage suivant du cron (15 min). Pour l’utilisateur, le bouton « tourne pour rien ».
Correctif : self::getDevices(true);

6. Le dossier des vignettes n’existe pas dans le paquet.
Les images sont écrites dans plugins/Creality_Cloud/data/img/, qui n’est pas livré. file_put_contents() échoue et aucune machine n’a d’image. Il faut le créer à l’installation, ou ajouter un .gitkeep.

7. La correspondance de modèle est trop stricte.
getPrinterModel() compare $list['name'] == trim($_model). Or le firmware n’annonce pas toujours le nom du catalogue : la K1 Max annonce CR-K1 Max alors que le catalogue dit K1 Max.
Correctif proposé : une seconde passe tolérante (minuscules, préfixes cr- / sonic_ / fast_ retirés, ponctuation supprimée), après l’égalité stricte. Reste un cas sans solution : l’i7 annonce F022 (version matérielle CR3CN250101C13), un code absent du catalogue.

Confidentialité

8. Le mot de passe du compte Creality est écrit en clair dans le journal.
En niveau debug, login() journalise json_encode($postfield), mot de passe compris.
Correctif : masquer le champ password dans la ligne de journal uniquement. Attention à ne pas toucher le json_encode($postfield) envoyé par setPost() : le même fragment y figure.

Remarques

  • Tout le diagnostic est en debug, y compris les échecs d’authentification. Au niveau par défaut, le journal reste vide et l’utilisateur ne voit rien. Un error sur l’échec de login() aiderait beaucoup.
  • Activer le plugin remet le niveau de journal à default. Il faut le repasser en debug après l’activation, pas avant.
  • Cloudflare répond parfois par une page HTML « Just a moment… » au lieu du JSON attendu (3 requêtes sur une nuit chez moi). Détecter une réponse non JSON et la journaliser clairement éviterait des erreurs peu parlantes.
  • Les valeurs ne portent que la date de leur dernier changement. Une machine allumée mais inactive affiche « En ligne » à la date de sa dernière impression, ce qui ressemble à une panne. Une indication de fraîcheur serait utile.

Bonne continuation, et merci encore pour le travail.

Le correctif (diff contre la 0.62) :

--- a/core/class/Creality_Cloud.class.php
+++ b/core/class/Creality_Cloud.class.php
@@ -424,6 +424,13 @@
      *
      * @return array|null Les informations du modèle d'imprimante si trouvé, sinon null.
      */
+    private static function normaliseModele($_nom)
+    {
+        $x = mb_strtolower(trim((string)$_nom));
+        $x = preg_replace('/^(sonic_|fast_|cr-)/', '', $x);
+        return preg_replace('/[^a-z0-9]/', '', $x);
+    }
+
     public static function getPrinterModel($_model)
     {
         $printerList = self::request(self::PATH_DEVICEPRINTERLIST_URL, array('version' => 1));
@@ -434,6 +441,16 @@
                         return $list;
                     }
                 }
+                // 2e passe : le firmware annonce parfois un nom different du catalogue
+                // (ex. « CR-K1 Max » cote machine, « K1 Max » cote catalogue)
+                $cible = self::normaliseModele($_model);
+                if ($cible !== '') {
+                    foreach ($printerList['result']['list'] as $list) {
+                        if (self::normaliseModele($list['name']) === $cible) {
+                            return $list;
+                        }
+                    }
+                }
             }
         }
     }
@@ -499,6 +516,7 @@
                             $telemetry = self::request(self::PATH_TELEMETRY_URL . $list['tbId'] . '/values/attributes?keys='/*tfCard,state,active,model,connect,printId,boxVersion,upgradeStatus,upgrade,bedTemp2,fan,nozzleTemp2,setPosition,lastActivityTime,layer,printStartTime,tfCard,curPosition'*/);
                             if (isset($telemetry['result'])) {
                                 foreach ($telemetry['result'] as $resultValue) {
+                                    if (!isset($resultValue['key']) || trim((string)$resultValue['key']) === '') { continue; }
                                     log::add(__CLASS__, 'debug', __FUNCTION__ . ' attributes ' . __('Key = ', __FILE__) . $resultValue['key'] . " - value = " . $resultValue['value']);
                                     $cmd = null;
                                     $existing_cmd = $eqLogic->getCmd('info', $resultValue['key']);
@@ -508,6 +526,7 @@
                                     if ($cmd === null || !is_object($cmd)) { // si n'existe pas, on crée la cmd depuis le fichier de conf
                                         $cmd = $eqLogic->loadCmdFromConf($resultValue['key']);
                                     }
+                                    if (!is_object($cmd)) { log::add(__CLASS__, 'debug', __FUNCTION__ . ' cle ignoree : ' . $resultValue['key']); continue; }
                                     if (is_bool($resultValue['value'])) {
                                         $resultValue['value'] = $resultValue['value'] ? 'true' : 'false';
                                     }
@@ -543,6 +562,7 @@
                                     if ($cmd === null || !is_object($cmd)) { // si n'existe pas, on crée la cmd depuis le fichier de conf
                                         $cmd = $eqLogic->loadCmdFromConf($key);
                                     }
+                                    if (!is_object($cmd)) { log::add(__CLASS__, 'debug', __FUNCTION__ . ' cle ignoree : ' . $key); continue; }
                                     if (is_bool($value[0]['value'])) {
                                         $value[0]['value'] = $value[0]['value'] ? 'true' : 'false';
                                     }
@@ -577,7 +597,7 @@
         if (config::byKey('getSlices', __CLASS__)) {
             self::getSlices();
         }
-        self::getDevices();
+        self::getDevices(true); // rapatrie aussi les valeurs, sinon le bouton ne montre rien
         log::add(__CLASS__, 'debug', __FUNCTION__ . __(' fin', __FILE__));
     }
 
@@ -698,7 +718,7 @@
      * @return array|bool Un tableau contenant le token d'authentification et l'identifiant de l'utilisateur en cas de succès,
      *                   ou FALSE en cas d'échec de la connexion.
      */
-    public function login()
+    public static function login()
     {
         //type 1 : numéro de mobile
         if (config::byKey('mobileormail', __CLASS__)) {
@@ -724,7 +744,7 @@
             'account'   => $account,
             'password'  => $password
         );
-        log::add(__CLASS__, 'debug', __FUNCTION__ . __(' postfield ', __FILE__) . json_encode($postfield));
+        log::add(__CLASS__, 'debug', __FUNCTION__ . __(' postfield ', __FILE__) . json_encode(array_merge($postfield, array('password' => '(masque)'))));
 
         try {
             $http = new com_http(self::buildUrl(self::PATH_LOGIN_URL));
@@ -892,7 +912,8 @@
             }
         }
 
-        if ($param_cmd === null || !is_array($param_cmd) && config::byKey('allCmd', __CLASS__)) {// si on a pas récupéré les paramètres, on ajoute manuellement
+        if ($param_cmd === null || !is_array($param_cmd)) {
+            if (!config::byKey('allCmd', __CLASS__)) { return null; } // cle inconnue et « Toutes les commandes » decoche : on ignore (le && passait avant le ||)// si on a pas récupéré les paramètres, on ajoute manuellement
             $cmd->setLogicalId($param);
             $cmd->setName($param);
             $cmd->setType('info');

Merci pour ces retours extrêmement détaillés, j’ai bien perdu mon temps pour certaines à comprendre d’où venait le problème.

Le problème n’arrivait que lorsqu’une authentification était nécessaire. Donc non, 1 seule requête API « mourait », pas « chaque ».

15 minutes ? D’où ça sort, ça ?


C’est d’ailleurs le fonctionnement voulu, mais vu que ça gêne l’IA, je vais ajouter un rafraichissement juste après découverte.

C’est le but, afficher des logs que quand on le demande (debug) et pas polluer quand on le demande pas.

:laughing:
C’est le fonctionnement des plugins jeedom qui est remis en question ?

Une date ancienne sur une valeur ne prouve pas une panne : elle correspond au dernier changement signalé
Une date distincte de dernière lecture du cloud a été ajoutée.

J’ai normalisé pour l’ensemble du parc Creality/SPARKX. cf. CrealityPrint/resources/profiles/Creality.json at master · CrealityOfficial/CrealityPrint · GitHub
Pas de F022 chez Spanx i7, hallucination ?

Merci pour le retour rapide et pour le correctif du rafraîchissement après découverte, c’est le point qui gênait le plus au quotidien.

Pour les logs en debug, c’est noté, le choix se comprend.

Sur le modèle « F022 » en revanche, ce n’est pas une valeur inventée de mon côté : c’est l’imprimante elle-même qui l’annonce. Sur ma SPARKX i7, la télémétrie locale renvoie :

  • model: F022
    • modelVersion: printer hw ver:CR3CN250101C13; printer sw ver:1.1.5.8; …
      Donc « F022 » est bien le code interne remonté par cette i7, même s’il n’apparaît pas dans le catalogue des modèles. Si la normalisation s’appuie sur le fichier officiel Creality, il faudrait que « F022 » (ou ce hardware CR3CN250101C13) y soit rattaché à « SPARKX i7 ». Je peux fournir un dump complet des attributs si ça t’aide à la mapper.

Merci encore pour le travail sur le plugin.