Shelly Gen2 et scripts dont antenne bluetooth pour Nut et modules "beacon compatible"

Bonjour,

Je viens de découvrir que les Shelly Gen2 peuvent exécuter des scripts.
Y-a-t-il des utilisateurs qui ont mis en œuvre ces scripts?

EDIT : Les Shelly PLUS HT ne permettent pas l’exécution de script, certainement pour ne pas faire augmenter la température interne et fausser les mesures de température.

1 « J'aime »

Premier script.

Capture d’écran du 2023-01-12 18-10-43

1 « J'aime »

Adaptation d’un script trouvé sur la toile.
Cela remonte des adresses MAC Bluetooth.

function scanCB(ev, res) {
    if (ev === BLE.Scanner.SCAN_RESULT) {
        //let StaticMAC = { "addr": res.addr.slice(0,-2), "rssi": res.rssi };
        let StaticMAC = { "addr": res.addr, "rssi": res.rssi, "macType": res.addr_type};
        //print(JSON.stringify(StaticMAC));
        if (StaticMAC['addr'] === "xx:xx:xx:xx:xx:xx") {
            //print(JSON.stringify(StaticMAC));
            MQTT.publish('shellies/script/bluetoothMAC', JSON.stringify(StaticMAC), 0, false);
            //Shelly.emitEvent( "ShellyScanresult", StaticMAC);
         }
    }
}

BLE.Scanner.Start({ duration_ms: -1}, scanCB);
1 « J'aime »

Je viens de recevoir des nut.
Voila tout ce que je peux faire remonter dans le plugin-jmqtt et qui doit pouvoir permettre de gérer la présence.

function scanCB(ev, res) {
    if (ev === BLE.Scanner.SCAN_RESULT) {
        if (res.addr === "dc:5f:xx:xx:xx:xx") {
            let unixtime = Shelly.getComponentStatus("sys").unixtime;
            MQTT.publish('shellies/script/bluetooth/nut/addr', res.addr, 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/addr_type', JSON.stringify(res.addr_type), 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/advData', JSON.stringify(res.advData), 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/scanRsp', JSON.stringify(res.scanRsp), 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/rssi', JSON.stringify(res.rssi), 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/flags', JSON.stringify(res.flags), 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/local_name', JSON.stringify(res.local_name), 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/manufacturer_data', JSON.stringify(res.manufacturer_data), 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/service_uuids', JSON.stringify(res.service_uuids), 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/service_data', JSON.stringify(res.service_data), 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/tx_power_level', JSON.stringify(res.tx_power_level), 0, false);
            MQTT.publish('shellies/script/bluetooth/nut/unixtime', JSON.stringify(unixtime), 0, false);
        }
    }
}

BLE.Scanner.Start({ duration_ms: -1}, scanCB);
1 « J'aime »

Bonjour,

SI je comprends bien, tu utilises les shelly GEN2 comme détecteur de la présence du NUT à proximité ?

J’utilise ces shelly pour ce qu’ils sont mais en plus j’essaye d’exploiter leur partie bluetooth.
Pour la gestion de présence avec les nut, un scénario avec comme déclencheur la commande info unixtime va faire le job.
La suite va être d’essayer de décoder advData qui doit permettre de remonter les infos de capteur bluethooth.

Modification du script pour pouvoir utiliser plusieurs nut.

function scanCB(ev, res) {
  if (ev === BLE.Scanner.SCAN_RESULT) {
    if (res.local_name === 'nut') {
      let topic = 'shellies/script/bluetooth/nut/' + res.addr + '/';
      let unixtime = Shelly.getComponentStatus("sys").unixtime;
      MQTT.publish(topic + 'addr', res.addr, 0, false);
      MQTT.publish(topic + 'addr_type', JSON.stringify(res.addr_type), 0, false);
      MQTT.publish(topic + 'advData', JSON.stringify(res.advData), 0, false);
      MQTT.publish(topic + 'scanRsp', JSON.stringify(res.scanRsp), 0, false);
      MQTT.publish(topic + 'rssi', JSON.stringify(res.rssi), 0, false);
      MQTT.publish(topic + 'flags', JSON.stringify(res.flags), 0, false);
      MQTT.publish(topic + 'local_name', JSON.stringify(res.local_name), 0, false);
      MQTT.publish(topic + 'manufacturer_data', JSON.stringify(res.manufacturer_data), 0, false);
      MQTT.publish(topic + 'service_uuids', JSON.stringify(res.service_uuids), 0, false);
      MQTT.publish(topic + 'service_data', JSON.stringify(res.service_data), 0, false);
      MQTT.publish(topic + 'tx_power_level', JSON.stringify(res.tx_power_level), 0, false);
      MQTT.publish(topic + 'unixtime', JSON.stringify(unixtime), 0, false);
    }
  }
}

