{"id":5248,"date":"2023-10-21T15:56:49","date_gmt":"2023-10-21T13:56:49","guid":{"rendered":"https:\/\/www.sigalou-domotique.fr\/?p=5248"},"modified":"2026-01-04T21:26:27","modified_gmt":"2026-01-04T19:26:27","slug":"utiliser-un-script-python-pour-enregistrer-des-donnees-dans-ha","status":"publish","type":"post","link":"https:\/\/www.sigalou-domotique.fr\/en\/utiliser-un-script-python-pour-enregistrer-des-donnees-dans-ha","title":{"rendered":"Using a Python script to save data in HA"},"content":{"rendered":"<p>I warn immediately, this Retex is only for pedagogical purposes, I write it to share a methodology, namely that I have realized this feature differently since then, so I do not have this process in prod.<!--more--><\/p>\r\n<h3>Integration to run the Python script<\/h3>\r\n<p>For a reason I never understood, the python scripting features built into HA are archi-mini, no import is possible, so I have to use an integration called <a href=\"https:\/\/github.com\/custom-components\/pyscript\" target=\"_blank\" rel=\"noopener\">pyscript<\/a>.<\/p>\r\n<p><a href=\"https:\/\/hacs-pyscript.readthedocs.io\/en\/latest\/\" target=\"_blank\" rel=\"noopener\">The doc's here.<\/a><\/p>\r\n<p>I pass the details to install it, it is very simple with HACS and everything is in the doc.<\/p>\r\n<p>In configuration.yaml, I have these lines:<\/p>\r\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\">pyscript: allow_all_imports: true hass _is _global: true<\/pre>\r\n<h3>I get a token<\/h3>\r\n<p>I open the Home Assistant user interface.<\/p>\r\n<p>In the lower left corner, I click on my username (or profile icon).<\/p>\r\n<p>I click on \"Profile\".<\/p>\r\n<p>I scroll down to the \u2018Long-term access tokens\u2019 section.\u00a0<\/p>\r\n<p>I click on \"Create Token\" to generate a new token.<\/p>\r\n<p>I give my token a name to make it easier for me to find it.<\/p>\r\n<p>Once the token is created, I see its contents. I copy the value of the token, i.e. the alphanumeric character series, and paste it into my configuration (next paragraph)<\/p>\r\n<h3>Declaration of the platform that makes the link between Python and the sensor<\/h3>\r\n<p>In configuration.yaml, I add these lines:<\/p>\r\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\">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' # Replace TOKEN with the token cf previous paragraph payload: '{{ data | tojson }}'  <\/pre>\r\n<p>This is the front door that will be supplied by the Python script (below) and will send the information to the sensor.wallbox _charges.<\/p>\r\n<h3>My Python script<\/h3>\r\n<p>I place the _charges.py wallbox file in the \/config\/pyscript folder<\/p>\r\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\">import datetime import pytz @service def wallbox _charges(lastdata=None, action=None, price=\"123\", km=\"123\", dateStart=\"01\/01\/2000 12:00:00\", value=\"123\"): _time _paris = pytz.timezone(\"Europe\/Paris\") now _timestamp = int(datetime.datetime.now().timestamp()) now _time _paris = datetime.datetime.fromtimestamp(now _timestamp, _time _paris) now _formatee = now _time _paris.strftime(\"%d\/%m\/%Y %H:%M:%S\") nowMoins20min = datetime.datetime.now() - datetime.timedelta(minutes=20) nowMoins20min _formatee = nowMoins20min.strftime(\"%H:%M\") dateDebutRecue = datetime.datetime.strptime(dateDebut, '%d\/%m\/%Y %H:%M:%S').replace(tzinfo=fuseau_heure_paris) debut_timestamp_formatee = dateDebutRecue.strftime(\"%d\/%m\/%Y %H:%M:%S\") debut_timestamp_formatee_SansSecondes = dateDebutRecue.strftime(\"%d\/%m\/%Y %H:%M\") # Turn strings into datetime objects now _dt = datetime.datetime.strptime(now _formatee, \"%d\/%m\/%Y %H:%M:%S\") debut_dt = datetime.datetime.strptime(debut_timestamp_formatee, \"%d\/%m\/%Y %H:%M:%S\") # Calculate the difference between timetime objectsDeCharge = now_dt - debut_dt - datetime.timedelta(minutes=20) # Format the duration in hours, minutes and seconds hours, rest = divmod(timeDeCharge.seconds, 3600) minutes, seconds = divmod(rest, 60) # Display duration as HH:MM:SS timeDeCharge_formatee = f\"{hours:02}:{minutes:02}\" # Write an information message in the newspaper #log.info(action) if action == \"register\": valueaadd = [{ \"saved_at\": now _timestamp, \"beginning\": debut_timestamp_formatee_SansSecondes, \"end\": nowLess20min _formatee, \"duration\": timeDeCharge_formatee, \"consumption\": value, \"km\": km, \"cost\": round(value * price, 2) }] if lastdata is not None: valuea add = lastdata + valuea add data = { \"state\": debut_timestamp_formatee_SansSecondes, \"attributes\": { \"updated _at\": now _dt.strftime(\"%d\/%m\/%Y %H:%M:%S\"), \"charge session\": valueadd } } if action == \"init\": data = { \"state\": \"on\", \"attributes\": { \"updated _at\": \u201814\/10\/2023 16:42:39\u2019, \u2018sessionscharge\u2019: [ { \"consumption\": 2.14, \"Cost\": 0.43, \"duration\": \"00:27\", \"beginning\": \u201819\/09\/2023 20:36\u2019, \u2018end\u2019: \"21:03\", \"km\": 17, \"saved _at\": 1695150359 }, { \"consumption\": 23.29, \"cost\": 4.66, \"duration\": \"04:19\", \"beginning\": \u201821\/09\/2023 21:06\u2019, \u2018end\u2019: \"01:25\", \"km\": 192, \"saved _at\": 1695323159 }, { \"consumption\": 7.68, \"cost\": 1.54, \"duration\": \"01:23\", \"beginning\": \u201822\/09\/2023 08:45\u2019, \u2018end\u2019: \"10:08\", \"km\": 63, \"saved_at\": 1695409559 }, { \"consumption\": 23.80, \"cost\": 4.76, \"duration\": \"04:26\", \"beginning\": \u201823\/09\/2023 16:57\u2019, \u2018end\u2019: \"21:23\", \"km\": 196, \"saved _at\": 1695495959 } ] } } # Call the REST service to send the data service _data = { \"token\": \"xxxxxx\", # Replace with the token \"data\": data } if action == \"register\" or action == \"init\": # Call the REST service asynchronously await hass.services.async_call('rest_command', 'send_data_to_api', _data service) # Write an error message in the log #log.error(\"End script\")<\/pre>\r\n<h3>Add two automations<\/h3>\r\n<p>One to initialise the sensor, the other to add data<\/p>\r\n<h4>Initialize data<\/h4>\r\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\">alias: Initializes load sessions description: \"\" trigger: [] condition: [] action: - service: pyscript.wallbox _charges data: Action: init mode: single\r\n<\/pre>\r\n<h4>Save data<\/h4>\r\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\">alias: Saves a load session description: &gt;- This automation is triggered when the sensor has not changed its value for 20 minutes and is not at 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: &gt;- {{ 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: record value: \"{{states('sensor.wallbox _portal _added_energy')}}\" prize: \"{{states('sensor.wallbox _portal _energy_price')}}\" km: \"{{states('sensor.wallbox _portal _added_range')}}\" lastdata: \"{{state _attr('sensor.wallbox _charges', 'sessionscharge')}}\" dateStart: &gt;- {{ 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: &gt;- {{ 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<\/pre>\r\n<p>To have the minimum value of the load, I use statistics to have the min value<\/p>\r\n<p>This sensor is declared in sensors.yaml<\/p>\r\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"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\n   <\/pre>\r\n<h3>Result of storing information<\/h3>\r\n<p><a href=\"https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-5258\" src=\"https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge.png\" alt=\"\" width=\"729\" height=\"546\" srcset=\"https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge.png 729w, https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge-600x449.png 600w, https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge-300x225.png 300w\" sizes=\"auto, (max-width: 729px) 100vw, 729px\" \/><\/a><\/p>\r\n<h3>Display card of all sessions<\/h3>\r\n<p><a href=\"https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge2.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-5261\" src=\"https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge2.png\" alt=\"\" width=\"388\" height=\"375\" srcset=\"https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge2.png 388w, https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge2-300x290.png 300w\" sizes=\"auto, (max-width: 388px) 100vw, 388px\" \/><\/a><\/p>\r\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\">&lt;div&gt;Charging sessions&lt;\/div&gt; \r\nLast update: {{ state_attr(&#039;sensor.wallbox _charges&#039;,&#039;updated _at&#039;)}} &lt;br&gt;&lt;br&gt;\r\n{% set items = state_attr(&#039;sensor.wallbox _charges&#039;, &#039;sessionscharge&#039;) %}\r\n&lt;table width=&#039;100%&#039;&gt;\r\n&lt;tbody&gt;\r\n{% for i in range(0, items | count, 1) %}\r\n&lt;tr&gt;\r\n&lt;td&gt;{{ items[i].start }} to {{ items[i].fin }} ({{ items[i].duration }})&lt;\/td&gt;\r\n&lt;td&gt;{{ items[i].consumption }} kWh&lt;\/td&gt;\r\n&lt;td&gt;+{{ items[i].km }} km&lt;\/td&gt;\r\n&lt;td&gt;{{ items[i].cost }} &euro;&lt;\/td&gt;\r\n&lt;\/tr&gt;\r\n\r\n{% endfor %}\r\n&lt;\/tbody&gt;\r\n&lt;\/table&gt;<\/pre>\r\n<p>Display card with totals by year, month, week<\/p>\r\n<p><a href=\"https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge3.png\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-5262\" src=\"https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge3.png\" alt=\"\" width=\"383\" height=\"415\" srcset=\"https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge3.png 383w, https:\/\/www.sigalou-domotique.fr\/wp-content\/uploads\/2023\/10\/charge3-277x300.png 277w\" sizes=\"auto, (max-width: 383px) 100vw, 383px\" \/><\/a><\/p>\r\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\">Type: markdown content: &amp;gt;-\r\n  &lt;div&gt;&lt;h1&gt;Charging sessions&lt;\/div&gt;  {%- set default _language = &#039;en&#039; %} Latest update: {{ state_attr(&#039;sensor.wallbox _charges&#039;,&#039;updated _at&#039;)}} &lt;br&gt;&lt;br&gt;\r\n\r\n\r\n  {% set items = state_attr(&#039;sensor.wallbox _charges&#039;, &#039;sessionscharge&#039;) %} {% 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_months = 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 &amp;gt;= as_timestamp(uneannee) %} {% set power_annee.value = power_annee.value + items[i].consumption %} {% set cout_annee.value = cout_annee.value + items[i].cost %} {% if items[i].saved_at | int &amp;gt;= as_timestamp(unmonth) %} {% set power_mois.value = power_mois.value + items[i].consumption %} {% set cout_mois.value = cout_mois.value + items[i].cost %} {% if items[i].saved_at | int &amp;gt;= as_timestamp(sevendays) %} {% set power_semaine.value = power_semaine.value + items[i].consumption %} {% set cout_semaine.value = cout_semaine.value + items[i].cost %} {% endif %} {% endif %} {% endif %} {% endfor %}\r\n\r\n  &lt;\/ha-alert&gt; &lt;ha-alert alert-type=&quot;info&quot; title=&quot;Last month: {{ power _mois.value | int }} kWh - {{ cost _mois.value | float | round(2) }} &euro;&quot;&gt;&lt;\/ha-alert&gt; &lt;ha-alert alert-type=&quot;info&quot; title=&quot;Last year: {{ power_annee.value | int }} kWh - {{ cout _annee.value | float | round(2) }} &euro;&quot;&gt;&lt;\/ha-alert&gt;\r\n\r\n\r\n  &lt;br&gt; \r\n\r\n\r\n  {% set items = state_attr(&#039;sensor.wallbox _charges&#039;, &#039;sessionscharge&#039;) %} {% set septjours = now() - timedelta(days=7) %}\r\n\r\n  &lt;table width=&#039;100%&#039;&gt; &lt;tbody&gt;  {% for i in range(0, items | count, 1) %} {% if items[i].saved_at | int &amp;gt;= as_timestamp(sevendays) %} {% set datetime _obj = strptime(items[i].start, &#039;%d\/%m\/%Y %H:%M&#039;) %}\r\n\r\n  &lt;tr&gt; &lt;td&gt;&lt;h5&gt;{{items[i].start }} to {{ items[i].fin }} ({{ items[i].duration }})&lt;\/td&gt; &lt;td&gt;{{ items[i].consumption }} kWh&lt;\/td&gt; &lt;td&gt;+{{ items[i].km }} km&lt;\/td&gt; &lt;td&gt;{{ items[i].cost }} &euro;&lt;\/td&gt; &lt;\/tr&gt; {% endif %} {% endfor %} &lt;\/tbody&gt; &lt;\/table&gt;\r\n    &lt;ha-alert alert-type=&quot;success&quot; title=&quot;Last week: {{ power_semaine.value | int}} kWh - {{ cost_semaine.value | float | round(2) }}&euro;&quot;&gt;    \r\nstyle: .: | 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; }\r\n<\/pre>\r\n<p>&nbsp;<\/p>","protected":false},"excerpt":{"rendered":"<p>I warn immediately, this Retex is only for pedagogical purposes, I write it to share a methodology, namely that I have realized this feature differently since then, so I do not have this process in prod.<\/p>","protected":false},"author":1,"featured_media":5257,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[14,53,166],"tags":[81],"class_list":["post-5248","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-domotique","category-home-assistant","category-retex","tag-developpement"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.sigalou-domotique.fr\/en\/wp-json\/wp\/v2\/posts\/5248","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.sigalou-domotique.fr\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.sigalou-domotique.fr\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.sigalou-domotique.fr\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.sigalou-domotique.fr\/en\/wp-json\/wp\/v2\/comments?post=5248"}],"version-history":[{"count":0,"href":"https:\/\/www.sigalou-domotique.fr\/en\/wp-json\/wp\/v2\/posts\/5248\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.sigalou-domotique.fr\/en\/wp-json\/wp\/v2\/media\/5257"}],"wp:attachment":[{"href":"https:\/\/www.sigalou-domotique.fr\/en\/wp-json\/wp\/v2\/media?parent=5248"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.sigalou-domotique.fr\/en\/wp-json\/wp\/v2\/categories?post=5248"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.sigalou-domotique.fr\/en\/wp-json\/wp\/v2\/tags?post=5248"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}