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’intègre la Wallbox à Home Assistant

Pour l’instant sous forme désorganisée, ce RetEx me permet de prendre des notes pour la compréhension des données qui remontent de l’intégration Wallbox

Les capteurs Wallbox

Je décortique l’entité Wallbox Portal Added Energy

Voici les données relevées

L’unité est le kWh donc nous sommes bien une quantité d’énergie. Il s’agit de la quantité d’énergie injectée dans le véhicule lors de la dernière charge.

Je ne branche pas le véhicule à la Wallbox tous les jours, je constate donc que quand je ne fais rien, le portail Wallbox garde la dernière valeur, de la dernière charge.

Le reset de cette quantité d’énergie doit se faire à l’heure à laquelle j’ai branché le véhicule. Je ferai attention les prochains jours.

Dans le graphique ci dessus, je vois que le 3 octobre, 5 octobre et le 7 octobre, entre 20h30 et 21h, la valeur est réinitialisée à 0.

Ensuite la Wallbox attend que la première plage horaire de charge arrive, 2h10 chez moi.

La Wallbox s’arrête seule quand le véhicule lui confirme qu’il a assez mangé, il est configuré sur 80% de la batterie pour les trajets quotidiens.

Les plateaux à 17.3kWh, à 22,18kWh, 23.86kWh et 23.43kWh vont me permettre d’afficher sur une carte HA la dernière quantité d’énergie injectée dans le véhicule… parfait.

Je décortique l’entité Wallbox Portal Added Range

Voici les données relevées

L’unité est le km. J’en déduis donc que c’est l’estimation du nombre de km ajoutés à chaque charge.

Je ne sais pas trop comment la Wallbox peut savoir cela, je pensais que c’était propre au véhicule et évidemment à la conduite, ce doit être une estimation.

Le fonctionnement de ces données est identique à l’entité précédente, je conclue immédiatement que je vais pouvoir ajouter sur la carte HA le dernier nombre de km ajoutés dans le véhicule… parfait.

Je décortique l’entité Wallbox Portal Charging Power

Voici les données relevées

L’unité est kW, c’est donc la puissance consommée par la Wallbox pour charger le véhicule.

On constate que c’est assez variable, cela est calculé par l’EM112 placé en début d’installation qui informe la Wallbox de combien d’énergie elle peut disposer, cela en fonction des autres équipements qui sont déjà allumés dans l’installation.

Je vais pouvoir ajouter cette information sur la carte HA, peut-être les dernière 24h car en principe, on n’est pas devant l’écran quand la charge est en cours.

Je décortique l’entité Wallbox Portal Charging Speed

Cette entité est toujours à zéro sur mon système, je ne dois pas utiliser ce mode. Peut être que cela est disponible en tri-phasé ou avec des équipements supplémentaires Je ne sais pas, comme la (mini, archi-mini) doc de l’intégration ne dit rien, je laisse tomber, je vais même désactiver cette entité.

Je décortique l’entité Wallbox Portal Cost

Cette entité est toujours à zéro sur mon système, je pensais y trouver le coût de la dernière charge. Je laisse tomber, je n’en ai pas besoin, je ferai moi-même la multiplication. Je désactive l’entité.

Je décortique l’entité Wallbox Portal Current Mode

Cette entité s’actualise mais toujours à la même valeur : 1, je ne dois utiliser que le Mode 1, de quoi, je n’en sais rien, comme cela ne m’apporte rien, je le désactive.

Je décortique les entités Wallbox Portal Depot Price et Wallbox Portal Energy Price

Ces entités me donne le prix du kWh, je l’ai fixé à 0.20€ dans la configuration, il me donne donc en permanence 0.20€.

Je ne comprends pas trop ce que veut dire Depot Price, je ne vais conserver que Energy Price.

Je vais le garder que si un jour j’affine cette valeur, je le ferai dans l’application Wallbox et si tout va bien, cela arrivera dans HA. Je suis surpris que Wallbox ne permette pas de mettre un prix pour les Heures Creuses et un prix pour les HP, peut être une évolution un jour.