BLE.Scanner.Start({ duration_ms: -1}, scanCB);

Pour les Nut, script final.

function scanCB(ev, res) {
  if (ev === BLE.Scanner.SCAN_RESULT) {
    if (res.local_name === 'nut') {
      let topic = 'shellies/script/bluetooth/nut/' + res.addr + '/';
      let unixtime = Shelly.getComponentStatus("sys").unixtime;
      MQTT.publish(topic + 'rssi', JSON.stringify(res.rssi), 0, false);
      MQTT.publish(topic + 'unixtime', JSON.stringify(unixtime), 0, false);
      MQTT.publish(topic + 'presence', JSON.stringify(unixtime), 0, false);
    }
  }
}

BLE.Scanner.Start({ duration_ms: -1}, scanCB);

Résultat avec 2 Nut.

Capture d’écran du 2023-01-17 20-37-14

Malgré que cela ne soit pas nécessaire pour la gestion de présence avec les Nut, pour d’autres matériels transmettant des données, il serait intéressant de pouvoir décoder les trames Bluetooth.
Il semble exister un projet bien avancé sachant faire cette tâche, theengs-decoder.

@Bad, penses-tu que ce projet pourrait être incorporé dans le #plugin-jmqtt ?

Hello Jeandhom,

Le plugin est déjà super complex, je ne pense pas que traiter des payloads BT soit son but.

Pour moi ce serait plus le travail d’un docker, qui ferait tourner theengs/decoder afin de prendre les payloads BT en MQTT et les renvoyer en MQTT, pour être ensuite intégrés dans des eq/cmd avec jMQTT.

Ca pourrait aussi être un plugin à part s’appuyant sur MQTT Manager et créant directement les équipements dans Jeedom.

Désolé,
Bad

2 « J'aime »

Salut Bad,
Merci pour ton expertise.

Je n’ai qu’un seul shelly PLUS en service, il est au RDC.
Malgré qu’il capte bien le Nut situé au sous-sol, je projette de remplacer le shelly 1 de la porte de garage situé aussi au sous-sol par un shelly PLUS 1.
Afin de savoir qui a remonté l’information affichée par le widget, j’ai modifié le script.

let origine = JSON.stringify(Shelly.getDeviceInfo().name);
function scanCB(ev, res) {
  if (ev === BLE.Scanner.SCAN_RESULT) {
    if (res.local_name === 'nut') {
      let topic = 'shellies/script/bluetooth/nut/' + res.addr + '/';
      let unixtime = JSON.stringify(Shelly.getComponentStatus("sys").unixtime);
      if(MQTT.isConnected()) {
        MQTT.publish(topic + 'rssi', JSON.stringify(res.rssi), 0, false);
        MQTT.publish(topic + 'unixtime', unixtime, 0, false);
        MQTT.publish(topic + 'presence', unixtime, 0, false);
        MQTT.publish(topic + 'origine', origine, 0, false);
      }
    }
  }
}

BLE.Scanner.Start({ duration_ms: -1}, scanCB);

Capture d’écran du 2023-03-03 21-56-46

Après de l’autoformation en JS et puis plus particulièrement en mJS, je vous propose une amélioration de ce script.
L’ancien script a besoin de Jeedom via un scénario pour détecter l’absence d’un Nut.
Maintenant, c’est le script, lui même, qui signale directement au broker l’absence d’un Nut.

Pour ce faire, j’ai ajouté deux modules.

  • le premier sert à connaître, via le broker, le dernier timestamp de présence de chaque Nut.
  • le deuxième est un timer qui surveille, toutes les dix secondes, si chaque Nut n’a pas signalé sa présence depuis les quatre-vingt-dix dernières secondes, puis le publie sur le broker.

