Je teste le Thermomètre Zigbee TH05Z [Echec]

Le marché des thermomètres Zigbee a vu une augmentation importante des prix des modèles les plus répandus. Même les Sonoff et les Aqara qui étaient les moins chers deviennent couteux si on veut en mettre dans toutes ses pièces.

J’ai commandé cet équipement Zigbee encore pas cher pour le tester. Notez que mes tests ne sont jamais “sponsoriés”, aucun produit testé n’est offert par telle ou telle marque ou tel ou tel distributeur de matériel. C’est acheté avec mon argent à moi, parfois financé par des reventes comme ce qui se trouve dans mon vide-grenier.

Où acheter cet équipement ?

Au jour d’écriture de ce Retex :

J’ajoute des piles

Ce thermomètre utilise 3 piles AAA non fournies

J’intègre ce boitier à Zigbee2MQTT

Je bascule Zigbee2MQTT en mode appairage et je branche la dernière pile.

A la connexion de la pile, la température s’affiche sur l’écran du thermomètre, elle est juste.

L’appairage démarre…

 

Mon test s’arrête là, il va falloir attendre que ce thermomètre soit intégré à Zigbee2MQTT…

 

Il reste un beau produit, bien fini, mais si on veut l’intégrer, il faudra passer par Tuya et non Zigbee2MQTT directement, ce que je me refuse de faire.

 

 

 

 

 

 

 

 

Je teste le Thermomètre Zigbee ZTH05

Le marché des thermomètres Zigbee a vu une augmentation importante des prix des modèles les plus répandus. Même les Sonoff et les Aqara qui étaientles moins chers deviennent couteux si on veut en mettre dans toutes ses pièces.

J’ai commandé cet équipement Zigbee encore pas cher pour le tester. Notez que mes tests ne sont jamais “sponsoriés”, aucun produit testé n’est offert par telle ou telle marque ou tel ou tel distributeur de matériel. C’est acheté avec mon argent à moi, parfois financé par des reventes comme ce qui se trouve dans mon vide-grenier.

Lire la suite

Je mets un thermomètre Zigbee dans le congélateur ? (Woox R7048)

Contexte et objectif

Depuis plus de 10 ans j’utilise les fidèles services des thermomètres Oregon Scientific, une marque et des équipements justes géniaux, aucun dysfonctionnement, des piles qui tiennent longtemps. Mais ils fonctionnent en 433Mhz avec un RFXCom, j’ai décidé de supprimer le 433Mhz pour tout basculer progressivement en Zigbee. Lire la suite

Je teste le Smart Switch ZG-005-RF

J’ai longtemps cherché comment disposer de contacts secs (aussi appelés Libre de potentiel) avec des NO (Normalement Ouvert) et NF (Normalement Fermé) comme le fait de manière géniale les SONOFF 4CH (Pro ou pas).

Ce type d’équipement en contact sec est capable de tout faire, aussi bien commander des appareils électriques, des lampes mais également commander des vannes d’arrosage (quelque soit leur tension) ou encore envoyer une impulsion pour ouvrir un portail, une gâche électrique ou une sirène.

J’ai commandé cet équipement Zigbee pour le tester. Notez que mes tests ne sont jamais “sponsoriés”, aucun produit testé n’est offert par telle ou telle marque ou tel ou tel distributeur de matériel. C’est acheté avec mon argent à moi, parfois financé par des reventes comme ce qui se trouve dans mon vide-grenier.

La documentation précise que la tension de votre choix peut aller jusqu’à 240V max. Jusqu’à 10 A (2200 W à 220 V) avec charge résistive1 (220 W max. à 220 V avec des LED) Lire la suite

Comparaison des frais des sites de ventes

Ce mini Retex pour comparer le coût pour l’acheteur et le vendeur des sites.

Sur ce comparatif, je pars sur un produit vente 10€ et je compare avec Mondial Relais.

  Produit Port Commission Coût acheteur Coût vendeur Recette vendeur
