Home Assistant - Recuperare informazioni aggiuntive da Netatmo (termostato)
In questa guida cerco di spiegare passo-passo (diciamo per principianti) come recuperare le informazioni aggiuntive fornite dalle API di Netatmo, che l’integrazione ufficiale purtroppo non espone. Le informazioni che interessavano a me (e che vedremo in questo esempio) sono l’orario di fine override (nel caso si imposti una temperatura manualmente) e la schedulazione settimanale. Come sempre cercherò di scrivere un articolo come piace a me: inserirò le informazioni necessarie senza dilungarmi troppo, senza continue ripetizioni (in stile SEO) e andando al punto, ma inserendo le informazioni che è necessario sapere (per capire cosa si sta facendo).
Come prima cosa, naturalmente, dovete avere un’installazione di Home Assistant funzionante e correttamente configurata. Inoltre bisogna chiaramente avere un termostato Netatmo già installato e funzionante con l'app Netatmo.
1. Configurazione tipo (HW/SW)
Nel mio caso mi trovo nella seguente situazione (cioè, se avete la mia stessa configurazione e seguite la guida, vi garantisco che funziona):
- Raspberry Pi 4 – 8 GB
- Home Assistant 2022.11.5
- Supervisor 2022.11.2
- Operating System 9.3
- NodeRed 13.5.3
2. Prerequisiti
Come già detto dovete avere:
- Installazione di Home Assistant (qualunque tipologia) funzionante
- Node-Red installato, configurato e funzionante (non obbligatorio, io l'ho usato perché per me è più comodo, voi potete anche convertire la logica usando le automazioni di HA)
3. Configurazione dell'account Netatmo
Se non avete ancora creato nessuna "App", vi si aprirà direttamente il form per inserire le informazioni (altrimenti cliccate sul bottone "Create" in alto a destra):
Inseriamo tutti i campi obbligatori e spuntiamo la casella in fondo. Fatto questo si abiliterà il bottone "Salva" in fondo alla pagina. Clicchiamoci sopra
Terminato il salvataggio si aprirà una sezione chiamata "App Technical Parameters", che contiene 2 dati fondamentali: "client ID" e "client secret". Segniamoceli da qualche parte, perché ci serviranno in seguito.
4. Autenticazione
- Authorization code: è il metodo standard, si chiama la WebAPI apposita indicando i dati (ID e secret) e un url per la risposta. Una volta convalidati i dati passati verrà invocato l'url di risposta. Noi NON utilizzeremo questo metodo.
- Client credentials: questo metodo è utilizzabile SOLO per scopi personali, è più "comodo" per i nostri scopi. Si fa una chiamata passando una serie di dati e lui risponde con 2 token (authentication e refresh token). Noi utilizzeremo QUESTO metodo
- Per prima cosa bisogna chiamare il metodo "password", passandogli tutte le informazioni richieste
- Se abbiamo inserito le informazioni corrette, il metodo ritorna 3 parametri: "access_token", "expires_in" e "refresh_token".
- access_token è il token da utilizzare per fare le chiamate alle API
- expires_in è il tempo (in secondi) di validità di access_token
- refresh_token è la chiave da utilizzare per "rinnovare" l'access_token
- Quando access_token scade, bisogna chiamare il metodo "refresh_token" per ottenere un nuovo access_token
5. Ottenere e aggiornare i Token
[{"id":"3843908b20b546be","type":"tab","label":"GetNetatmoToken","disabled":true,"info":"","env":[]},{"id":"028b51485b704c5b","type":"debug","z":"3843908b20b546be","name":"debug 2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1080,"y":100,"wires":[]},{"id":"b671f19e61752b57","type":"bigtimer","z":"3843908b20b546be","outtopic":"","outpayload1":"","outpayload2":"","name":"Big Timer","comment":"","lat":0,"lon":0,"starttime":"0","endtime":"1439","starttime2":0,"endtime2":0,"startoff":"0","endoff":0,"startoff2":0,"endoff2":0,"offs":0,"outtext1":"","outtext2":"","timeout":1440,"sun":true,"mon":true,"tue":true,"wed":true,"thu":true,"fri":true,"sat":true,"jan":true,"feb":true,"mar":true,"apr":true,"may":true,"jun":true,"jul":true,"aug":true,"sep":true,"oct":true,"nov":true,"dec":true,"day1":0,"month1":0,"day2":0,"month2":0,"day3":0,"month3":0,"day4":0,"month4":0,"day5":0,"month5":0,"day6":0,"month6":0,"day7":0,"month7":0,"day8":0,"month8":0,"day9":0,"month9":0,"day10":0,"month10":0,"day11":0,"month11":0,"day12":0,"month12":0,"d1":0,"w1":0,"d2":0,"w2":0,"d3":0,"w3":0,"d4":0,"w4":0,"d5":0,"w5":0,"d6":0,"w6":0,"xday1":0,"xmonth1":0,"xday2":0,"xmonth2":0,"xday3":0,"xmonth3":0,"xday4":0,"xmonth4":0,"xday5":0,"xmonth5":0,"xday6":0,"xmonth6":0,"xday7":0,"xmonth7":0,"xday8":0,"xmonth8":0,"xday9":0,"xmonth9":0,"xday10":0,"xmonth10":0,"xday11":0,"xmonth11":0,"xday12":0,"xmonth12":0,"xd1":0,"xw1":0,"xd2":0,"xw2":0,"xd3":0,"xw3":0,"xd4":0,"xw4":0,"xd5":0,"xw5":0,"xd6":0,"xw6":0,"suspend":false,"random":false,"randon1":false,"randoff1":false,"randon2":false,"randoff2":false,"repeat":true,"atstart":true,"odd":false,"even":false,"x":100,"y":80,"wires":[[],["57f3cdd19472aabd"],[]]},{"id":"38381b2a565db1d7","type":"http request","z":"3843908b20b546be","name":"NetatmoRequestToken","method":"POST","ret":"obj","paytoqs":"ignore","url":"https://api.netatmo.com/oauth2/token","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[{"keyType":"Content-Type","keyValue":"","valueType":"other","valueValue":"application/x-www-form-urlencoded;charset=UTF-8"}],"x":860,"y":220,"wires":[["028b51485b704c5b","94209b6f667e6475","c19dd44366ac1da6","79e3ea5426a00d40"]]},{"id":"f3abdc833f5cdaea","type":"inject","z":"3843908b20b546be","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"grant_type=password&client_id=myclid&client_secret=myclscr&username=pippo%40gmail.com&password=123456&scope=read_station read_thermostat","payloadType":"str","x":70,"y":400,"wires":[["57f3cdd19472aabd"]]},{"id":"94209b6f667e6475","type":"api-call-service","z":"3843908b20b546be","name":"Set Expiration Value","server":"72e5b6ee.b05ef8","version":5,"debugenabled":false,"domain":"input_text","service":"set_value","areaId":[],"deviceId":[],"entityId":["input_text.netatmotokenexpiration"],"data":"{\t \"value\": $sum([$toMillis($now()), (payload.expires_in*1000)])\t}\t","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1160,"y":240,"wires":[[]]},{"id":"c19dd44366ac1da6","type":"api-call-service","z":"3843908b20b546be","name":"Set Token Value","server":"72e5b6ee.b05ef8","version":5,"debugenabled":false,"domain":"input_text","service":"set_value","areaId":[],"deviceId":[],"entityId":["input_text.netatmotoken"],"data":"{\t \"value\": payload.access_token\t}\t","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1140,"y":320,"wires":[[]]},{"id":"79e3ea5426a00d40","type":"api-call-service","z":"3843908b20b546be","name":"Set Refresh Token Value","server":"72e5b6ee.b05ef8","version":5,"debugenabled":false,"domain":"input_text","service":"set_value","areaId":[],"deviceId":[],"entityId":["input_text.netatmorefreshtoken"],"data":"{\t \"value\": payload.refresh_token\t}\t","dataType":"jsonata","mergeContext":"","mustacheAltTags":false,"outputProperties":[],"queue":"none","x":1170,"y":400,"wires":[[]]},{"id":"57f3cdd19472aabd","type":"api-current-state","z":"3843908b20b546be","name":"Scadenza Token","server":"72e5b6ee.b05ef8","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_text.netatmotokenexpiration","state_type":"str","blockInputOverrides":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":200,"y":180,"wires":[["adbafc5de0d009f5"]]},{"id":"adbafc5de0d009f5","type":"switch","z":"3843908b20b546be","name":"E' scaduto?","property":"$number(payload)\t","propertyType":"jsonata","rules":[{"t":"lt","v":"$toMillis($now())","vt":"jsonata"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":310,"y":280,"wires":[["174f5e8e454f768a"],["62a76bf1ee1cf107"]]},{"id":"d642fbb091c24342","type":"debug","z":"3843908b20b546be","name":"debug 4","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":760,"y":480,"wires":[]},{"id":"fff28ce24e7eadd6","type":"function","z":"3843908b20b546be","name":"PayloadNewToken","func":"var msg = {\n payload: \"grant_type=password&client_id=myclid&client_secret=myclscr&username=pippo%40gmail.com&password=123456&scope=read_station read_thermostat\"\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":160,"wires":[["38381b2a565db1d7"]]},{"id":"62a76bf1ee1cf107","type":"debug","z":"3843908b20b546be","name":"debug 5","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":380,"y":440,"wires":[]},{"id":"174f5e8e454f768a","type":"api-current-state","z":"3843908b20b546be","name":"Refresh Token","server":"72e5b6ee.b05ef8","version":3,"outputs":1,"halt_if":"","halt_if_type":"str","halt_if_compare":"is","entity_id":"input_text.netatmorefreshtoken","state_type":"str","blockInputOverrides":false,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"entityState"},{"property":"data","propertyType":"msg","value":"","valueType":"entity"}],"for":"0","forType":"num","forUnits":"minutes","override_topic":false,"state_location":"payload","override_payload":"msg","entity_location":"data","override_data":"msg","x":520,"y":240,"wires":[["f49a7e9979fdc551"]]},{"id":"79b4c45909fdf9a5","type":"inject","z":"3843908b20b546be","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"grant_type=password&client_id=myclid&client_secret=myclscr&username=pippo%40gmail.com&password=123456&scope=read_station read_thermostat","payloadType":"str","x":370,"y":60,"wires":[["fff28ce24e7eadd6"]]},{"id":"f49a7e9979fdc551","type":"function","z":"3843908b20b546be","name":"PayloadRefreshToken","func":"var pl = \"grant_type=refresh_token&refresh_token=\";\npl = pl + msg.payload.replace('|', '%7C');\npl = pl + \"&client_id=myclid&client_secret=myclscr\";\nvar msg = {\n payload: pl\n}\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":680,"y":340,"wires":[["d642fbb091c24342","38381b2a565db1d7"]]},{"id":"72e5b6ee.b05ef8","type":"server","name":"Home Assistant","addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"","connectionDelay":false,"cacheJson":false,"heartbeat":false,"heartbeatInterval":"","statusSeparator":"","enableGlobalContextStore":false}]
"grant_type=password&client_id=myclid&client_secret=myclscr&username=pippo%40gmail.com&password=123456&scope=read_station read_thermostat"
- myclid con il vostro "client ID" ottenuto al punto 3.
- myclscr con il vostro "client secret" ottenuto sempre al punto 3
- pippo%40gmail.com con la mail che avete utilizzato per la registrazione a Netatmo.
ATTENZIONE: il valore deve essere URLENCODED (codificato per url). Potete usare un tool online (come ad esempio questo URL Encode and Decode - Online (urlencoder.org)) per ottenere il valore codificato - 123456 con la vostra password dell'account Netatmo.
ATTENZIONE: il valore deve essere URLENCODED (codificato per url). Potete usare un tool online (come ad esempio questo URL Encode and Decode - Online (urlencoder.org)) per ottenere il valore codificato
Nella sezione che si aprirà sulla destra, dobbiamo andare a sostituire alcuni parti del contenuto della terza riga. In particolare il contenuto è il seguente:
&client_id=myclid&client_secret=myclscr
- myclid con il vostro "client ID" ottenuto al punto 3.
- myclscr con il vostro "client secret" ottenuto sempre al punto 3
Spiegazione del Flow
- il primo è collegato a "PayloadNewToken" e serve la prima volta per "lanciare il comando" di ottenimento token.
Dopo la prima volta non dovrebbe più servire, ma potrebbe anche tornare utile nel caso di problemi ai server di Netatmo, oppure se lasciassimo spento HA per molto tempo (non è chiaro se questo token venga cancellato dopo un certo periodo di tempo, la documentazione non lo specifica) - il secondo è collegato a "Scadenza Token" e serve per "forzare" manualmente il controllo sulla scadenza
- Il primo manda un segnale ogni volta che il timer si accende o si spegne
- Il secondo manda un segnale ogni minuto in cui il timer rimane acceso
- Il terzo serve per messaggi TTS o per debug
- "debug 2" scrive il risultato nella console di debug, ci server per controllare se va tutto bene e vedere eventuali errori
- "Set Expiration Value" scrive in "input_text.netatmotokenexpiration" la data/ora di scadenza appena ricevuta
- "Set Token Value" scrive in "input_text.netatmotoken" il valore di Access Token appena ricevuto
- "Set Refresh Token Value" scrive in "input_text.netatmorefreshtoken" il valore di Refresh Token appena ricevuto
6. I WebService "Energia" di Netatmo
Nota: vi consiglio di fare un po' di prove modificando delle impostazioni per vedere tutti i parametri che possono comparire. Ad esempio "therm_setpoint_start_time" e "therm_setpoint_end_time" sono presenti solo se sul termostato viene impostata una temperatura manuale.
7. I sensori REST
rest:
####################################################
# #
# NETATMO #
# #
####################################################
- resource: https://api.netatmo.com/api/homestatus?home_id=home_id
scan_interval: 600
timeout: 60
headers:
Content-Type: application/json
Authorization: >
Bearer {{ states("input_text.netatmotoken") }}
sensor:
- name: NetatmoT Next Period
value_template: "{{ value_json.body.home.rooms[0].therm_setpoint_end_time }}"
- name: NetatmoT Start Period
value_template: "{{ value_json.body.home.rooms[0].therm_setpoint_start_time }}"
- name: Netatmo Sala Raggiungibile
value_template: "{{ value_json.body.home.rooms[0].reachable }}"
- name: Netatmo Sala Usa Antic.
value_template: "{{ value_json.body.home.rooms[0].anticipating }}"
- name: Netatmo Sala Perc. Apertura Valvole
value_template: "{{ value_json.body.home.rooms[0].heating_power_request }}"
- name: Netatmo Sala Finestra Aperta
value_template: "{{ value_json.body.home.rooms[0].open_window }}"
- home_id è il codice alfanumerico che identifica la vostra casa (quello che avete messo prima nella chiamata di esempio sul sito di Netatmo)
- "scan_interval: 600" indica che andiamo a richiedere i dati ogni 10 minuti (600 secondi)
8. I sensori Command Line
- "zone_id" che indica la temperatura da tenere nella fascia oraria (a me non interessava andarla a recuperare, ma se a voi serve, più in basso sono definite tutte le "zones")
- "m_offset" che indica l'orario (in minuti dalle 00:00 del lunedì) di inizio della fascia oraria
sensor:
- platform: command_line
name: netatmo_schedules
scan_interval: 3600
command: >
echo "{\"events\":" $(
curl
-s
-H 'Authorization: Bearer {{ states('input_text.netatmotoken') }}'
'https://api.netatmo.com/api/homesdata'
) "}"
value_template: >
{{ value_json.events | length }}
json_attributes:
- events
9. I sensori Template
sensor:
- platform: template
sensors:
#Netatmo (termostato) - Mantiene temperatura fino a
netatmot_held_until:
friendly_name: "Temp. Fino alle"
value_template: >
{% set manual = states('sensor.netatmot_next_period') %}
{% set epochnow = as_timestamp(now()) %}
{% set weekminutenow = now().weekday() * 1440 + now().hour*60 + now().minute %}
{% set monday = now().timestamp() - now().weekday() *86400 - now().hour * 3600 - now().minute * 60 %}
{% set found = namespace(h_bool=False) %}
{% set minutesfound = namespace(found=0) %}
{% set getschedule = false %}
{% if manual | string == '' %}
{% set getschedule = true %}
{% elif manual is undefined %}
{% set getschedule = true %}
{% elif manual | string == 'unknown' %}
{% set getschedule = true %}
{% elif manual | float < epochnow %}
{% set getschedule = true %}
{% endif %}
{% if getschedule == true %}
{% for i in states.sensor.netatmo_schedules.attributes.events.body.homes[0].schedules[0].timetable if found.h_bool == false %}
{% if weekminutenow < i.m_offset %}
{% set found.h_bool = true %}
{% set minutesfound.found = i.m_offset %}
{% endif %}
{% endfor %}
{% else %}
{{ manual | int | timestamp_custom('%H:%M') }}
{% endif %}
{% if found.h_bool %}
{{ (monday | int + minutesfound.found*60) | int | timestamp_custom('%H:%M') }}
{% endif %}
icon_template: "mdi:clock"
#Netatmo (termostato) - Inizio schedulazione
netatmot_start_sched:
friendly_name: "Schedulazione iniz. alle"
value_template: >
{% set manual = states('sensor.netatmot_start_period') %}
{% set epochnow = as_timestamp(now()) %}
{% set weekminutenow = now().weekday() * 1440 + now().hour*60 + now().minute %}
{% set monday = now().timestamp() - now().weekday() *86400 - now().hour * 3600 - now().minute * 60 %}
{% set found = namespace(h_bool=False) %}
{% set minutesfound = namespace(found=0) %}
{% set getschedule = false %}
{% if manual | string == '' %}
{% set getschedule = true %}
{% elif manual is undefined %}
{% set getschedule = true %}
{% elif manual | string == 'unknown' %}
{% set getschedule = true %}
{% endif %}
{% if getschedule == true %}
{% for i in states.sensor.netatmo_schedules.attributes.events.body.homes[0].schedules[0].timetable if found.h_bool == false %}
{% if weekminutenow > i.m_offset %}
{% set minutesfound.found = i.m_offset %}
{% else %}
{% set found.h_bool = true %}
{% endif %}
{% endfor %}
{% else %}
{{ manual | int | timestamp_custom('%H:%M') }}
{% endif %}
{% if found.h_bool %}
{{ (monday | int + minutesfound.found*60) | int | timestamp_custom('%H:%M') }}
{% endif %}
icon_template: "mdi:clock"
Commenti
Posta un commento