EDIT - La fonction scanCB semble, rarement, renvoyer une adresse MAC non valide. J’ajoute au script une vérification de cette adresse.

let origine = JSON.stringify(Shelly.getDeviceInfo().name);
let genericTopic = "shellies/script/bluetooth/nut/";
let nuts = [];
let callback = [genericTopic, nuts];

function isMacValid(mac) {
  if (mac.length !== 17) {
    return false;
  }
  for (let i=0; i<17; i++) {
    if (i%3 === 2) {
      if (mac.at(i) !== 0x3a) { //0x3a = :
        return false;
      }
    } else {
      //0x30 = 0, 0x39 = 9, 0x61 = a et 0x66 = f
      if (mac.at(i) < 0x30 || (mac.at(i) > 0x39 && mac.at(i) < 0x61) || mac.at(i) > 0x66) {
        return false;
      }
    }
  }
  return true;
}

MQTT.subscribe(
  genericTopic + "#",
  function (topic, message, callback) {
    let endTopic = topic.slice(callback[0].length + 18);
    if (endTopic === "unixtime") {
      let mac = topic.slice(callback[0].length, callback[0].length + 17);
      let existePas = 1;
      for (let i=0; i<callback[1].length; i++) {
        if (callback[1][i].mac === mac) {
          callback[1][i].timePublish = JSON.parse(message);
          existePas = 0;
        }
      }
      if (existePas === 1 && isMacValid(mac)) {
        callback[1].push({mac: mac, timePublish: JSON.parse(message)});
      }
    }
  },
  callback
);

Timer.set(
  10000,
  true,
  function (callback) {
    for (let i=0; i<callback[1].length; i++) {
      if (callback[1][i].timePublish + 90 <= Shelly.getComponentStatus("sys").unixtime) {
        if(MQTT.isConnected()) {
          MQTT.publish(callback[0] + callback[1][i].mac + '/presence', "0", 0, false);
        }
      }
    }
  },
  callback
);

function scanCB(ev, res, genericTopic) {
  if (ev === BLE.Scanner.SCAN_RESULT) {
   if (res.local_name === 'nut' && isMacValid(res.addr)) {
      let topic = genericTopic + res.addr + '/';
      let unixtime = JSON.stringify(Shelly.getComponentStatus("sys").unixtime);
      if(MQTT.isConnected()) {
        MQTT.publish(topic + 'rssi', JSON.stringify(res.rssi), 0, false);
        MQTT.publish(topic + 'unixtime', unixtime, 0, false);
        MQTT.publish(topic + 'presence', "1", 0, false);
        MQTT.publish(topic + 'origine', origine, 0, false);
      }
    }
  }
}

BLE.Scanner.Start({ duration_ms: -1}, scanCB, genericTopic);
2 « J'aime »

Ajout d’une commande info par Shelly permettant de connaître le nombre de Nuts présent dans sa zone de réception.

Capture d’écran du 2023-05-22 19-13-28

let origine = Shelly.getDeviceInfo().name;
let genericTopic = "shellies/script/scanNut/";
let nutsShelly = []; //nuts vus par ce Shelly
let nutsBroker = []; //nuts vus par le broker

if(MQTT.isConnected()) {
  MQTT.publish(genericTopic + origine + '/nombreNut', "0", 0, false);
}

function isMacValid(mac) {
  if (mac.length !== 17) {
    return false;
  }
  for (let i=0; i<17; i++) {
    if (i%3 === 2) {
      if (mac.at(i) !== 0x3a) { //0x3a = :
        return false;
      }
    } else {
      //0x30 = 0, 0x39 = 9, 0x61 = a et 0x66 = f
      if (mac.at(i) < 0x30 || (mac.at(i) > 0x39 && mac.at(i) < 0x61) || mac.at(i) > 0x66) {
        return false;
      }
    }
  }
  return true;
}

function nutAjout (mac, unixtime, nuts) {
  let existePas = 1;
  for (let i=0; i<nuts.length; i++) {
    if (nuts[i].mac === mac) {
      nuts[i].timePublish = unixtime;
      existePas = 0;
    }
  }
  if (existePas === 1 && isMacValid(mac)) {
    nuts.push({mac: mac, timePublish: unixtime});
  }
}

