Dashboard HTML suivi de vos batteries

Bonjour à tous,

Je vous propose un petit partage/tuto d’un dashboard de suivi des batteries construit en HTML, CSS et Javascript via Node-Red :

Le flow est surement pas parfait, car c’est mon premier « projet » sur NodeRed
Je n’ai pas utilisé le node template pour la page HTML car je n’arrivais pas à faire ce que je veux en mustache pour boucler sur la liste des batteries, je me suis donc rabattu vers un langage que je connais mieux, le JS.

Je suis donc ouvert à toutes critiques et idées d’améliorations :slight_smile:

Rendu final PC:


Rendu Mobile:

Pré-réquis :
HA, Node-red, Optionnel Battery Note :
image
Permet d’avoir des infos supplémentaires sur les batteries de vos capteurs (type de battery, nombre, date de remplacement etc…)

Pour la partie HTML et CSS je me suis basé sur le projet de Bedimcode :

Voici le petit Flow créer pour générer le dashboard :

Le déclencheur du flow est un simple horodatage avec un run toutes les 60 minutes, mais vous pouvez bien entendu également lier le get all battery avec un state event ou autre déclencheur.

Le get all battery est aussi très simple puisqu’il récupère tous les capteurs avec une classe battery. C’est dans le node Normalize que je fais un tri et retire les doublons liés au fait que j’utilise battery note.
image

La partie un peu plus complexe (c’est un grand mot ^^ ) commence ici: Normalize Battery Level

Globalement, ce nœud me permet de filtrer plus finement mes capteurs et surtout de dédoublonner mes entitées battery du fait que j’utilise l’intégration battery note qui créer une entité supplémentaire qui s’appelle battery_plus. J’ai également deux intégration Xiaomi (Miot auto et gateway 3 pour mes capteurs Bluetooth )

Dans la première partie du code, je garde uniquement les entitées dont le nom inclus ‹ battery_plus › cela me permet directement de supprimer l’entité standard battery de home assistant.
Ensuite le « parseInt » permet de transformer le texte de valeur de la battery en format Integer (nombre entier) ainsi les valeurs de battery qui serait unknow par exemple, seront transformés en « NaN » not a number et pourront facilement être exclu par la suite.

Ensuite je modifie les friendly name de mes entités pour les raccourcir et retirer le terme Batterie+ de leur nom.
Pour terminer cette première partie, je fini sur un .sort qui me permet de mettre les entités dans l’ordre croissant de leur valeur de batterie (Les batteries les plus faibles en premier)

Ensuite la deuxième partie du code est une boucle qui me permet de stocker les valeurs de batteries en fonction de leur valeur dans 5 grandes catégories ( je ne m’en suis pas servie dans le reste de ma page, mais à la base je m’étais dit que ça pourrait être utile… j’ai donc laissé cette partie qui ne dérange pas pour la suite) [100-75] [75-50][50-25][25-10][10-0]

Maintenant que les données ont été triées et ordonnées, on va pouvoir générer un page HTML dynamique. Je vais simplement vous expliquer la partie script, le HTML n’a rien de particulier.

Donc la petite subtilité ici, c’est que les capteurs sont dans une map qui contient des listes.
J’ai donc une première boucle qui contient 5 listes à traiter et ensuite dans la seconde boucle, j’ai mes différentes batteries.
Je génère donc une nouvelle carte de batterie pour chacune avec les informations que je désire afficher et je la rajoute à ma page HTML d’origine.

Ensuite l’autre partie de script sera exécuté directement dans la page HTML et non dans node-Red.
Ce script sert à générer dynamiquement le CSS de vos différentes batteries pour ajuster le visuel a son niveau.

Pour terminer on sauvegarde simplement cette page HTML dans Home assistant :


Il faudra adapter le chemin en fonction de l’url que vous aller indiquer dans votre config HA.
Et, également de comment tourne votre serveur Red ( Docker ou plugin HA )