Je décortique l’entité Wallbox Portal Discharged Energy

L’unité est en kWh, je ne vois pas pourquoi on pourrait utiliser la Wallbox pour décharger une batterie. J’imagine que cette fonctionnalité est prévue quelque part, je ne l’ai pas vue. Sur certains chargeurs de piles, on a un bouton pour décharger les piles avant de les charger, c’est peut-être cette logique, cette entité me donne 0 en permanence, je vais la désactiver à ce stade.

Je décortique les entités Wallbox Portal Max Available Power et Wallbox Portal Max. Charging Current

L’unité est en A et les affichent en permanence 32A, c’est la valeur configuré dans ma Wallbox (je suis en monophasé). Je les désactivent, cela n’apporte rien à la carte HA.

Je décortique l’entité Wallbox Portal State of Charge

Cette entité qui est en % n’affiche rien chez moi. Peut etre utile en tri-phasé, je ne sais pas, je la désactive.

Je décortique l’entité Wallbox Portal Status Description

Entité particulièrement importante, elle me donne l’état de la Wallbox, c’est elle qui va rendre mon écran intéressant.

A ce stade, j’ai repéré 5 états que je vais utiliser pour faire de beaux logos d’état de la Wallbox

  • Scheduled
  • Ready
  • Waiting for car demand
  • Disconnected
  • Charging

Bilan de ces analyses

Voici ce que je garde pour les exploiter 

Les Contrôles Wallbox

Je décortique l’entité Wallbox Portal Pause Resume

Elle a deux valeurs :

  • Désactivée soit Pause
  • Activée soit Resume (Reprise)

J’ai testé cela, c’est assez pratique. Par exemple, on branche le véhicule, la charge ne démarre pas car nous sommes hors de la plage de charge (=heures creuses chez moi), on appuie sur Reprise et la charge démarre. J’imagine que si on est en charge, si on appuie sur Pause, cela doit couper la charge mais je n’ai pas testé.

Un bouton sur le Dashboard peut être sympa et pratique.

Je décortique l’entité Wallbox Portal Locked Unlocked

Cette entité n’a pas d’intérêt pour moi, elle doit servir pour les Wallbox partagées dans des co-pro par exemple ou chacun vient la déverrouiller avec une carte RFID pour s’en servir.

Elle est toujours sur Déverrouillé, je la désactive.

Je décortique l’entité Wallbox Portal Max. Charging Current

C’est un curseur qui va de 0 à la valeur max du courant configuré.

Elle n’a d’intérêt que si on n’utilise pas de EM112 en tête d’installation et qu’on veut réduire la consommation de la Wallbox pour laisser un peu de place à d’autres équipements.

Chez moi, c’est configuré sur 32A et je n’y touche jamais, je désactive cette entité.

Bilan de ces analyses

Voici ce que je garde pour les exploiter

Je crée de belles images pour les états de la Wallbox

A noter que je n’en ai pas trouvé, nulle-part sur le web. Alors je lance mon vieux Photoshop et c’est parti…

Je ne dépose pas les images mais si vous les réutilisez, je ne demande rien, juste d’indiquer la source.

Je vais créer une carte pour afficher l’état de la Wallbox

Comme expliqué plus haut, la Wallbox peut prendre 5 états.

  • Scheduled
  • Ready
  • Waiting for car demand
  • Disconnected
  • Charging

Ces états sont fournis par entité : Wallbox Portal Status Description

La carte sera donc

type: image
entity: sensor.wallbox_portal_status_description
style:
  width: 100%
  top: 50%
  left: 50%
state_image:
  Scheduled: /local/maison/Wallbox/Scheduled.png
  Ready: /local/maison/Wallbox/Ready.png
  Waiting for car demand: /local/maison/Wallbox/Waiting-for-car-demand.png
  Disconnected: /local/maison/Wallbox/Disconnected.png
  Charging: /local/maison/Wallbox/Charging.png

A noter que j’ai placé les 5 fichiers png dans le dossier /config/www/maison/Wallbox