MQTT.subscribe(
  genericTopic + "#",
  function (topic, message, callback) {
    let endTopic = topic.slice(callback[0].length + 18);
    if (endTopic === "unixtime") {
      let mac = topic.slice(callback[0].length, callback[0].length + 17);
      nutAjout (mac, JSON.parse(message), callback[1]);
    }
  },
  [genericTopic, nutsBroker]
);

Timer.set(
  10000,
  true,
  function (callback) {
    for (let i=0; i<callback[1].length; i++) {
      if (callback[1][i].timePublish + 110 <= Shelly.getComponentStatus("sys").unixtime) {
        if(MQTT.isConnected()) {
          MQTT.publish(callback[0] + callback[1][i].mac + '/presence', "0", 0, false);
        }
      }
    }
    for (let i=0; i<callback[3].length; i++) {
      if (callback[3][i].timePublish + 220 <= Shelly.getComponentStatus("sys").unixtime) {
        callback[3].splice(i, 1);
        if(MQTT.isConnected()) {
          MQTT.publish(callback[0] + callback[2] + '/nombreNut', JSON.stringify(callback[3].length), 0, false);
        }
      }
    }
  },
  [genericTopic, nutsBroker, origine, nutsShelly]
);

function scanCB(ev, res, callback) {
  if (ev === BLE.Scanner.SCAN_RESULT) {
    if (res.local_name === 'nut' && isMacValid(res.addr)) {
      let mac = res.addr;
      let topic = callback[0] + mac + '/';
      let unixtime = Shelly.getComponentStatus("sys").unixtime;
      nutAjout (mac, unixtime, callback[1]);
      if(MQTT.isConnected()) {
        MQTT.publish(topic + 'rssi', JSON.stringify(res.rssi), 0, false);
        MQTT.publish(topic + 'unixtime', JSON.stringify(unixtime), 0, false);
        MQTT.publish(topic + 'presence', "1", 0, false);
        MQTT.publish(topic + 'origine', JSON.stringify(callback[2]), 0, false);
        MQTT.publish(callback[0] + callback[2] + '/nombreNut', JSON.stringify(callback[1].length), 0, false);
      }
    }
  }
}

BLE.Scanner.Start({ duration_ms: -1}, scanCB, [genericTopic, nutsShelly, origine]);
1 « J'aime »

Ajout de la liste des Nuts pour Chaque Shelly.

let origine = Shelly.getDeviceInfo().name;
let genericTopic = "shellies/script/scanNut/";
let nutsShelly = []; //nuts vus par ce Shelly
let nutsBroker = []; //nuts vus par le broker
let nutsName = {
                 "aa:aa:aa:aa:aa:aa": "Nut01",
                 "bb:bb:bb:bb:bb:bb": "Nut02",
                 "cc:cc:cc:cc:cc:cc": "Nut03",
                 "dd:dd:dd:dd:dd:dd": "Nut04"
               };

if(MQTT.isConnected()) {
  MQTT.publish(genericTopic + origine + '/nombreNut', "0", 0, false);
  MQTT.publish(genericTopic + origine + '/nameNut', "", 0, false);
}

function isMacValid(mac) {
  if (mac.length !== 17) {
    return false;
  }
  for (let i=0; i<17; i++) {
    if (i%3 === 2) {
      if (mac.at(i) !== 0x3a) { //0x3a = :
        return false;
      }
    } else {
      //0x30 = 0, 0x39 = 9, 0x61 = a et 0x66 = f
      if (mac.at(i) < 0x30 || (mac.at(i) > 0x39 && mac.at(i) < 0x61) || mac.at(i) > 0x66) {
        return false;
      }
    }
  }
  return true;
}

function nutList (nuts, nutsName) {
  let nutsList = "";
  for (let i=0; i<nuts.length; i++) {
    nutsList += nutsName[nuts[i].mac] + " ";
  }
  let long = nutsList.length - 1;
  nutsList = nutsList.slice(0, long);
  return nutsList;
}