Config HA pour avoir le panel de suivi des batteries qui pointe vers l’emplacement de stockage de la page HTML:
image

Le flow pour jouer avec :wink:

[{"id":"b7e2f52708e15f8a","type":"comment","z":"e453ff62415924bf","name":"Batteries Dashboard","info":"","x":230,"y":920,"wires":[]},{"id":"72efcb545bfc0e84","type":"ha-get-entities","z":"e453ff62415924bf","name":"Get All Battery","server":"01bc5675ea304f7f","version":1,"rules":[{"property":"attributes.device_class","logic":"is","value":"battery","valueType":"str"}],"outputType":"array","outputEmptyResults":true,"outputLocationType":"msg","outputLocation":"payload","outputResultsCount":1,"x":430,"y":960,"wires":[["9c0e8a73c7402aef"]]},{"id":"28cf29e7b576a96e","type":"inject","z":"e453ff62415924bf","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"3600","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":230,"y":960,"wires":[["72efcb545bfc0e84"]]},{"id":"06d378c662f594d3","type":"function","z":"e453ff62415924bf","name":"Webpage","func":"const moment = global.get('moment');\nconst parisTime = moment().tz(\"Europe/Paris\").locale('fr').format('LLLL');\nvar batteryLevelMap = msg.payload;\nvar allItems = [];\nvar htmlPage = `<!DOCTYPE html>\n <html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"refresh\" content=\"60\">\n\n <!--=============== REMIXICONS ===============-->\n <link href=\"https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css\" rel=\"stylesheet\">\n\n <!--=============== CSS ===============-->\n <style>\n /*=============== GOOGLE FONTS ===============*/\n @import url(\"https://fonts.googleapis.com/css2?family=Rubik:wght@400;600&display=swap\");\n\n /*=============== VARIABLES CSS ===============*/\n .grid {\n display: grid;\n grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));\n gap: 10px;\n }\n :root {\n /*========== Colors ==========*/\n /*Color mode HSL(hue, saturation, lightness)*/\n --gradient-color-red: linear-gradient(90deg, \n hsl(7, 89%, 46%) 15%,\n hsl(11, 93%, 68%) 100%);\n --gradient-color-orange: linear-gradient(90deg, \n hsl(22, 89%, 46%) 15%,\n hsl(54, 90%, 45%) 100%);\n --gradient-color-yellow: linear-gradient(90deg, \n hsl(54, 89%, 46%) 15%,\n hsl(92, 90%, 45%) 100%);\n --gradient-color-green: linear-gradient(90deg, \n hsl(92, 89%, 46%) 15%,\n hsl(92, 90%, 68%) 100%);\n --text-color: #fff;\n --body-color: hsl(0, 0%, 11%);\n --container-color: hsl(0, 0%, 9%);\n\n /*========== Font and typography ==========*/\n /*.5rem = 8px | 1rem = 16px ...*/\n --body-font: 'Rubik', sans-serif;\n\n --biggest-font-size: 2.5rem;\n --normal-font-size: .938rem;\n --smaller-font-size: .75rem;\n }\n\n /* Responsive typography */\n @media screen and (min-width: 968px) {\n :root {\n --biggest-font-size: 2.75rem;\n --normal-font-size: 1rem;\n --smaller-font-size: .813rem;\n }\n }\n\n /*=============== BASE ===============*/\n * {\n box-sizing: border-box;\n padding: 0;\n margin: 0;\n }\n\n body {\n font-family: var(--body-font);\n font-size: var(--normal-font-size);\n background-color: var(--body-color);\n color: var(--text-color);\n }\n\n /*=============== BATTERY ===============*/\n .battery {\n height: 100vh;\n display: grid;\n place-items: center;\n margin: 0 1.5rem;\n }\n\n .battery__card {\n position: relative;\n width: 100%;\n height: 240px;\n background-color: var(--container-color);\n padding: 1.5rem 2rem;\n border-radius: 1.5rem;\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n align-items: center;\n }\n .battery__data {\n height: -webkit-fill-available;\n display: grid;\n }\n .battery__text {\n margin-bottom: 0.5rem;\n font-size: large;\n font-weight: bolder;\n width: max-content;\n }\n\n .battery__percentage {\n font-size: var(--biggest-font-size);\n margin: 0 0 auto 0;\n }\n\n .battery__status {\n position: absolute;\n bottom: 1.5rem;\n display: flex;\n align-items: center;\n column-gap: .5rem;\n font-size: var(--smaller-font-size);\n }\n\n .battery__status i {\n font-size: 1.25rem;\n }\n\n .battery__pill {\n position: relative;\n width: 75px;\n height: 180px;\n background-color: var(--container-color);\n box-shadow: inset 20px 0 48px hsl(0, 0%, 16%), \n inset -4px 12px 48px hsl(0, 0%, 56%);\n border-radius: 3rem;\n justify-self: flex-end;\n }\n\n .battery__level {\n position: absolute;\n inset: 2px;\n border-radius: 3rem;\n overflow: hidden;\n }\n\n .battery__liquid {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 36px;\n background: var(--gradient-color-red);\n box-shadow: inset -10px 0 12px hsla(0, 0%, 0%, .1), \n inset 12px 0 12px hsla(0, 0%, 0%, .15);\n transition: .3s;\n }\n\n .battery__liquid::after {\n content: '';\n position: absolute;\n height: 8px;\n background: var(--gradient-color-red);\n box-shadow: inset 0px -3px 6px hsla(0, 0%, 0%, .2);\n left: 0;\n right: 0;\n margin: 0 auto;\n top: -4px;\n border-radius: 50%;\n }\n\n /* Full battery icon color */\n .green-color {\n background: var(--gradient-color-green);\n }\n\n /* Battery charging animation */\n .animated-green {\n background: var(--gradient-color-green);\n animation: animated-charging-battery 1.2s infinite alternate;\n }\n\n /* Low battery animation */\n .animated-red {\n background: var(--gradient-color-red);\n animation: animated-low-battery 1.2s infinite alternate;\n }\n\n .animated-green,\n .animated-red,\n .green-color {\n -webkit-background-clip: text;\n color: transparent;\n }\n\n @keyframes animated-charging-battery {\n 0% {\n text-shadow: none;\n }\n 100% {\n text-shadow: 0 0 6px hsl(92, 90%, 68%);\n }\n }\n\n @keyframes animated-low-battery {\n 0% {\n text-shadow: none;\n }\n 100% {\n text-shadow: 0 0 8px hsl(7, 89%, 46%);\n }\n }\n\n /* Liquid battery with gradient color */\n .gradient-color-red,\n .gradient-color-red::after {\n background: var(--gradient-color-red);\n }\n\n .gradient-color-orange,\n .gradient-color-orange::after {\n background: var(--gradient-color-orange);\n }\n\n .gradient-color-yellow,\n .gradient-color-yellow::after {\n background: var(--gradient-color-yellow);\n }\n\n .gradient-color-green,\n .gradient-color-green::after {\n background: var(--gradient-color-green);\n }\n\n /*=============== BREAKPOINTS ===============*/\n /* For small devices */\n @media screen and (max-width: 320px) {\n .battery__card {\n zoom: .8;\n }\n }\n\n /* For medium devices \n @media screen and (min-width: 430px) {\n .battery__card {\n width: 312px;\n }\n }*/\n\n /* For large devices \n @media screen and (min-width: 1024px) {\n .battery__card {\n zoom: 1.5;\n }\n }*/\n .datetime-style {\n font-family: 'Arial', sans-serif;\n color: #333;\n background-color: #f9f9f9;\n padding: 8px 16px;\n border-radius: 5px;\n box-shadow: 0 2px 4px rgba(0,0,0,0.1);\n display: inline-block;\n margin: 10px;\n }\n </style>\n <title>Suivi des batteries</title>\n </head>\n <body>\n <div class=\"date\">\n <div>Date du Snapshot: </div>\n <div id=\"datetime-display\" class=\"datetime-style\">${parisTime}</div>\n <!--=============== BATTERY ===============-->\n <section class=\"battery grid\">`;\n batteryLevelMap.forEach((items, category) => {\n items.forEach(sensor => {\n allItems.push(sensor);\n var newDiv = `<div class=\"battery__card\">\n <div class=\"battery__data\">\n <p class=\"battery__text\">${sensor.attributes.friendly_name}</p>\n <h1 class=\"battery__percentage\">\n ${sensor.state}\n </h1>\n <p>Type: ${sensor.attributes.battery_type_and_quantity}</p>\n <p class=\"battery__status\">\n Low battery <i class=\"ri-plug-line\"></i>\n </p>\n </div>\n <div class=\"battery__pill\">\n <div class=\"battery__level\">\n <div class=\"battery__liquid\"></div>\n </div>\n </div>\n </div>`;\n htmlPage = htmlPage + newDiv;\n });\n });\n htmlPage = htmlPage + `</section>\n <!--=============== MAIN JS ===============-->\n <script>\n /*=============== BATTERY ===============*/\n/*=============== BATTERY ===============*/\ninitBattery()\n\nfunction initBattery(){\n const batteryLiquid = document.querySelectorAll('.battery__liquid'),\n batteryStatus = document.querySelectorAll('.battery__status'),\n batteryPercentage = document.querySelectorAll('.battery__percentage')\n var i = 0;\n batteryPercentage.forEach((batt) =>{\n /* 1. We update the number level of the battery */\n let level = Math.floor(batteryPercentage[i].innerHTML)\n console.log(level);\n batteryPercentage[i].innerHTML = level+ '%'\n\n /* 2. We update the background level of the battery */\n batteryLiquid[i].style.height = level+'%'\n\n /* 3. We validate full battery, low battery and if it is charging or not */\n if(level == 100){ /* We validate if the battery is full */\n batteryStatus[i].innerHTML = 'Full battery <i class=\"ri-battery-2-fill green-color\"></i>'\n batteryLiquid[i].style.height = '103%' /* To hide the ellipse */\n }\n else if(level <= 20 &! batt.charging){ /* We validate if the battery is low */\n batteryStatus[i].innerHTML = 'Low battery <i class=\"ri-plug-line animated-red\"></i>'\n }\n else if(batt.charging){ /* We validate if the battery is charging */\n batteryStatus[i].innerHTML = 'Charging... <i class=\"ri-flashlight-line animated-green\"></i>'\n }\n else{ /* If it's not loading, don't show anything. */\n batteryStatus[i].innerHTML = ''\n }\n \n /* 4. We change the colors of the battery and remove the other colors */\n if(level <=20){\n batteryLiquid[i].classList.add('gradient-color-red')\n batteryLiquid[i].classList.remove('gradient-color-orange','gradient-color-yellow','gradient-color-green')\n }\n else if(level <= 40){\n batteryLiquid[i].classList.add('gradient-color-orange')\n batteryLiquid[i].classList.remove('gradient-color-red','gradient-color-yellow','gradient-color-green')\n }\n else if(level <= 80){\n batteryLiquid[i].classList.add('gradient-color-yellow')\n batteryLiquid[i].classList.remove('gradient-color-red','gradient-color-orange','gradient-color-green')\n }\n else{\n batteryLiquid[i].classList.add('gradient-color-green')\n batteryLiquid[i].classList.remove('gradient-color-red','gradient-color-orange','gradient-color-yellow')\n }\n i++;\n })\n}\n </script>\n </body>\n</html>`;\n\nfunction getBatteryClass(category) {\n switch (category) {\n case '10': return 'very-low-battery';\n case '25': return 'low-battery';\n case '50': return 'medium-battery';\n case '75': return 'high-battery';\n case 'full': return 'very-high-battery';\n default: return '';\n }\n}\n\nmsg.payload = htmlPage\n\nreturn msg;","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":870,"y":960,"wires":[["abdddf65db1f13a7"]]},{"id":"abdddf65db1f13a7","type":"file","z":"e453ff62415924bf","name":"Save File","filename":"/HAnodeRed/batterycheck.html","filenameType":"str","appendNewline":false,"createDir":false,"overwriteFile":"true","encoding":"none","x":1050,"y":960,"wires":[[]]},{"id":"9c0e8a73c7402aef","type":"function","z":"e453ff62415924bf","name":"Normalize Battery Level","func":"// Initialization of arrays\nvar less10array = [];\nvar less25array = [];\nvar less50array = [];\nvar less75array = [];\nvar fullarray = [];\n\nvar ThisArray = msg.payload.filter(entity => entity.entity_id.includes('battery_plus')).map(entity => {\n entity.state = parseInt(entity.state);\n if (entity.attributes.friendly_name && entity.attributes.friendly_name.includes(\"Batterie+\")) {\n entity.attributes.friendly_name = entity.attributes.friendly_name.replace(\"Batterie+\", \"\");\n }\n if (entity.attributes.friendly_name && entity.attributes.friendly_name.includes(\"Battery+\")) {\n entity.attributes.friendly_name = entity.attributes.friendly_name.replace(\"Battery+\", \"\");\n }\n if (entity.attributes.friendly_name && entity.attributes.friendly_name.includes(\"Mi Temperature\")) {\n entity.attributes.friendly_name = entity.attributes.friendly_name.replace(\"Mi Temperature\", \"Temp.\");\n }\n if (entity.attributes.friendly_name && entity.attributes.friendly_name.includes(\"Détecteur de fumée\")) {\n entity.attributes.friendly_name = entity.attributes.friendly_name.replace(\"Détecteur de fumée\", \"Fumée\");\n }\n if (entity.attributes.friendly_name && entity.attributes.friendly_name.includes(\"Xiaomi Temperature and Humidity Monitor Clock\")) {\n entity.attributes.friendly_name = \"Temp. Salon\";\n }\n return entity;\n}).sort((a, b) => parseInt(a.state) - parseInt(b.state));\nThisArray.forEach(myFunction);\n\nfunction myFunction(item, index) {\n const batteryLevelAttr = item.attributes && item.attributes[\"Battery Level\"];\n const state = !isNaN(batteryLevelAttr) ? batteryLevelAttr : item.state;\n const level = parseInt(state);\n\n if (item.attributes && item.attributes.entity_class === \"MiotSensorSubEntity\" && item.entity_id !== \"sensor.xiaomi_temperature_and_humidity_monitor_clock_battery_plus\") {\n // Skip this item\n } else if (!isFinite(level)) {\n // Skip if level is not finite\n } else if (level < 10) {\n less10array.push(item);\n } else if (level < 25) {\n less25array.push(item);\n } else if (level < 50) {\n less50array.push(item);\n } else if (level < 75) {\n less75array.push(item);\n } else {\n fullarray.push(item);\n }\n}\n\n// Creating and populating the map after arrays are populated\nvar batteryLevelMap = new Map([\n ['10', less10array],\n ['25', less25array],\n ['50', less50array],\n ['75', less75array],\n ['full', fullarray],\n]);\n\n// Verifying the map contents\nconsole.log(batteryLevelMap);\nmsg.payload = batteryLevelMap;\nreturn msg;\n","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":660,"y":960,"wires":[["06d378c662f594d3"]]},{"id":"01bc5675ea304f7f","type":"server","name":"Home Assistant","version":5,"addon":false,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true,"heartbeat":false,"heartbeatInterval":"30","areaSelector":"friendlyName","deviceSelector":"friendlyName","entitySelector":"friendlyName","statusSeparator":": ","statusYear":"hidden","statusMonth":"short","statusDay":"numeric","statusHourCycle":"default","statusTimeFormat":"h:m","enableGlobalContextStore":false}]

2 « J'aime »

Bravo !

Et merci pour le partage !

Mais tu sais il y a aussi manière de faire beaucoup plus simple (à moins que je n’aie pas suivi toutes les subtilités) avec une carte auto-entities disponible sous HACS:

exemple donné là:

Ce que ça donne chez moi:

le code:
  • Il faut installer auto-entities vias HACS (obligatoire)
  • dans mon cas j’utilise aussi bar-card dispo via HACS mais on peut utiliser n’importe quelle carte comme des jauges par exemple.

il y a des options de filtrage et de tri dans auto-entities pour organiser la liste comme on veut et exclure les entité non pertinentes (batteries des smartphones, ipad, pc, brosses à dent etc par exemple dans mon cas…).

Le seul truc qui n’est pas automatique, c’est que j’ai renommé mes entités batteries avec un nom plus court incluant le type de pile (mais c’est à faire uniquement une fois au début), et ça c’est redoutable pour la liste de course… par exemple là il va me falloir des CR2032 !

type: custom:auto-entities
card:
  type: custom:bar-card
  title_position: inside
  height: 48
  positions:
    icon: none
    indicator: inside
    name: inside
    value: inside
  show_icon: false
  align: split
  columns: '1'
  max: 100
  unit_of_measurement: '%'
  severity:
    - color: '#d11e1e'
      to: '5'
      from: '0'
    - color: '#cf2d11'
      from: '6'
      to: '10'
    - color: '#cc3900'
      from: '11'
      to: '15'
    - color: '#c84400'
      from: '16'
      to: '20'
    - color: '#c44d00'
      from: '21'
      to: '25'
    - color: '#bf5600'
      from: '26'
      to: '30'
    - color: '#b95f00'
      from: '31'
      to: '35'
    - color: '#b36600'
      from: '36'
      to: '40'
    - color: '#ac6e00'
      from: '41'
      to: '45'
    - color: '#a57500'
      from: '46'
      to: '50'
    - color: '#9d7b00'
      from: '51'
      to: '55'
    - color: '#948100'
      from: '56'
      to: '60'
    - color: '#8b8700'
      from: '61'
      to: '65'
    - color: '#818d00'
      from: '66'
      to: '70'
    - color: '#769200'
      from: '71'
      to: '75'
    - color: '#6a9700'
      from: '76'
      to: '80'
    - color: '#5d9c00'
      from: '81'
      to: '85'
    - color: '#4da100'
      from: '86'
      to: '90'
    - color: '#39a500'
      from: '91'
      to: '95'
    - color: '#15a911'
      from: '96'
      to: '100'
  animation:
    state: 'on'
    speed: '2'
  title: Etat des piles
filter:
  exclude:
    - entity_id: ^sensor.brosse*$
    - entity_id: ^sensor.huawei*$
    - entity_id: ^sensor.ipad*$
    - entity_id: ^sensor.moto*$
    - entity_id: ^sensor.pc*$
    - entity_id: ^sensor.smart*$
    - entity_id: ^sensor.tablette*$
    - entity_id: ^sensor.roomba*$
  include:
    - entity_id: ^sensor.*_battery_level$
    - entity_id: ^sensor.*_battery$
    - entity_id: ^sensor.*_batt$
    - entity_id: ^sensor.*_battery_charge$
sort:
  method: state
  numeric: true
show_empty: false

Un exemple en grille qui ressemble plus à ta présentation:

le code
type: custom:auto-entities
card:
  type: grid
card_param: cards
filter:
  exclude:
    - entity_id: ^sensor.brosse*$
    - entity_id: ^sensor.huawei*$
    - entity_id: ^sensor.ipad*$
    - entity_id: ^sensor.moto*$
    - entity_id: ^sensor.pc*$
    - entity_id: ^sensor.smart*$
    - entity_id: ^sensor.tablette*$
    - entity_id: ^sensor.roomba*$
  include:
    - entity_id: ^sensor.*_battery_level$
      options:
        type: gauge
        severity:
          green: 50
          yellow: 25
          red: 0
    - entity_id: ^sensor.*_battery$
      options:
        type: gauge
        severity:
          green: 50
          yellow: 25
          red: 0
    - entity_id: ^sensor.*_batt$
      options:
        type: gauge
        severity:
          green: 50
          yellow: 25
          red: 0
    - entity_id: ^sensor.*_battery_charge$
      options:
        type: gauge
        severity:
          green: 50
          yellow: 25
          red: 0
sort:
  method: state
  numeric: true
show_empty: false

Hello BBE,

merci pour ton retour, en effet, j’avais aussi testé la solution des cards avec auto-entities.
Mais le problème principal que j’avais était le fait que je me retrouvais avec beaucoup de doublons dans les listes liés à l’utilisation de battery note et également à cause du fais que certains de mes capteurs sont dédoublés par l’utilisation de Miot auto (cloud) et Gateway3 (local) pour mes capteurs xiaomi.
Je garde la partie cloud car de temps en temps l’intégration Gateway3 est capricieuse, donc mes capteurs sont intégrés via les deux.

J’imagine qu’il est possible également d’utiliser des filtres plus ou moins complexe pour éviter les doublons dans auto-entities, mais je n’y suis pas parvenu…

Je voulais également pouvoir renommer mes entités avec des noms moins long, ce qui est surement possible aussi, mais dois grandement alourdir la lisibilité de la carte.

Exemple de mon ancienne carte :
image

Le code de ma carte :

type: vertical-stack
title: Batteries
cards:
  - type: custom:auto-entities
    card:
      show_header_toggle: false
      title: Faibles
      state_color: false
      type: entities
      card_mod:
        style: |
          ha-card .card-header {
            font-size: 14px;
            color: white;
            font-weight: 600
          }
          ha-card {
              #states > div {
                margin: -15px 0px !important;
              }
            .card-content hui-sensor-entity-row:
              $:
                hui-generic-entity-row:
                  $: |
                    .info {
                      margin-left: 0px !important;
                     }
            --mdc-icon-size: 10px !important;
            --paper-item-icon-color: Red;
            font-size: 12px !important;
            padding: 0px 0px 0px !important;
            font-weight: 200;
            background: none;
            .card-content div {
              margin-top: 0px !important;
              margin-bottom: 0px  !important;
            }
          }
    filter:
      include:
        - attributes:
            device_class: battery
          state: < 21
      exclude:
        - attributes:
            service_description: Battery
        - name: /[Ll]ow/
        - name: /[Ss]tate/
    sort:
      method: state
      numeric: true
    show_empty: true
  - type: custom:auto-entities
    card:
      show_header_toggle: false
      title: Moyen
      state_color: false
      type: entities
      card_mod:
        style: |
          ha-card .card-header {
            font-size: 14px;
            color: white;
            font-weight: 600
          }
          ha-card {
              #states > div {
                margin: -15px 0px !important;
              }
            .card-content hui-sensor-entity-row:
              $:
                hui-generic-entity-row:
                  $: |
                    .info {
                      margin-left: 0px !important;
                     }
            --mdc-icon-size: 10px !important;
            --paper-item-icon-color: yellow;
            font-size: 12px !important;
            padding: 0px 0px 0px !important;
            font-weight: 200;
            background: none;
            .card-content div {
              margin-top: 0px !important;
              margin-bottom: 0px  !important;
            }
          }
    filter:
      include:
        - attributes:
            device_class: battery
          state: < 51
      exclude:
        - attributes:
            service_description: Battery
        - name: /[Ll]ow/
        - name: /[Ss]tate/
        - state: < 20
    sort:
      method: state
      numeric: true
    show_empty: false

