Sommaire
Cet article s’inscrit dans une série de tutos présentant comment développer en python sa propre intégration. Plus d’infos et liste des tutos sur : Développer pour Home Assistant - Introduction .
L’objectif de ce troisième tuto est d’enrichir fonctionnellement notre entité créée au tuto2 .
Prérequis
Avoir déroulé avec succès les deux premiers articles tuto1 et tuto2 . Vous devez donc avoir une entité avec un état qui est une mesure en secondes.
Les points abordés
Dans cet article, tu vas apprendre à :
- utiliser l’objet
hass
, - déclencher périodiquement la mise à jour d’une entité,
- mettre à jour l’état de l’entité,
- publier et recevoir des évènements,
- s’abonner aux changements d’une autre entité
- implémenter un service
On va couvrir l’ensemble des flux décrit dans Home Assistant Core Architecture ( ici ) :
L’objet hass
S'il y a bien un objet important dans le développement Home Assistant, c’est l’objet hass
. De type HomeAssistant
, on l’a déjà rencontré dans le tuto2 sans l’expliquer, on va le faire ici.
Cet objet est partout. Il représente l’instance Home Assistant sur laquelle l’entité est configurée et permet d’accéder à tous les objets manipulés par Home Assistant : les configurations, les états, les entités, les bus d’évènements, …
Comme il est indispensable, on va le mémoriser dès que possible, soit à la construction de l’entité et dans les attributs de l’entité. On utilisera une variable privée, et de ce fait commençant par un _
selon la règle de nommage Python. Pour cela, on définit un attribut self._hass
dans la fonction d’initialisation de l’entité. Puis, on stocke l’objet hass
dedans :
class TutoHacsElapsedSecondEntity(SensorEntity):
...
_hass: HomeAssistant
def __init__(
self,
hass: HomeAssistant, # pylint: disable=unused-argument
entry_infos, # pylint: disable=unused-argument
) -> None:
"""Initisalisation de notre entité"""
...
self._hass = hass
On peut utiliser cet objet pour :
- lire des informations : liste des domaines, des intégrations, accès à l’
entity registry
ou à ladevice registry
, accès à la configuration de Home Assistant (timezone, unité de mesure, etc), - écrire des informations. Il est fréquent de voir des intégrations qui sauvegarde leurs informations dans cet objet. Par exemple, l’intégration LocalTuya stocke tous ses devices dans
hass.data[DOMAIN][TUYA_DEVICES]
. Ça lui permet d’accéder à ses devices partout (puisque l’objethass
est partout). On ne va pas le faire dans ce tuto, mais sache que cela existe et que c’est fréquemment utilisé.
On verra dans le tuto5 une utilisation avancée de cet objet pour rechercher toutes des entités, même celles qui ne nous appartiennent pas.
Plus d’informations sur cet objet voir ici.
Déclencher périodiquement la mise à jour d’une entité
Pour ce faire, il faut faire générer par Home Assistant un évènement basé sur le temps, puis capter cet évènement. Lors de la réception de cet évènement, on incrémentera le compteur (exprimé en secondes) de l’entité.
Dans notre capteur, on a besoin d’une fonction spéciale qui est appelée par Home Assistant lorsque l’entité a été prise en compte. C’est la méthode async_added_to_hass
qui est définie dans la classe de base de toutes les entités et que l’on va surcharger pour ajouter notre comportement souhaité.
On marque cette méthode avec l’annotation @callback
pour signifier que l’on surcharge une méthode de la classe de base. Même si ce n’est pas indispensable, cela donne des indications au lecteur.
from datetime import timedelta
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.event import async_track_time_interval
class TutoHacsElapsedSecondEntity(SensorEntity):
...
@callback
async def async_added_to_hass(self):
"""Ce callback est appelé lorsque l'entité est ajoutée à HA """
# Arme le timer
timer_cancel = async_track_time_interval(
self._hass,
self.incremente_secondes, # la méthode qui sera appelée toutes les secondes
interval=timedelta(seconds=1),
)
# desarme le timer lors de la destruction de l'entité
self.async_on_remove(timer_cancel)
Le fonctionnement de la méthode async_added_to_hass
est le suivant :
- on appelle la fonction helper
async_track_time_interval
qui programme un timer périodique (intervalle égal 1 seconde dans l’exemple), - on donne à ce helper l’objet
hass
, la méthode de notre entité qui sera appelée à chaque échéance du timer et l’intervalle, - cet appel retourne une fonction qui doit être appelée pour stopper le timer,
- on passe cette fonction d’annulation à la méthode de la classe
Entity
nomméeasync_on_remove
qui appelle toutes les méthodes qu’on lui aura données lors de la destruction de l’entité. Si on ne le fait pas, le timer continuera de poster des évènements dans le vide.
Il ne nous reste plus qu’à créer la méthode que l’on veut appeler toutes les secondes :
class TutoHacsElapsedSecondEntity(SensorEntity):
...
@callback
async def incremente_secondes(self, _):
"""Cette méthode va être appelée toutes les secondes"""
_LOGGER.info("Appel de incremente_secondes à %s", datetime.now())
Testons pour voir si notre méthode est bien appelée toutes les secondes (Command
+ Shift
+ P
/ Taches
…) Si on regarde les logs, on constate bien ceci :
2023-04-10 21:20:06.027 INFO (MainThread) [custom_components.tuto_hacs.sensor] Appel de incremente_secondes à 2023-04-10 21:20:06.027870
2023-04-10 21:20:07.034 INFO (MainThread) [custom_components.tuto_hacs.sensor] Appel de incremente_secondes à 2023-04-10 21:20:07.034205 2023-04-10 21:20:08.038 INFO (MainThread) [custom_components.tuto_hacs.sensor] Appel de incremente_secondes à 2023-04-10 21:20:08.038764
2023-04-10 21:20:09.040 INFO (MainThread) [custom_components.tuto_hacs.sensor] Appel de incremente_secondes à 2023-04-10
21:20:09.040590 2023-04-10 21:20:10.043 INFO (MainThread) [custom_components.tuto_hacs.sensor] Appel de incremente_secondes à 2023-04-10 21:20:10.043220 ...
Notre méthode incremente_secondes
est bien appelée toutes les secondes.
_
indique qu’on ne veut pas tenir compte du 2nd argument. Normalement, l’évèvement est passé en 2nd argument mais comme on ne s’en sert pas ici, on remplace le deuxième argument par _
.Mettre à jour l’état de l’entité
Cela se fait en appelant la méthode async_write_ha_state
définie dans la classe de base Entity
. On va remplacer notre méthode incremente_secondes
par celle-ci :
@callback
async def incremente_secondes(self, _):
"""Cette méthode va être appelée toutes les secondes"""
_LOGGER.info("Appel de incremente_secondes à %s", datetime.now())
# On incrémente la valeur de notre etat
self._attr_native_value += 1
# On sauvegarde le nouvel état
self.async_write_ha_state()
On en profite pour initialiser la valeur du compteur à 0 (et non pas 12) dans la méthode __init__
, afin de démarrer à zéro.
On redémarre, on voit toujours les logs “bouger” toutes les secondes et si on regarde sur le web ( http://localhost:9123/lovelace/0 ), on voit bien notre compteur évoluer toutes les secondes :
Publier et recevoir des évènements
Le cœur de Home Assistant est basé sur un bus d’évènements sur lequel on peut publier ou s’abonner (publish / subscribe). Il est fondamental de savoir s’y interfacer puisque c’est par là que vont passer toutes communications entre les différents composants de Home Assistant.
Publication d’un évènement
On va modifier notre méthode incremente_secondes
de notre entité vedette pour envoyer un évènement toutes les 5 secondes :
@callback
async def incremente_secondes(self, _):
"""Cette méthode va être appelée toutes les secondes"""
_LOGGER.info("Appel de incremente_secondes à %s", datetime.now())
# On incrémente la valeur de notre etat
self._attr_native_value += 1
# On sauvegarde le nouvel état
self.async_write_ha_state()
# Toutes les 5 secondes on envoie un event
if self._attr_native_value % 5 == 0:
self._hass.bus.fire(
"event_changement_etat_TutoHacsElapsedSecondEntity",
{"nb_secondes": self._attr_native_value},
)
Ça tient en une ligne : self._hass.bus.fire
qui prend en argument : le type d’évènement et un JSON qui contient des infos sur l’événement.
On arrête et on relance Home Assistant. Si on contrôle dans le web ou dans “Outils de développement / Événement” et qu’on s’abonne à l’évènement event_changement_etat_TutoHacsElapsedSecondEntity
, on constate cela :
Toutes les 5 secondes, on a bien un évènement généré qui contient dans ses data, l’attribut nb_secondes
qui s’incrémente bien de 5 en 5.
💡 Si on s’abonne aux évènements de typestate_changed
on voit que toutes les secondes, notre changement d’état fait l’objet d’un évènement. La ligneself.async_write_ha_state()
génère un évènement de typestate_changed
qui contient les informations suivantes :
event_type: state_changed
data:
entity_id: sensor.tuto_hacs_entite_3
old_state:
entity_id: sensor.tuto_hacs_entite_3
state: "639"
attributes:
state_class: measurement
unit_of_measurement: s
device_class: duration
icon: mdi:timer-play
friendly_name: Tuto HACS Entité
last_changed: "2023-04-11T17:13:57.377736+00:00"
last_updated: "2023-04-11T17:13:57.377736+00:00"
context:
id: 01GXRMYNC17VXC1H17M771R38E
parent_id: null
user_id: null
new_state:
entity_id: sensor.tuto_hacs_entite_3
state: "640"
attributes:
state_class: measurement
unit_of_measurement: s
device_class: duration
icon: mdi:timer-play
friendly_name: Tuto HACS Entité
last_changed: "2023-04-11T17:13:58.383054+00:00"
last_updated: "2023-04-11T17:13:58.383054+00:00"
context:
id: 01GXRMYPBF28WR4T481B5K0QQ9
parent_id: null
user_id: null
origin: LOCAL
time_fired: "2023-04-11T17:13:58.383054+00:00"
context:
id: 01GXRMYPBF28WR4T481B5K0QQ9
parent_id: null
user_id: null
on va pouvoir donc très facilement récupérer les changements d’état des entités : il suffira de s’abonner aux évènements state_changed
.
Réception des évènements
On va créer une deuxième entité qui va écouter les évènements de la première et va stocker dans son état la date du dernier évènement reçu (ça ne sert à rien, mais pourquoi pas après tout).
Si tu es motivé, tu peux le faire sous la forme d’un exercice. A part la réception d’un évènement, tout le reste à déjà été vu dans le tuto2
Voici ce qu’il faut faire :
- ajouter une classe pour notre deuxième entité (dans
sensor.py
) et se mettre en écoute des évènements de la première entité, - faire en sorte qu’elle soit instanciée au setup de la plate-forme (dans
async_setup_platform
desensor.py
), - interpréter les évènements reçus.
Ajouter une classe pour notre 2ᵉ capteur
sensor.py
:
...
class TutoHacsElapsedSecondEntity(SensorEntity):
...
class TutoHacsListenEntity(SensorEntity):
"""La classe de l'entité TutoHacs qui écoute la première"""
_hass: HomeAssistant
# On va stocker dans cet attribut l'instance de l'entité à écouter
_entity_to_listen: TutoHacsElapsedSecondEntity
def __init__(
self,
hass: HomeAssistant, # pylint: disable=unused-argument
entry_infos, # pylint: disable=unused-argument
entity_to_listen: TutoHacsElapsedSecondEntity, # L'entité qu'on veut écouter
) -> None:
"""Initisalisation de notre entité"""
self._hass = hass
self._attr_has_entity_name = True
self._device_id = entry_infos.get(CONF_DEVICE_ID)
# On lui donne un nom et un unique_id différent
self._attr_name = entry_infos.get(CONF_NAME) + " Ecouteur"
self._attr_unique_id = self._device_id + "_ecouteur"
# Pas de valeur tant qu'on n'a pas reçu
self._attr_native_value = None
self._entity_to_listen = entity_to_listen
@property
def should_poll(self) -> bool:
"""Pas de polling pour mettre à jour l'état"""
return False
@property
def icon(self) -> str | None:
return "mdi:timer-settings-outline"
@property
def device_class(self) -> SensorDeviceClass | None:
"""Cette entité"""
return SensorDeviceClass.TIMESTAMP
@callback
async def async_added_to_hass(self):
"""Ce callback est appelé lorsque l'entité est ajoutée à HA"""
# Arme l'écoute de la première entité
listener_cancel = async_track_state_change_event(
self.hass,
[self._entity_to_listen.entity_id],
self._on_event,
)
# desarme le timer lors de la destruction de l'entité
self.async_on_remove(listener_cancel)
Ça ressemble beaucoup à la classe créée dans le tuto2 , mais il y a quelques subtilités :
- Dans
__init__
il faut lui donner unname
(resp.unique_id
) qui sera unique. Pour cela, on concatèneEcouteur
(resp._ecouteur
) auname
(resp.unique_id
) - la device class est positionnée à
TIMESTAMP
et non pasDURATION
car notre entité représente une date absolue et pas une durée, - il n’y a pas de
state_class
ni denative_unit_of_mesurement
puisque l’état de notre entité n’est pas une mesure à proprement parler, - dans la méthode
async_added_to_hass
(qui est appelé par HA quand l’entité est ajoutée), on utilise le Helperasync_track_state_change_event
qui permet de se mettre en écoute des changements d’état dont l’entity_id
est donné en 2d paramètre (dans un tableau, car on peut en écouter plusieurs). C’est ici que se passe le lien entre les 2 entités : celle qui émet des changements d’état et notre deuxième qui les écoute, - comme vu dans le tuto2 , lorsqu’on se met en écoute d’un évènement, il faut se désabonner lorsque l’entité est supprimée. Sinon, on continue de recevoir les évents alors que l’entité a été supprimée de HA. Ça se fait avec l’appel à
async_on_remove
qui prend en paramètre le retour deasync_track_state_change_event
. La méthode appelée à chaque changement d’état reçu sera_on_event
qu’on verra ci-dessous.
Instancier cette classe au démarrage de la plate-forme
Pour cela, il faut modifier légèrement la fonction async_setup_platform
(cf. tuto2 au besoin) et ajouter le code suivant :
async def async_setup_platform(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
discovery_info=None, # pylint: disable=unused-argument
):
"""Configuration de la plate-forme tuto_hacs à partir de la configuration
trouvée dans configuration.yaml"""
_LOGGER.debug("Calling async_setup_entry entry=%s", entry)
entity1 = TutoHacsElapsedSecondEntity(hass, entry)
entity2 = TutoHacsListenEntity(hass, entry, entity1)
async_add_entities([entity1, entity2], True)
Tu remarques qu’on passe à notre deuxième classe la première entité créée. C’est utilisé dans async_added_to_hass
pour se mettre en écoute de ses changements d’état.
Interpréter les évènements reçus
Cela va se faire dans la méthode _on_event
qu’il faut ajouter à notre 2d classe. Le code ressemble à ça :
@callback
async def _on_event(self, event: Event):
"""Cette méthode va être appelée à chaque fois que l'entité
"entity_to_listen" publie un changement d'état"""
_LOGGER.info("Appel de _on_event à %s avec l'event %s", datetime.now(), event)
new_state: State = event.data.get("new_state")
# old_state: State = event.data.get("old_state")
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
_LOGGER.warning("Pas d'état disponible. Evenement ignoré")
return
# On recherche la date de l'event pour la stocker dans notre état
self._attr_native_value = new_state.last_changed
# On sauvegarde le nouvel état
self.async_write_ha_state()
Ce code déroule les étapes suivantes :
- Il reçoit un
event
du typeEvent
en argument.Event
une core classe qu’il est important de connaitre. UnEvent
contient l’entity_id
de l’émetteur, un champdata
de typedictionnary
qui contient 2 attributs :new_state
etold_state
avec respectivement le nouvel état et l’ancien état de l’entité, - on commence par récupérer son ancien et nouvel état :
new_state: State = event.data.get("new_state")
. J’ai mis en commentaire le code qui permet de récupérer l’ancien état (non utilisé ici) - l’objet
new_state
est de typeState
qui est aussi une core classe à connaitre. Il contient : l’état dans le champstate
, lesstate_class
,unit_of_mesurement
,device_class
de cet état, et 2 “timestamp”last_changed
(l’horodatage du changement d’état) etlast_updated
(l’horodatage de la dernière mise à jour). Ces 2 horodatages peuvent être différents dans le cas d’une entité avec “polling”. Dans ce cas, la date de remontée de l’état (date du poll) n’est pas forcément la date du changement d’état. Si on poll toutes les minutes, il peut y avoir jusqu’à une minute d’écart entre le changement d’état effectif et la date de sa remontée. Dans notre cas et dans la plupart des cas, on préfère utiliser la date du changement d’état effectif et donclast_changed.
- On vérifie que l’état est bien positionné avec cette ligne :
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN)
. Il faut qu’il y ait unnew_state
et que lestate
de cenew_state
ne soit pas ‘Unavailable" ni “Unknown”. Si c’est le cas, on arrête ici et on ne met pas à jour notre état, - s’il y a bien un état valide, on met à jour notre propre état avec l’horodatage du dernier changement :
self._attr_native_value = new_state.last_changed
- on sauvegarde notre nouvel état avec
self.async_write_ha_state()
Il faudra ajouter les quelques imports suivants pour que cela fonctionne (en début de fichier) :
from datetime import datetime, timedelta
from homeassistant.const import UnitOfTime, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, callback, Event, State
💡 Tu remarques que je type mes variables ou attributs de classe quasi systématiquement. Cela se fait avec le : State
par exemple. Ce n’est pas obligatoire avec Python qui est un langage interprété et non typé par défaut, mais ça a plusieurs vertues de le faire quand même :
- VSC va vous signaler tout de suite une erreur si les types ne correspondent pas,
- En passant la souris sur le type tu vas avoir la description de la classe,
- en maintenant ‘Command (sur Mac)’ enfoncée, tu vas pouvoir ouvrir le code de la classe et éventuellement mettre des points d’arrêt dans cette classe, même si c’est une classe système ou Home Assistant,
- tu avoir accès à l’auto-complétion. Dès que tu vas taper le ‘.’ après ton attribut, la liste des méthodes et attributs utilisables s’affichent avec leur arguments.
Bref, je le conseille vivement ce typage, car il simplifie beaucoup la phase de développement et facilite la relecture du code.
Démarrage de Home Assistant
Vérifies qu’il n’y a pas d’erreur :
Relance Home Assistant (Command
+ Shift
+ P
) et regarde les logs. Tu dois voir quelque-chose comme ça :
-04-15 08:10:36.889 INFO (MainThread) [custom_components.tuto_hacs.sensor] Appel de incremente_secondes à 2023-04-15 08:10:36.889856 2023-04-15 08:10:36.896 INFO (MainThread) [custom_components.tuto_hacs.sensor] Appel de _on_event à 2023-04-15 08:10:36.896349 avec l'event <Event state_changed[L]: entity_id=sensor.tuto_hacs_entite_3, old_state=<state sensor.tuto_hacs_entite_3=2243; state_class=measurement, unit_of_measurement=s, device_class=duration, icon=mdi:timer-play, friendly_name=Tuto HACS Entité @ 2023-04-15T10:10:35.889760+02:00>, new_state=<state sensor.tuto_hacs_entite_3=2244; state_class=measurement, unit_of_measurement=s, device_class=duration, icon=mdi:timer-play, friendly_name=Tuto HACS Entité @ 2023-04-15T10:10:36.890148+02:00>>
Ce log se répète toutes les secondes, puisque l’état de la première entité se met à jour toutes les secondes.
Si on regarde dans l’Outil de développement / État" ( http://localhost:9123/developer-tools/state ) de notre interface web HA et que l’on cherche “ecouteur”, on voit bien notre deuxième entité avec comme état l’horodatage qui change toutes les secondes :
Le dashboard aperçu (ici) affiche aussi nos 2 entités :
Implémenter un service
Un service est un point d’accès à notre intégration appelable depuis l’extérieur (une autre intégration, une automatisation, etc).
Créer un service se fait très simplement avec les étapes suivantes :
- déclaration de notre service dans un fichier de description
services.yaml
, - enregistrement du service au setup de la plate-forme,
- implémentation du service à proprement parler.
On va implémenter un service qui permet de remettre à zéro notre compteur pour l’exemple.
Déclaration du service
Home Assistant découvre les services exposés par les intégrations grâce au fichier services.yaml
présent à la racine de l’intégration. Pour notre exemple, il va ressembler à cela :
raz_compteur:
name: Raz compteur
description: Remet à zéro le compteur de temps et optionnellement donne la valeur de départ
target:
entity:
integration: tuto_hacs
fields:
valeur_depart:
name: Valeur départ
description: La valeur de départ du compteur
required: false
advanced: false
example: "10"
default: "0"
selector:
number:
min: 0
max: 900
step: 1
mode: slider
Ce fichier contient :
- la description d’un service nommé
raz_compteur
, avec un nom et une description pour les utilisateurs à travers “Outil de développement / Service”, - la cible
target
. On indique ici que toutes les entités de l’intégrationtuto_hacs
peuvent être ciblées,
une description des paramètres du service dans la structure fields
. On a un seul paramètre :
- le nom du paramètre :
valeur_depart
avec un nom pour l’utilisateur et une description, - le caractère obligatoire ou pas du paramètre (ici, il est facultatif),
- est-ce qu’il apparait seulement en mode ‘Avancé’ ?. Ici non puisqu’on veut le voir tout le temps,
- un exemple de valeur,
- la valeur par défaut,
- et un
selector
qui permet à l’utilisateur de choisir la valeur qu’il veut. Ici, on utilise un selector de typenumber
avec une valeur minimale de 0, une valeur maximale de 900, un pas de 1 et l’utilisateur pourra choisir la valeur sur un “slider”. On verra le rendu un peu en dessous.
Home Assistant propose un nombre de sélecteurs très impressionnants et vraiment bien foutus. Tu trouveras la liste ici.
Enregistrement du service au setup
Lors du setup de notre intégration, on doit enregistrer notre service et dire quelle méthode doit être appelée lorsque le service est invoqué. Ça se fait dans la fonction async_setup_platform
à l’aide du code suivant :
async def async_setup_platform(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
discovery_info=None, # pylint: disable=unused-argument
):
...
# Add services
platform = async_get_current_platform()
platform.async_register_entity_service(
SERVICE_RAZ_COMPTEUR,
{vol.Optional("valeur_depart"): cv.positive_int},
"service_raz_compteur",
)
- on récupère notre plate-forme courante avec :
async_get_current_platform()
, - on enregistre notre service avec l’appel à :
platform.async_register_entity_service
. Cet appel prend 3 paramètres :
- Le nom du service que l’on a mis dans la constante
SERVICE_RAZ_COMPTEUR
définie dans notreconst.py
. Elle doit être égale au nom du service dans leservices.yaml
à savoir :raz_compteur
dans l’exemple, - une structure
Voluptuous
qui en charge de vérifier les paramètres donnés lors de l’appel du service. Voir ci-dessous, - le nom de la méthode sur notre classe d’entité qui sera appelée lorsque le service est invoqué. Ici, on appellera la méthode :
TutoHacsElapsedSecondEntity.service_raz_compteur
.
On va avoir besoin des imports suivants :
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import (
...
async_get_current_platform,
)
from .const import (
...
SERVICE_RAZ_COMPTEUR,
)
et on va définir notre constante SERVICE_RAZ_COMPTEUR
dans notre const.py
:
...
SERVICE_RAZ_COMPTEUR = "raz_compteur"
Voluptuous
Cette partie est complexe et sera abordée beaucoup plus en détail avec le tuto4 . Pour l’instant, on va juste donner une structure qui liste les paramètres valeur_depart
, donne son caractère facultatif (vol.Optional
) et indique qu’on attend un entier positif (cv.positive_int
).
C’est une des parties les moins bien documentée à la fois dans Home Assistant, mais aussi dans le package voluptuous
lui-même donc je ne rentre pas plus dans le détail dans ce tuto.
Pour les curieux, la seule doc à peu près potable est ici .
Redémarrage de Home Assistant
Tu commences à en avoir l’habitude maintenant, mais je le répète encore :
- on vérifie qu’on n’a pas d’erreur dans l’onglet “PROBLEMES” de VSC,
- on (re)démarre avec ‘Command + Shift + P’,
- on ne doit pas voir d’erreur les logs, seulement notre compteur qui tourne toutes les secondes.
Vas ensuite dans les “Outils de développement / Services” ( ici ) et tape ’tuto’ dans la boite de recherche des services. Tu dois voir notre service :
Sélectionne-le et tu vas voir apparaitre l’interface qui permet de configurer l’appel du service :
Tu peux :
- sélectionner des entités et tu constates qu’il n’y a bien que les entités de notre intégration qui sont présentées,
- modifier la “Valeur départ” avec un slider ou directement en tapant la valeur,
- ne pas passer de valeur de départ en la décochant. C’est dû au caractère facultatif de notre paramètre “valeur_depart”.
L’appel du service provoque une erreur de type :
AttributeError: 'TutoHacsElapsedSecondEntity' object has no attribute 'service_raz_compteur'
Effectivement, notre classe TutoHacsElapsedSecondEntity
n’a pas encore de méthode service_raz_compteur
. On va y remédier tout de suite.
On voit que la structure est en place, le service est bien déclaré et pris en compte par Home Assistant.
Implémentation du service
Pour cela, c’est très simple, il suffit d’ajouter une méthode service_raz_compteur
à notre classe TutoHacsElapsedSecondEntity
:
async def service_raz_compteur(self, valeur_depart: int):
"""Appelée lors de l'invocation du service 'raz_compteur'
Elle prend en argument la 'valeur_depart' qui est
construite à partir du paramètre 'valeur_depart'
"""
_LOGGER.info(
"Appel du service service_raz_compteur valeur_depart: %d", valeur_depart
)
self._attr_native_value = valeur_depart if valeur_depart is not None else 0
# On sauvegarde le nouvel état
self.async_write_ha_state()
- Notre méthode est appelée avec
valeur_depart
en argument typé enint
dans l’exemple. - On loggue l’appel (toujours utile en cas de debug).
- On affecte notre état avec la valeur passée ou avec 0 si elle est absente (
None
). Python permet de faire tout ça en une seule ligne avec la forme_attr_native_value = valeur_depart if valeur_depart is not None else 0
qui se lit très bien. - On sauvegarde notre nouvel état avec
self.async_write_ha_state()
Redémarre Home Assistant et vérifie que cette fois l’appel du service se passe bien. Tu dois avoir une coche verte.
Si tu regardes le nouvel état de ton entité (http://localhost:9123/developer-tools/state ou http://localhost:9123/lovelace/0), tu dois constater le redémarrage du compteur à la valeur spécifiée.
Pour éviter ça :
- on peut implémenter le service dans la classe
TutoHacsListenEntity
mais cela fera un appel qui ne sert à rien, - ou alors limiter les entités ciblées dans le
target
de notreservices.yaml
. Pour cela, on peut utiliser le paramètredevice_class
du selector àduration
puisque seule la première classe à cettedevice_class
. On a alors une configurationtarget
qui ressemble à ça :
target:
entity:
integration: tuto_hacs
device_class: duration
Intégrer notre entité dans l’écosystème Home Assistant
Les services sont très utiles pour intégrer notre intégration dans l’écosystème Home Assistant. Grâce à eux, on va pouvoir faire une automatisation qui fait un RAZ du compteur sur un évènement particulier, ou alors intégrer le raz dans un script.
Pour cela, il faut ajouter le yaml suivant (donné par “Outils de développement / Services / Passez en mode YAML”) :
service: tuto_hacs.raz_compteur
data:
valeur_depart: 90
target:
entity_id: sensor.tuto_hacs_entite_3
On peut aussi utiliser notre entité comme trigger des automatisations.
Voici un exemple complet :
alias: Raz le compteur après une minute
description: Remet à zéro le compteur lorsqu'il dépasse 60 (donc toutes les minutes)
trigger:
- platform: numeric_state
entity_id: sensor.tuto_hacs_entite_3
above: 60
condition: []
action:
- service: tuto_hacs.raz_compteur
data:
valeur_depart: "0"
target:
entity_id: sensor.tuto_hacs_entite_3
mode: single
Cette automatisation se déclenche lorsque la valeur du compteur est supérieure à 60 et remet à zéro le compteur si c’est le cas via l’utilisation du service.
Vérifies que cela fonctionne bien en accédant http://localhost:9123/lovelace/0
Conclusion
Ce tuto t’a permis d’apprendre à créer des entités qui interagissent avec l’extérieur en publiant des états, écoutant les états des autres entités et en publiant des services utilisables par les automatisations et les scripts.
Il est impossible d’être exhaustif tellement l’écosystème Home Assistant est riche. Pour découvrir d’autres façons d’interagir c’est le moment de faire un tour dans la documentation de référence et notamment dans les articles suivants :
- Listening for events
- Integration Manifes t
- Integration Configuration via YAML
- Integration Services
- Config
💡 Dans le prochain tuto4 , on va apprendre à configurer notre intégration à travers l’interface de Home Assistant et non plus à travers le fichier configuration.yaml
.
Listes des fichiers références de ce tuto
Note : ne sont présents que les fichiers modifiés par rapport au tuto précédent.
const.py
""" Les constantes pour l'intégration Tuto HACS """
from homeassistant.const import Platform
DOMAIN = "tuto_hacs"
PLATFORMS: list[Platform] = [Platform.SENSOR]
CONF_NAME = "name"
CONF_DEVICE_ID = "device_id"
DEVICE_MANUFACTURER = "JMCOLLIN"
SERVICE_RAZ_COMPTEUR = "raz_compteur"
sensor.py
""" Implements the VersatileThermostat sensors component """
import logging
from datetime import datetime, timedelta
import voluptuous as vol
from homeassistant.const import UnitOfTime, STATE_UNAVAILABLE, STATE_UNKNOWN
from homeassistant.core import HomeAssistant, callback, Event, State
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.entity_platform import (
AddEntitiesCallback,
async_get_current_platform,
)
from homeassistant.components.sensor import (
SensorEntity,
SensorDeviceClass,
SensorStateClass,
)
from homeassistant.helpers.event import (
async_track_time_interval,
async_track_state_change_event,
)
from homeassistant.helpers.entity import DeviceInfo, DeviceEntryType
import homeassistant.helpers.config_validation as cv
from .const import (
DOMAIN,
DEVICE_MANUFACTURER,
CONF_DEVICE_ID,
CONF_NAME,
SERVICE_RAZ_COMPTEUR,
)
_LOGGER = logging.getLogger(__name__)
async def async_setup_platform(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
discovery_info=None, # pylint: disable=unused-argument
):
"""Configuration de la plate-forme tuto_hacs à partir de la configuration
trouvée dans configuration.yaml"""
_LOGGER.debug("Calling async_setup_entry entry=%s", entry)
entity1 = TutoHacsElapsedSecondEntity(hass, entry)
entity2 = TutoHacsListenEntity(hass, entry, entity1)
async_add_entities([entity1, entity2], True)
# Add services
platform = async_get_current_platform()
platform.async_register_entity_service(
SERVICE_RAZ_COMPTEUR,
{vol.Optional("valeur_depart"): cv.positive_int},
"service_raz_compteur",
)
class TutoHacsElapsedSecondEntity(SensorEntity):
"""La classe de l'entité TutoHacs"""
_hass: HomeAssistant
def __init__(
self,
hass: HomeAssistant, # pylint: disable=unused-argument
entry_infos, # pylint: disable=unused-argument
) -> None:
"""Initisalisation de notre entité"""
self._hass = hass
self._attr_has_entity_name = True
self._attr_name = entry_infos.get(CONF_NAME)
self._device_id = entry_infos.get(CONF_DEVICE_ID)
self._attr_unique_id = self._device_id + "_seconds"
self._attr_native_value = 12
@property
def should_poll(self) -> bool:
"""Do not poll for those entities"""
return False
@property
def icon(self) -> str | None:
return "mdi:timer-play"
@property
def device_class(self) -> SensorDeviceClass | None:
return SensorDeviceClass.DURATION
@property
def state_class(self) -> SensorStateClass | None:
return SensorStateClass.MEASUREMENT
@property
def native_unit_of_measurement(self) -> str | None:
return UnitOfTime.SECONDS
@callback
async def async_added_to_hass(self):
"""Ce callback est appelé lorsque l'entité est ajoutée à HA"""
# Arme le timer
timer_cancel = async_track_time_interval(
self._hass,
self._incremente_secondes, # La methode à appeler périodiquement
interval=timedelta(seconds=1),
)
# desarme le timer lors de la destruction de l'entité
self.async_on_remove(timer_cancel)
@callback
async def _incremente_secondes(self, _):
"""Cette méthode va être appelée toutes les secondes"""
_LOGGER.info("Appel de incremente_secondes à %s", datetime.now())
# On incrémente la valeur de notre etat
self._attr_native_value += 1
# On sauvegarde le nouvel état
self.async_write_ha_state()
# Toutes les 5 secondes on envoie un event
if self._attr_native_value % 5 == 0:
self._hass.bus.fire(
"event_changement_etat_TutoHacsElapsedSecondEnity",
{"nb_secondes": self._attr_native_value},
)
async def service_raz_compteur(self, valeur_depart: int):
"""Appelée lors de l'invocation du service 'raz_compteur'
Elle prend en argument la 'valeur_depart' qui est
construite à partir du paramètre 'valeur_depart'
"""
_LOGGER.info(
"Appel du service service_raz_compteur valeur_depart: %d", valeur_depart
)
self._attr_native_value = valeur_depart if valeur_depart is not None else 0
# On sauvegarde le nouvel état
self.async_write_ha_state()
class TutoHacsListenEntity(SensorEntity):
"""La classe de l'entité TutoHacs qui écoute la première"""
_hass: HomeAssistant
_entity_to_listen: TutoHacsElapsedSecondEntity
def __init__(
self,
hass: HomeAssistant, # pylint: disable=unused-argument
entry_infos, # pylint: disable=unused-argument
entity_to_listen: TutoHacsElapsedSecondEntity, # L'entité qu'on veut écouter
) -> None:
"""Initisalisation de notre entité"""
self._hass = hass
self._attr_has_entity_name = True
self._device_id = entry_infos.get(CONF_DEVICE_ID)
# On lui donne un nom et un unique_id différent
self._attr_name = entry_infos.get(CONF_NAME) + " Ecouteur"
self._attr_unique_id = self._device_id + "_ecouteur"
# Pas de valeur tant qu'on n'a pas reçu
self._attr_native_value = None
self._entity_to_listen = entity_to_listen
@property
def should_poll(self) -> bool:
"""Pas de polling pour mettre à jour l'état"""
return False
@property
def icon(self) -> str | None:
return "mdi:timer-settings-outline"
@property
def device_class(self) -> SensorDeviceClass | None:
"""Cette entité"""
return SensorDeviceClass.TIMESTAMP
@callback
async def async_added_to_hass(self):
"""Ce callback est appelé lorsque l'entité est ajoutée à HA"""
# Arme l'écoute de la première entité
listener_cancel = async_track_state_change_event(
self.hass,
[self._entity_to_listen.entity_id],
self._on_event,
)
# desarme le timer lors de la destruction de l'entité
self.async_on_remove(listener_cancel)
@callback
async def _on_event(self, event: Event):
"""Cette méthode va être appelée à chaque fois que l'entité
"entity_to_listen" publie un changement d'état"""
_LOGGER.info("Appel de _on_event à %s avec l'event %s", datetime.now(), event)
new_state: State = event.data.get("new_state")
# old_state: State = event.data.get("old_state")
if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN):
_LOGGER.warning("Pas d'état disponible. Evenement ignoré")
return
# state.last_changed.astimezone(self._current_tz)
# On recherche la date de l'event pour la stocker dans notre état
self._attr_native_value = new_state.last_changed
# On sauvegarde le nouvel état
self.async_write_ha_state()
services.yaml
raz_compteur:
name: Raz compteur
description: Remet à zéro le compteur de temps et optionnellement donne la valeur de départ
target:
entity:
integration: tuto_hacs
device_class: duration
fields:
valeur_depart:
name: Valeur départ
description: La valeur de départ du compteur
required: false
advanced: false
example: "10"
default: "0"
selector:
number:
min: 0
max: 900
step: 1
mode: slider
automations.yaml
- id: '1681554365445'
alias: Raz le compteur après une minute
description: Remet à zéro le compteur lorsqu'il dépasse 60 (donc toutes les minutes)
trigger:
- platform: numeric_state
entity_id: sensor.tuto_hacs_entite_3
above: 60
condition: []
action:
- service: tuto_hacs.raz_compteur
data:
valeur_depart: '0'
target:
entity_id: sensor.tuto_hacs_entite_3
mode: single