Quante volte hai dato il comando di spegnere una luce o una presa smart in Home Assistant, ma poi scopri che in realtà è rimasta accesa? Magari a causa di un problema di connessione, un’interferenza o semplicemente perché il dispositivo non ha ricevuto correttamente il comando. Questa dovrebbe essere una funzione base di Home Assistant, ma purtroppo non c’è un controllo nativo che verifichi se un’entità ha effettivamente cambiato stato dopo il comando. Ma ora c’è State Enforcer!
State Enforcer si applica a qualunque dispositivo come luci o prese comandate ma ci sono diversi casi che rendono questo approccio quasi obbligatorio. Pensa infatti se il problema si verificasse spegnendo l’irrigazione che anzi che andare per 20 minuti andasse per 10 ore finché non ti accorgi di avere un lago in giardino quando torni a casa dal lavoro…. Oppure non si spenga il boiler raggiunta la temperatura. O ancora le luci del giardino restassero accese anche per tutto il giorno, finita la notte. Un bello spreco no?
Per risolvere questo problema ho creato State Enforcer, uno script che si assicura che una luce, una presa o qualsiasi altra entità di Home Assistant si accenda o si spenga davvero, riprovando più volte se necessario. Se, nonostante i tentativi, il cambio di stato non avviene entro un timeout impostato, riceverai una notifica con due opzioni: riprova o annulla.
Sommario
Come funziona State Enforcer
Lo script funziona in modo molto semplice:
- Dai il comando di accensione o spegnimento per un’entità (ad esempio una luce o una presa smart).
- Lo script attende qualche secondo e controlla se il cambio di stato è avvenuto.
- Se l’entità non ha cambiato stato, riprova il comando più volte fino a raggiungere il timeout impostato.
- Se il dispositivo ha cambiato stato, tutto ok! Se invece, dopo vari tentativi, il cambio di stato non è riuscito, riceverai una notifica interattiva.
- La notifica ti permetterà di riprovarci manualmente o di annullare il tentativo.
Come installarlo
Per utilizzare State Enforcer, devi aggiungere il package YAML al tuo Home Assistant. Lo script richiede due dipendenze:
- Multinotify, per inviare le notifiche.
- Set State, per creare entità dinamiche.
Puoi trovare il package completo e le istruzioni dettagliate nel repository GitHub
Una volta installato, puoi usare State Enforcer con un’azione simile nelle tue automazioni e script:
actions: - action: script.turn_on target: entity_id: script.state_enforcer data: variables: entity_id: light.living_room action: off timeout: 30
Se non specifichi il timeout, verrà usato il valore predefinito di 30 secondi.
Le notifiche interattive
Se lo script non riesce a cambiare lo stato dell’entità entro il tempo impostato, ti arriverà una notifica con queste opzioni:
🔄 Riprova → Lo script tenterà nuovamente di cambiare lo stato.
❌ Annulla → Ferma il tentativo e segnala il problema.
Ecco un esempio di messaggio:


Questo ti permette di gestire il problema direttamente dal telefono o da qualsiasi altro dispositivo connesso a Home Assistant e di venire a conoscenza del problema, che altrimenti avresti ignorato per chissà quanto tempo.
Come funziona?
Se hai letto altri miei articoli sai che non mi piace condividere un lavoro se non ti spiego come funziona, così che possa essere didattico e darti degli spunti per migliorare le tue capacità di scrittura di script e automazioni per Home Assistant o, se sei già esperto, esserti di ispirazione per qualche tua creazione o, capendone il principio, possa tu personalizzare questo package per le tue specifiche esigenze.
Spezzerò nei singoli blocchi lo script per spiegarti cosa fanno i singoli pezzi. Tieni presente che il package potrebbe evolversi su github e le informazioni che seguono sono relative alla versione 1.0, che saranno comunque valide e interessanti per capire il principio di funzionamento del package.
Partiamo dallo script principale.
Lo script state_enforcer
Saltando le banalità come la definizione dei campi con i selettori per essere di comodo uso da strumenti per sviluppatori e dalla UI soffermiamoci su mode: parallel che permette allo script di essere eseguito più volte contemporaneamente. Visto che richiamiamo lo script tramite script.turn_on, come descritto sopra, questo permette a più istanze dello script di restare in esecuzione in background a cercare di attivare lo stato voluto, un’istanza indipendente per ogni entità (su quest’ultima parte ci ritorniamo dopo).
variables: service_name: "{{ 'homeassistant.turn_on' if action == 'on' else 'homeassistant.turn_off' }}" expected_state: "{{ action }}" # "on" or "off" entity_lock: "tmp.lock_toggle_{{ entity_id | replace('.', '_') }}" # Entity name we will use to lock the execution based on the specific entity timeout_sec: "{{ timeout | default(30) }}" # Timeout with default value start_time: "{{ now().timestamp() }}" # Start time we will use to evaluate time elapsed for timeout
Qui impostiamo le variabili che useremo in seguito nello script:
service_name
: sarà il nome del servizio da usare per accendere o spegnere come richiesto dal parametro dello scriptexpected_state
: sarà il valore che verificheremo per controllare se l’entità è passata allo stato richiestoentity_lock
: generiamo un nome di entità temporanea (che creeremo o aggiorneremo con set_state) che ci servirà poi per garantire che ci sia un solo script in esecuzione per ogni entità richiesta dall’utente. Immagina che richiamiamo lo script per spegnere una luce e subito dopo, mentre lo script ci sta ancora provando, ne richiamiamo uno per accenderla e questi vengano eseguiti contemporaneamente! Questo meccanismo fa si che la seconda istanza faccia terminare la prima (quella per spegnere la luce), ne attenda la conclusione e poi inizi il suo lavoro di accendere la luce. Vedrai più avanti come viene fatto.timeout_sec
: è il timeout sicuramente valorizzato con un eventuale valore di default, visto che possiamo non specificare il timeoutstart_time
: data/ora di partenza, usato per calcolare quando viene raggiunto il timeout
- alias: "Create a persistent notification" action: persistent_notification.create continue_on_error: true data: title: State enforcer running message: "Enforcing state **{{ expected_state }}** for **{{ states[entity_id].name }}** (max {{ timeout_sec }} seconds)..." notification_id: "{{ entity_id }}"
Con questa azione creiamo una notifica persistente che rimarrà visibile durante l’esecuzione dello script, così che se c’è un’entità che sta facendo fatica ad essere modificata di stato ne avremo l’evidenza in modo semplice, risultando in una sorta di “Task manager” di State Enforcer, ci permetterò cioè di vedere quali attività State Enforcer stia correntemente eseguendo, visto che al termine dello script la notifica persistente viene rimossa.
# We stop other instance of this script that is working on the same entity, if present. # So if wanted to turn on a light and, while trying to do it, we issue the command to turn it off this will shit down the turn on trial and start the turn off one. # It's like having "mode: restart" for the script but based on the specific entity you requested (Home Assistant don't have such a feature, we implement it in that way) - alias: "Stop another instance on the same light, if running" if: "{{ is_state(entity_lock, 'running') }}" then: - alias: "Stop other instance" action: python_script.set_state continue_on_error: true data: allow_create: true entity_id: "{{ entity_lock }}" state: "stop" - wait_template: "{{ is_state(entity_lock, 'stopped') }}" timeout: "00:00:10" continue_on_timeout: true - if: "{{ not is_state(entity_lock, 'stopped') }}" then: - alias: "Inform of the failure" action: persistent_notification.create continue_on_error: true data: title: State enforcer warning message: > **{{ now().timestamp() | timestamp_custom("%Y-%m-%d %H:%M:%S") }}** State enforcer could not stop a previous instance while enforcing state **{{ expected_state }}** of **{{ states[entity_id].name }}**. Current entity lock value: **{{ states(entity_lock) }}**"
Questo è il punto dove, tramite l’if, verifichiamo che non ci siano istanze in esecuzione dello script sulla stessa entità (verificando se l’entità entity_lock
è in stato running
). Se lo è impostiamo tale entità sul valore “stop”, che farà terminare l’esecuzione della precedente istanza. In seguito attendiamo che entity_lock
valga stopped
, ovvero che l’istanza precedente sia terminata (viene impostato tale valore al termine dello script). Se questo non avviene nell’arco di 10 secondi viene creata un’altra notifica persistente, che questa volta non verrà cancellata.
# Here we set the lock that allows us to do what described in the previous block - alias: "Set the lock" action: python_script.set_state continue_on_error: true data: allow_create: true entity_id: "{{ entity_lock }}" state: "running"
A questo punto informiamo eventuali istanze future dello script con questa entità che lo script è in esecuzione, impostando running
nell’entità entity_lock
# This is the real core: a cycle that will exit when the state is what we desire, the lock is telling us to stop (another instance) or timeout occured - alias: "Cycle to do the action and check the state" repeat: while: - "{{ not is_state(entity_id, expected_state) }}" # State check - "{{ is_state(entity_lock, 'running') }}" # Lock check - "{{ (now().timestamp() - start_time) | int < timeout }}" # Timeout check sequence: - alias: "Try to set the state" action: "{{ service_name }}" continue_on_error: true target: entity_id: "{{ entity_id }}" - alias: "Wait 5 sec but exit if the lock requests to stop" wait_template: "{{ is_state(entity_lock, 'stop') or is_state(entity_id, expected_state) }}" timeout: "00:00:05" continue_on_timeout: true
Questo è il cuore dello script. Effettuiamo un ciclo finchè una delle seguenti condizioni non intervenga:
- lo stato dell’entità desiderata diventi quello quello voluto
- Lo stato della
entity_lock
diventi diversa darunning
(ovvero una nuova istanza sta chiedendo di terminare l’esecuzione di questa istanza) - E’ trascorso il tempo massimo impostato tramite il parametro timeout (o 30 secondi se non impostato)
Nel ciclo viene poi eseguito il servizio per spegnere o accendere l’entità voluta e quindi vengono attesi 5 secondi ma terminando l’attesa nei casi che farebbero terminare l’esecuzione ovvero se ricevuto il comando di fermare l’istanza da una nuova istanza (stop
) o l’entità specificata raggiunge finalmente lo stato desiderato.
# If we failed to set the state after the timeout specified we will call the state_enforcer_notifications script, in the other file - alias: "If could not set the desired state we have failed. Notify it to the user" if: "{{ not is_state(entity_id, expected_state) }}" then: - alias: "Async notification about the failure" action: script.turn_on continue_on_error: true target: entity_id: script.state_enforcer_notifications data: variables: entity_id: "{{ entity_id }}" expected_state: "{{ expected_state }}" timeout: "{{ timeout_sec }}"
Terminato il ciclo i casi sono due: o l’entità ha assunto lo stato desiderato oppure viene avviato lo script state_enforcer_notifications
, che vedremo in seguito, per informare del fallimento dell’operazione.
# Now we remove the lock as the script is ended, allowing new instances to know there is no other instance running - alias: "Remove the lock" action: python_script.set_state continue_on_error: true data: allow_create: true entity_id: "{{ entity_lock }}" state: "stopped"
A questo punto impostiamo la nostra entity_lock
a stopped
per informare eventuali nuove istanze dello script su questa entità che non c’è più alcuna attività in corso.
# At last we remove the persistent notification that is telling us there is an instance of State Enforcer running - alias: "Eventually clear a previous persistent notification" action: persistent_notification.dismiss continue_on_error: true data: notification_id: "{{ entity_id }}"
Infine togliamo la notifica persistente che ci informava dell’esecuzione in corso dello script su questa entità.
Le notifiche tramite lo script state_enforcer_notifications
Se i dispositivi che usiamo funzionano correttamente l’esecuzione si conclude con lo script State Enforcer visto sopra. Se invece per tutto il tempo specificato in cui sono stati fatti i retry lo script non riuscisse a spegnere o accendere il dispositivo allora interviene lo script che andiamo ora ad analizzare.
Vediamone sempre i pezzi salienti seguiti dalla descrizione del blocco di codice.
- action: script.multinotify data: title: "State enforcer timeout" message: "Non è stato possibile {{'accendere' if expected_state == 'on' else 'spegnere'}} l'entità {{ entity_name }} entro {{ timeout }} secondi. Valore attuale: {{ entity_state }}" notify_app: notify.mobile_app_tel_henrik alexa_target: media_player.piani_inferiori notify_pushover: notify.pushover notify_html5: notify.html5_hsozziedge notify_ha: true icon: warning group: state_enforcer channel: warning subtitle: "Timeout {{'accensione' if expected_state == 'on' else 'spegnimento'}} {{ entity_name }} dopo {{ timeout }} sec" app_actions: - action: "state_enforcer_retry|{{ entity_id }}|{{ expected_state }}|{{ timeout }}" icon: /local/refresh.png title: "Riprova" data: entity_id: "{{ entity_id }}" - action: "state_enforcer_cancel|{{ entity_id }}|{{ expected_state }}|{{ timeout }}" icon: /local/cancel.png title: "Annulla" data: entity_id: "{{ entity_id }}"
In sostanza lo script è tutto qui, in questa unica azione. Sembrerebbe una normale notifica tramite Multinotify invece nasconde uno spunto che secondo me val la pena approfondire. Ma andiamo con ordine.
La notifica ha un messaggio simile a “Non è stato possibile spegnere l’entità Pompa piscina entro 30 secondi. Valore attuale: on”. Viene inviata su uno smartphone, dei dispositivi Alexa, su Pushover, su client registrati HTML5, e come notifica persistente su Home Assistant (grazie a multinotify tutto questo si può fare in una unica azione!).
La peculiarità sono le azioni che vengono fornite con la notifica (e possono essere usate su smartphone e HTML5): Riprova e Annulla.
Il problema è che quando l’utente clicca su una azione (ad es. “Riprova”) di una notifica l’unico dato utile che viene passato ad Home Assistant, tramite il relativo evento che poi vediamo, è il nome dell’azione che abbiamo definito.
Ma non possiamo usare un’azione tipo “retry” in quanto dobbiamo anche indicare ad Home Assistant di quale entità vogliamo fare il retry, cosa vogliamo farci (accenderla o spegnerla?) e il timeout che era stato richiesto. Come fare? Lo mettiamo tutto nell’azione, che poi nell’automazione scomporremo in parti.
E così l’azione diventa una cosa come: state_enforcer_retry|switch.pompa_piscina|off|30 dove il carattere pipe “|” separa le parti:
- l’azione scelta dall’utente “state_enforcer_retry”
- l’entità in questione “switch.pompa_piscina”
- quale stato desideriamo che abbia “off”
- Il tempo di timeout: 30 secondi
L’annullamento invia un’azione simile ma con la prima componente “state_enforcer_cancel”. Attualmente non ci facciamo molto se non inserire una notifica persistente ma, funzionalmente, questa azione equivale a eliminare la notifica. Ma essendo gli utenti abituati ad avere messaggi di sistema con le opzioni “Riprova” e “Annulla” il fatto di poter premere “Annulla” anzi che eliminare la notifica, anche se l’effetto è del tutto simile, rende molto intuitivo capire cosa accade premendo il tasto.
Gestiamo ora le azioni “Riprova” e “Annulla”
automation: # Action from state enforcer notify - id: 2408f862-1045-4eb4-91f1-f5a47bc05c55 alias: "Azione - Notifica - State enforcer" description: "Actions from state enforcer notifications" trigger: # We get actions from mobile app and from HTML5 push notifications the same way - trigger: event event_type: - "mobile_app_notification_action" - "html5_notification.clicked" variables: # Here we set the minimun needed variables to use in conditions event_action_complete: "{{ trigger.event.data['action'] }}" action: "{{ event_action_complete | regex_findall('^([^|]+)') | first }}" conditions: # We check that the action is one of managed ones - "{{ action in ['state_enforcer_retry', 'state_enforcer_cancel']}}" action: # Here we set more variables. There is no way to "transport" some information (like entity_id, expected state and timeout) with the notification # so that they'll be posted back when the user press the action. So I've put them in the action itself and I separate the components with regex. # The action format is thus: <action>|<entity_id>|<expected_state>|<timeout> # For example if we tried to turn off light.kitchen for 60 seconds, it failed and send the notification to the user that pressed the "Retry" button # we will receive an action like this: "state_enforcer_retry|light.kitchen|off|60" - variables: entity_id: "{{ event_action_complete | regex_findall('^[^|]+\\|([^|]+)') | first }}" expected_state: "{{ event_action_complete | regex_findall('^[^|]+\\|[^|]+\\|([^|]+)') | first }}" timeout: "{{ (event_action_complete | regex_findall('^[^|]+\\|[^|]+\\|[^|]+\\|([^|]+)') | first | int(30)) }}" entity_name: "{{ states[entity_id].name }}" - alias: "Depending on user reply" choose: - conditions: - "{{ action == 'state_enforcer_retry'}}" # User preseend "Retry" sequence: - alias: "Async retry state enforcer" action: script.turn_on continue_on_error: true target: entity_id: script.state_enforcer data: variables: entity_id: "{{ entity_id }}" action: "{{ expected_state }}" timeout: "{{ timeout }}" - conditions: - "{{ action == 'state_enforcer_cancel' }}" # User preseend "Cancel" sequence: - alias: "Inform that user cancelled" action: persistent_notification.create continue_on_error: true data: title: "State enforcer: annullato dall'utente" message: > Non è stato possibile {{'accendere' if action == 'on' else 'spegnere'}} l'entità {{ entity_name }} entro {{ timeout }} secondi In seguito (**{{ now().timestamp() | timestamp_custom("%Y-%m-%d %H:%M:%S") }}**) un utente ha annullato eventuali ulteriori tentativi
Questa automazione può spaventare vista così ma è più semplice di quanto sembri, vediamola.
- trigger: event event_type: - "mobile_app_notification_action" - "html5_notification.clicked"
Il trigger gestisce sia l’evento mobile_app_notification_action
che html5_notification.clicked
ovvero sia le azioni che arrivano dalle notifiche alle app companion che dai client HTML5.
variables: # Here we set the minimun needed variables to use in conditions event_action_complete: "{{ trigger.event.data['action'] }}" action: "{{ event_action_complete | regex_findall('[^|]+') | first }}" conditions: # We check that the action is one of managed ones - "{{ action in ['state_enforcer_retry', 'state_enforcer_cancel']}}"
Qui estraiamo le variabili minime per determinare se l’azione è derivata da State Enforcer oppure da altro che dobbiamo ignorare.
Per farlo impostiamo la variabile event_action_complete
che contiene l’intera azione composta di più parti come spiegato sopra e da essa estraiamo action
che contiene solo la prima parte dell’azione, ovvero state_enforcer_retry
o state_enforcer_cancel
. Lo facciamo tramite una Regular Expression usando la funzione regex_findall.
A questo punto, come condizione, verifichiamo che l’azione sia una tra le due previste. Se così è andiamo ad estrarre anche gli altri parametri tramite le seguenti impostazioni di ulteriori variabili:
- variables: entity_id: "{{ (event_action_complete | regex_findall('[^|]+'))[1] }}" expected_state: "{{ (event_action_complete | regex_findall('[^|]+'))[2] }}" timeout: "{{ (event_action_complete | regex_findall('[^|]+'))[3] | int(30) }}" entity_name: "{{ states[entity_id].name }}"
In entity_id
mettiamo la seconda componente, in expected_state
la terza ed in timeout
la quarta. Infine in entity_name
preleviamo il nome dell’entità per scrivere un messaggio di senso compiuto.
Da qui in poi è semplice: se è stato cliccato Riprova viene ora richiamato nuovamente lo script state_enforcer
con i parametri estratti sopra, se invece è stato premuto Annulla viene aggiunta una persistent notification ad Home Assistant.
Conclusione
State Enforcer è una soluzione semplice ma efficace per evitare che le tue automazioni falliscano senza che tu te ne accorga. Ora puoi essere sicuro che le luci e le prese si accendano o si spengano davvero, e in caso di problemi, sarai subito informato con la possibilità di intervenire.
Se vuoi provarlo, trovi il codice completo nel mio blog! Se hai domande o suggerimenti, scrivimi nei commenti. 😉
Buongiorno Henrik, grazie della condivisione, idea molto utile soprattutto per gli esempi che hai riportato. Giusta la Nota ‘dispositivi critici’: se non sbaglio i controlli possono essere anche implementati a cascata?Personalmente mi viene in mente il mio termo-arredo: se non si accendesse o peggio non si spegnesse sarebbero guai, ma oltre all’interruttore il dispositivo è dotato di un termostato e in questo caso la sicurezza c’è (almeno finche il termostato funziona!!), proverò ad applicare senz’altro il tuo script.
Avrei bisogno di capire meglio ESPHOME come controllo locale e Home Assitant solo ‘informato’…
Comunque un saluto e un grazie.
P.S.: un problema simile è quello delle integrazioni, nello specifico l’integrazione del termostato bTicino che spesso si trova in ‘state: unavailable’, e allora succede che se automatizzi il termostato ti puoi trovare con la casa fredda o viceversa.
Ho provato a risolvere il problema con una soluzione assai meno sofisticata della tua: – ho creato un’automazione con un choose che se l’integrazione è in ‘state: unavailable’ allora utilizzo la funzione ‘homeassistant.reload_config_entry’ fino alla sua uscita dallo stato di ‘unavailable’ (monitoro le volte di reaload con un counter.retry_count, ultimamente sono sempre zero o max. 1 reload; forse l’integrazione è stata aggiornata e funziona meglio).
Ciao Daniele, grazie mille per le parole gentili 😊
I controlli possono essere effettuati a cascata senza problemi. Se mentre il mio script cerca di spegnere senza riuscirci un dispositivo e questo si spegne di sua spontanea volontà e poi torna online ad un certo punto dei retry il mio script lo troverà spento uscendo come “tutto ok, missione compiuta” 😉
Il tuo termoarredo sicuramente ha integrato un termostato e quindi in caso resti acceso il rischio è di consumare tanta corrente, non di compromettere la sicurezza di casa (ho fatto delle assunzioni senza conoscere bene la situazione, spero di aver compreso bene la situazione)
Che brutto il comportamento dell’integrazione BTicino di cui parli caspita! Hai fatto bene, come workaround, di ricaricare l’integrazione, se questo riporta online il suo stato.
Speriamo abbiano risolto a monte il problema cmq. Se quel che fai con il termostato è usare un servizio turn_on o turn_off di un entità switch puoi usare questo script. Se invece agisci su un’entità climate allora no.
Dimenticavo, impagabile davvero la spiegazione, anzi direi la lezione, dello script, merita una lode.
Difficoltà: purtroppo utilizzo da più tempo del tuo Multinotify il pacchetto di notifiche di hassiohelp.eu direi con soddisfazione, e le automazioni sono implementate tutte con questo servizio, sono implementabili entrambi o entrano in conflitto ? Il tuo pacchetto non dipende dall’ add-on AppDeamon se ho capito bene?
Per Multinotify vs pacchetto notifiche non ti preoccupare, non sono geloso 🤣 So che è valido il loro pacchetto. È molto diverso e si sovrappone al mio solo parzialmente. Ci sono cose del loro che il mio non fa e viceversa.
Usa quel che preferisci 😉
Multinotify non richiede appdaemon, lo puoi usare insieme al loro (non stravolge niente di HA, è un po’ come questo script, se non lo usi non cambia niente in HA).
Puoi anche pensare di usare il loro pacchetto oppure i servizi singoli di HA senza usare Multinotify riscrivendo solo lo script delle notifiche che contiene una sola azione che chiama Multinotify. In tal caso resterebbe solo la dipendenza allo script python set_state. Ma perdere Multinotify, se usi Alexa, sarebbe un peccato 😜