Vente direct 10€ 4.40€ 0€ 14.40€ 0€ 10€
Le Bon Coin 10€ 3.49€ 0.99€ 14.48€ 0€ 10€
Vinted 10€ 2.88€ 0.9€ 13.78€ 0€ 10€
Le Bon Coin 80€ 6.49€ 3.20€ 89.69€ 0€ 80€

 

Utiliser un script Python pour enregistrer des données dans HA

Je préviens tout de suite, ce Retex n’est qu’à vocation pédagogique, je le rédige pour partager une méthodologie, à savoir que j’ai réalisé cette fonctionnalité différemment depuis, donc je n’ai pas ce process en prod.

L’intégration pour exécuter le script Python

Pour une raison que je n’ai jamais compris, les fonctionnalités scripts python intégrées à HA sont archi-mini, aucun import n’est possible, je dois donc utiliser une intégration appelée pyscript.

La doc est ici.

Je passe les détails pour l’installer, c’est très simple avec HACS et tout est dans la doc.

Dans configuration.yaml, j’ai bien ces lignes :

pyscript:
  allow_all_imports: true
  hass_is_global: true

Je récupère un jeton

J’ouvre l’interface utilisateur de Home Assistant.

Dans le coin inférieur gauche, je clique sur mon nom d’utilisateur (ou l’icône du profil).

Je clique sur “Profil”.

Je fais défiler vers le bas jusqu’à la section “Jetons d’accès longue durée”. 

Je clique sur “Créer un jeton” pour générer un nouveau jeton.

Je donne un nom à mon jeton pour m’y retrouver plus facilement.

Une fois le jeton créé, je vois son contenu. Je copie la valeur du jeton, c’est-à-dire la série de caractères alphanumériques, et je la colle dans ma configuration (paragraphe suivant)

Déclaration de la plateforme qui fait le lien entre Python et le sensor

Dans configuration.yaml, j’ajoute ces lignes :

rest_command:
  send_data_to_api:
    url: 'http://homeassistant.local:8123/api/states/sensor.wallbox_charges'
    method: 'post'
    content_type: 'application/json'
    headers:
      Authorization: 'Bearer TOKEN'  # Remplacer TOKEN par le token cf paragraphe précédent
    payload: '{{ data | tojson }}'  

Il s’agit de la porte d’entrée qui sera approvisionné par le script Python (plus bas) et qui enverra les informations dans le sensor sensor.wallbox_charges.

Mon script Python

Je place le fichier wallbox_charges.py dans le dossier /config/pyscript