function nutAjout (mac, unixtime, nuts) {
  let existePas = 1;
  for (let i=0; i<nuts.length; i++) {
    if (nuts[i].mac === mac) {
      nuts[i].timePublish = unixtime;
      existePas = 0;
    }
  }
  if (existePas === 1 && isMacValid(mac)) {
    nuts.push({mac: mac, timePublish: unixtime});
  }
}

MQTT.subscribe(
  genericTopic + "#",
  function (topic, message, callback) {
    let endTopic = topic.slice(callback[0].length + 18);
    if (endTopic === "unixtime") {
      let mac = topic.slice(callback[0].length, callback[0].length + 17);
      nutAjout (mac, JSON.parse(message), callback[1]);
    }
  },
  [genericTopic, nutsBroker]
);

Timer.set(
  10000,
  true,
  function (callback) {
    for (let i=0; i<callback[1].length; i++) {
      if (callback[1][i].timePublish + 110 <= Shelly.getComponentStatus("sys").unixtime) {
        if(MQTT.isConnected()) {
          MQTT.publish(callback[0] + callback[1][i].mac + '/presence', "0", 0, false);
        }
      }
    }
    for (let i=0; i<callback[3].length; i++) {
      if (callback[3][i].timePublish + 110 <= Shelly.getComponentStatus("sys").unixtime) {
        callback[3].splice(i, 1);
        let list = nutList (callback[3], callback[4]);
        if(MQTT.isConnected()) {
          MQTT.publish(callback[0] + callback[2] + '/nombreNut', JSON.stringify(callback[3].length), 0, false);
          MQTT.publish(callback[0] + callback[2] + '/nameNut', list, 0, false);
        }
      }
    }
  },
  [genericTopic, nutsBroker, origine, nutsShelly, nutsName]
);

function scanCB(ev, res, callback) {
  if (ev === BLE.Scanner.SCAN_RESULT) {
    if (res.local_name === 'nut' && isMacValid(res.addr)) {
      let mac = res.addr;
      let topic = callback[0] + mac + '/';
      let unixtime = Shelly.getComponentStatus("sys").unixtime;
      nutAjout (mac, unixtime, callback[1]);
      let list = nutList (callback[1], callback[3]);
      if(MQTT.isConnected()) {
        MQTT.publish(topic + 'rssi', JSON.stringify(res.rssi), 0, false);
        MQTT.publish(topic + 'unixtime', JSON.stringify(unixtime), 0, false);
        MQTT.publish(topic + 'presence', "1", 0, false);
        MQTT.publish(topic + 'origine', JSON.stringify(callback[2]), 0, false);
        MQTT.publish(callback[0] + callback[2] + '/nombreNut', JSON.stringify(callback[1].length), 0, false);
        MQTT.publish(callback[0] + callback[2] + '/nameNut', list, 0, false);
      }  
    }
  }
}

BLE.Scanner.Start({ duration_ms: -1}, scanCB, [genericTopic, nutsShelly, origine, nutsName]);

Ajout du monitoring de l’état (stop/start) du script.

Topic : topicDuShelly/status/script:1 avec [running].

Depuis la version 1.0.3 du firmware des Shelly Gen2, le script ne fonctionne plus car la fonction MQTT.subscribe() ne supporte plus les topics finissant par # (c’est gênant!).
Les développeurs d’Allterco Robotics sont informés du bug et ils sont dessus.

Salut @Jeandhom ,

Tout d’abord merci pour ce bout de script qui collait à la perfection à mon besoin ! :pray:

J’ai essayé de passer mon Shelly I4+ en version stable 1.0.7 hier. Manifestement cela n’a pas résolu le problème de calcul de la présence. A confirmer.

Cela me renvoi toujours les données principales donc je calcul la présence dans Jeedom, moindre mal. Je garde aussi sous le coude la possibilité de remonter la présence d’à peu près n’importe quel équipement bluetooth à proximité. C’est top !

Encore merci à toi pour ce partage !! :grin:

1 « J'aime »

Le support Shelly m’a confirmé que ce soucis n’était pas réglé avec le firmware 1.0.7.

EDIT: ni avec le firmware 1.0.8.

à suivre donc !
superbe idée en tous cas, merci.

1 « J'aime »