Dans la doc auto-entities il y a une option:

  • unique: Whether to remove duplicate values after filtering and sorting. Set to true to remove exact duplicate entries. Set to entity to remove entries with the same entity id. Default: false.

Tu avais essayé?

C’est vrai qu’auto-entities, c’est toujours un peu capricieux à mettre en place, rien que dans l’exemple que je t’ai donné, il y a deux cas d’utilisations: le premier, simple ou c’est les entités qu’on fait varier, mais ça ne marche pas pour toutes les cartes, des fois il faut faire varier les cartes et passer le type de carte en option (exemple avec les gauge…).

  • Le gros avantage que j’y vois par rapport à ta solution, c’est la possibilité de l’intégrer facilement à un dashboard puisque c’est une carte.
  • L’avantage de ta solution, c’est du 100% sur mesure et tu peux faire absolument tout ce que tu veux.
1 « J'aime »

Oui j’avais testé cette option, mais les ID de mes capteurs Xiaomi ne sont pas uniques entre les deux intégrations.
Ça me permet de savoir quand j’appelle les locals ou les clouds dans mes automations.

L’idée, c’était aussi de me familiariser avec Node car j’aimerais beaucoup passer mes automations dessus, je trouve ça plus lisible et plus permissif niveau possibilitées :slight_smile:

Dans ton cas tu pourrais exclure les cartes avec le « + », ça suffirait pour les doublons…
Pour le renommage, il y a probablement des façons de faire automatique, moi j’ai choisi de renommer « à la main » chaque sensor.xxx_battery avec un friedly name qui est: nom court (type pile) comme dans les exemples précédents, pour ton exemple j’aurai mis « Fumée grenier (CR2032) »

Je le fais à la fin, quand j’ai fini mes filtrages, en cliquant sur la carte et en allant sur les options de chaque entité dans la carte pop up « more info »…

Là il ne te reste plus que l’include un par un des sensors, mais du coup on perd beaucoup de l’interret de auto entities… (a part le tri en fonction de l’état).

Tout à fait respectable !

C’est la conclusion que j’en avais tiré également après avoir passé pas mal de temps sur la doc et les conversations autour de auto-entitites

1 « J'aime »

Bref je n’avais pas toutes les subtilités !!

Peut être qu’on peut filtrer sur le friendly name, ça te permettrait de faire d’une pierre deux coup, tu modifie ton titre et tu filtres dessus…

Si tu filtres sur name, tu teste le friendly_name.

Si tu as renommé toutes tes batteries à monitorer avec le type de piles (comme je fais moi), tu as juste à lister tes types de piles

exemple avec les CR2032 (et aucun exclude !!):