import datetime
import pytz
@service
def wallbox_charges(lastdata=None, action=None, prix="123", km="123", dateDebut="01/01/2000 12:00:00", valeur="123"):
    fuseau_horaire_paris = pytz.timezone("Europe/Paris")

    maintenant_timestamp = int(datetime.datetime.now().timestamp())
    maintenant_heure_paris = datetime.datetime.fromtimestamp(maintenant_timestamp, fuseau_horaire_paris)
    maintenant_formatee = maintenant_heure_paris.strftime("%d/%m/%Y %H:%M:%S")

    maintenantMoins20min = datetime.datetime.now() - datetime.timedelta(minutes=20)
    maintenantMoins20min_formatee = maintenantMoins20min.strftime("%H:%M")
    
    
    dateDebutRecue = datetime.datetime.strptime(dateDebut, '%d/%m/%Y %H:%M:%S').replace(tzinfo=fuseau_horaire_paris)
    debut_timestamp_formatee = dateDebutRecue.strftime("%d/%m/%Y %H:%M:%S")
    debut_timestamp_formatee_SansSecondes = dateDebutRecue.strftime("%d/%m/%Y %H:%M")
    
     # Transformez les chaînes en objets datetime
    maintenant_dt = datetime.datetime.strptime(maintenant_formatee, "%d/%m/%Y %H:%M:%S")
    debut_dt = datetime.datetime.strptime(debut_timestamp_formatee, "%d/%m/%Y %H:%M:%S")

    # Calculez la différence entre les objets datetime
    tempsDeCharge = maintenant_dt - debut_dt - datetime.timedelta(minutes=20)

    # Formatez la durée en heures, minutes et secondes
    heures, reste = divmod(tempsDeCharge.seconds, 3600)
    minutes, secondes = divmod(reste, 60)

    # Affichez la durée au format HH:MM:SS
    tempsDeCharge_formatee = f"{heures:02}:{minutes:02}"


    # Écrivez un message d'information dans le journal
    #log.info(action)




    if action == "enregistrer":
        valeuraajouter =  [{
                          "saved_at": maintenant_timestamp,
                          "début": debut_timestamp_formatee_SansSecondes,
                          "fin": maintenantMoins20min_formatee,
                          "durée": tempsDeCharge_formatee,
                          "consommation": valeur,
                          "km": km,
                          "coût": round(valeur * prix, 2)
                        }]
        if lastdata is not None:
            valeuraajouter = lastdata + valeuraajouter
        data =  {
                    "state": debut_timestamp_formatee_SansSecondes,  
                    "attributes": {
                      "updated_at": maintenant_dt.strftime("%d/%m/%Y %H:%M:%S"),
                      "sessionscharge": 
                      valeuraajouter
                    }
                }

    if action == "init":
        data =  {
                "state": "on",
                    "attributes": {
                      "updated_at": "14/10/2023 16:42:39",
                      "sessionscharge": [
                        {
                          "consommation": 2.14,
                          "coût": 0.43,
                          "durée": "00:27",
                          "début": "19/09/2023 20:36",
                          "fin": "21:03",
                          "km": 17,
                          "saved_at": 1695150359
                        },
                        {
                          "consommation": 23.29,
                          "coût": 4.66,
                          "durée": "04:19",
                          "début": "21/09/2023 21:06",
                          "fin": "01:25",
                          "km": 192,
                          "saved_at": 1695323159
                        },
                        {
                          "consommation": 7.68,
                          "coût": 1.54,
                          "durée": "01:23",
                          "début": "22/09/2023 08:45",
                          "fin": "10:08",
                          "km": 63,
                          "saved_at": 1695409559
                        },
                        {
                          "consommation": 23.80,
                          "coût": 4.76,
                          "durée": "04:26",
                          "début": "23/09/2023 16:57",
                          "fin": "21:23",
                          "km": 196,
                          "saved_at": 1695495959
                        }
                       ]
                    }
                }   



    # Appelez le service REST pour envoyer les données
    service_data = {
        "token": "xxxxxx",  # Remplacer par le token
        "data": data
    }

    if action == "enregistrer" or action == "init":
        # Appelez le service REST de manière asynchrone
        await hass.services.async_call('rest_command', 'send_data_to_api', service_data)
        
    # Écrivez un message d'erreur dans le journal
    #log.error("Fin script")

Ajouter deux automatisations

Une pour initialiser le sensor, l’autre pour ajouter une donnée

Initialiser les données

alias: Initialise les sessions de charge
description: ""
trigger: []
condition: []
action:
  - service: pyscript.wallbox_charges
    data:
      action: init
mode: single

Enregistrer les données

alias: Enregistre une session de charge
description: >-
  Cette automatisation se déclenche lorsque le capteur n'a pas changé de valeur
  depuis 20 minutes et n'est pas à 0
trigger:
  - platform: state
    entity_id:
      - sensor.wallbox_portal_added_range
    for:
      hours: 0
      minutes: 20
      seconds: 0
    enabled: true
condition:
  - condition: numeric_state
    entity_id: sensor.wallbox_portal_added_energy
    above: 0
  - condition: not
    conditions:
      - condition: state
        entity_id: sensor.wallbox_charges
        state: >-
          {{ as_timestamp(states('sensor.wallbox_portal_added_range_min')) | int
          |     timestamp_custom('%d/%m/%Y %H:%M:%S', true)}}
    enabled: false
