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;
        }

Â