type: custom:auto-entities
card:
  type: entities
filter:
  include:
    - name: /CR2032/
sort:
  method: state
  numeric: true
show_empty: false

image

A tester, c’est une option différente…

Ca n’empeche pas de faire du node-red par ailleurs…

Excellent workaround !
Ça évite la contrainte de faire entité par entité et on a souvent moins de types de batterie que d’appareil ^^

Comme quoi, parfois, il suffit de penser « out of the box » comme on dit.

1 « J'aime »

Salut Maxime ! Tout d’abord merci beaucoup pour ton travail ! L’affichage est génial. J’ai voulu refaire le projet et j’ai un message d’erreur sur la function webpage.
image
J’ai repris ton template mais j’arrive pas à comprendre ce qui se passe. Si tu as une idée je suis preneur :slight_smile:

Hello Clayman,

Merci beaucoup pour ton retour :slight_smile:
En effet, excuse-moi, j’ai oublié un petit détail, j’ai déclaré une variable global dans Node-red :

image
tu dois ajouter cette ligne dans ton fichier settings.js de nodeRed.

Re ! Le fichier settings.js il faut le créée dans /local/node-red/settings.js ? Ou il y a une façon d’y aller ?

Normalement il est créer automatiquement avec l’intégration de Node-red.

De mon côté j’ai un service docker qui tourne en dehors de homeAssistant donc il ne doit pas être au même endroit que toi si tu utilises l’addon de Node-Red directement en plugin HomeAssistant.

Mais il devrait directement être à la racine de ton dossier node-red
image