action:
  - service: pyscript.wallbox_charges
    data:
      action: enregistrer
      valeur: "{{states('sensor.wallbox_portal_added_energy')}}"
      prix: "{{states('sensor.wallbox_portal_energy_price')}}"
      km: "{{states('sensor.wallbox_portal_added_range')}}"
      lastdata: "{{state_attr('sensor.wallbox_charges', 'sessionscharge')}}"
      dateDebut: >-
        {{ as_timestamp(states('sensor.wallbox_portal_added_range_min')) | int |
        timestamp_custom('%d/%m/%Y %H:%M:%S', true)}}
  - service: variable.update_sensor
    data:
      replace_attributes: false
      value: >-
        {{ as_timestamp(states('sensor.wallbox_portal_added_range_min')) | int
        |     timestamp_custom('%d/%m/%Y %H:%M:%S', true)}}
    target:
      entity_id: sensor.wallbox_charges
    enabled: false
mode: single

Pour avoir la valeur minimal de la charge, j’utilise statistic pour avoir la valeur min

Ce sensor est déclaré dans sensors.yaml

- platform: statistics
  name: "Wallbox Portal Added Range Min"
  entity_id: sensor.wallbox_portal_added_range
  state_characteristic: datetime_value_min
  max_age:
    hours: 10  
   

Résultat du stockage des infos

Carte d’affichage de toutes les sessions

<div>Sessions de charge</div> 
Dernière mise à jour : {{ state_attr('sensor.wallbox_charges','updated_at')}} <br><br>
{% set items = state_attr('sensor.wallbox_charges', 'sessionscharge') %}
<table width='100%'>
<tbody>
{% for i in range(0, items | count, 1) %}
<tr>
<td>{{ items[i].début }} à {{ items[i].fin }} ({{ items[i].durée }})</td>
<td>{{ items[i].consommation }} kWh</td>
<td>+{{ items[i].km }} km</td>
<td>{{ items[i].coût }} €</td>
</tr>

{% endfor %}
</tbody>
</table>

Carte d’affichage avec des totaux par année, mois, semaine

type: markdown
content: >-
  <div><h1>Sessions de charge</div>  {%- set default_language = 'fr' %} Dernière
  mise à jour : {{ state_attr('sensor.wallbox_charges','updated_at')}} <br><br>


  {% set items = state_attr('sensor.wallbox_charges', 'sessionscharge') %}

  {% set septjours = now() - timedelta(days=7) %}

  {% set power_semaine = namespace(value=0) %}

  {% set cout_semaine = namespace(value=0) %}

  {% set unmois = now() - timedelta(days=30) %}

  {% set power_mois = namespace(value=0) %}

  {% set cout_mois = namespace(value=0) %}

  {% set uneannee = now() - timedelta(days=365) %}

  {% set power_annee = namespace(value=0) %}

  {% set cout_annee = namespace(value=0) %}


  {% for i in range(0, items | count, 1) %}
    {% if items[i].saved_at | int >= as_timestamp(uneannee) %}
    {% set power_annee.value = power_annee.value + items[i].consommation %}
    {% set cout_annee.value = cout_annee.value + items[i].coût %}
      {% if items[i].saved_at | int >= as_timestamp(unmois) %}  
        {% set power_mois.value = power_mois.value + items[i].consommation %}
        {% set cout_mois.value = cout_mois.value + items[i].coût %}
        {% if items[i].saved_at | int >= as_timestamp(septjours) %}
          {% set power_semaine.value = power_semaine.value + items[i].consommation %}
          {% set cout_semaine.value = cout_semaine.value + items[i].coût %}        
        {% endif %}
      {% endif %}
    {% endif %}
  {% endfor %}

  </ha-alert> <ha-alert alert-type="info" title="Dernier mois : {{
  power_mois.value | int }} kWh - {{ cout_mois.value | float | round(2) }}
  &euro;"></ha-alert> <ha-alert alert-type="info" title="Dernière année : {{
  power_annee.value | int }} kWh - {{ cout_annee.value | float | round(2) }}
  &euro;"></ha-alert>


  <br> 


  {% set items = state_attr('sensor.wallbox_charges', 'sessionscharge') %}

  {% set septjours = now() - timedelta(days=7) %}

  <table width='100%'> <tbody>  {% for i in range(0, items | count, 1) %}  {% if
  items[i].saved_at | int >= as_timestamp(septjours) %} {% set datetime_obj =
  strptime(items[i].début, '%d/%m/%Y %H:%M') %}

  <tr> <td><h5>{{items[i].début }} à {{ items[i].fin }} ({{ items[i].durée
  }})</td> <td>{{
    items[i].consommation }} kWh</td> <td>+{{ items[i].km }} km</td> <td>{{
    items[i].coût }} €</td> </tr> {% endif %} {% endfor %} </tbody> </table>
    <ha-alert alert-type="success" title="Dernière semaine : {{
      power_semaine.value | int}} kWh - {{ cout_semaine.value | float | round(2)
      }}&euro;">    
style:
  .: |
    ha-card {
      margin: 0px 0px 0px 0px;
      box-shadow: none;
     }
  ha-markdown:
    $: |
      h5 {
        font-size: 15px !important;
        font-weight: normal !important;
        padding: 0px 0px 0px 8px !important;
        border-left: 3px solid #429f46;
        }

 

J’ajoute un gif animé transparent à mon Dashboard HA

Mon objectif

Mon objectif est de réaliser une animation via un gif animé.

Je pourrai changer de gif en fonction de l’état d’une entité.

Je vais dessiner les deux boules (une rouge une bleue)

Un petit coup de Photoshop ou autre et voici le fichier de base

Je vais créer l’animation dans Canva

Ma version de photoshop étant ancienne, je n’ai pas la fonctionnalité montage à la maison, j’utilise Canva (en ligne).

Pour éviter une marque blanche au détourage, je mets le fond de mon écran HA en fond, le résultat est :

Je vais basculer sur un fond transparent

Je n’ai pas trouvé comment faire cela avec Canva, j’utilise un outil en  ligne

Le résultat donne 

Je fais deux autres animations

Pour avoir trois états, je génère deux autres fichiers, un plus rapide et un plus lent, donc trois fichiers au total :

J’ajoute un template qui va correspondre à la puissance consommée 

Dans mon fichier templates.yaml, j’ajoute

- name: Consommation_vitesse
  unique_id: sensor.consommation_vitesse
  state: >-
          {% if (states('sensor.linky_puissance_apparente')|float) < 400 %}
            lente
          {% elif (states('sensor.linky_puissance_apparente')|float) >4000 %}
            rapide
          {% else %}
            moyenne
          {% endif %} 

Sur le dashboard, j’ajoute une carte qui va afficher un des 3 gif animés, cela en fonction de la valeur de sensor.consommation_vitesse.

- type: image
  entity: sensor.consommation_vitesse
  tap_action:
    action: none
  style:
    width: 7%
    top: 91.6%
    left: 26.7%
  state_image:
    lente: /local/maison/Conso/lente.gif
    moyenne: /local/maison/Conso/moyenne.gif
    rapide: /local/maison/Conso/rapide.gif

 

J’intègre le Power Boost EM112 de chez Carlo Gavazzi dans Home Assistant via le port RS485 (Modbus)

Information importante

A noter que l’intégration décrite ci-dessous ne peut fonctionner que si la clé USB/RS485 est le seul équipement du bus RS485 connectée au EM112. En effet (et grâce à l’aide d’Eric G.) J’ai compris qu’il ne pouvait y avoir qu’un seul équipement maitre sur le bus. Or le maitre est celui qui interroge et l’esclave celui qui répond. l’EM112 est donc esclave.

Mon idée première était de sniffer les trames de dialogues entre l’EM112 et ma Wallbox, la Wallbox étant maitre, je ne peux faire cela. Pour la beauté de l’exercice, j’ai testé avec deux maitres, cela fonctionne on retrouve des valeurs mais de temps en temps, quand les deux maitres posent une question ensemble, on se retrouve avec une erreur.

Si quelqu’un peut m’orienter vers une manière de “snifer” le bus 485 pour intercepter les informations que le EM112 envoie à une Wallbox par exemple, je suis preneur car à ce stade, je ne peux avoir une Wallbox et une Clé RS485/USB en parallèle (deux maitres).

Lire la suite