Home Assistant & IP's

Suite à une configuration un peu courte de la plage de mon serveur DHCP, j'ai eu des modules Shelly (et d'autres) qui ont un peu perdu les pédales... En IT je monitore avec des outils IP comme PRTG, mais l'idée m'est venue de monitorer les modules domotique dans Home Assistant.

Pour ça il y a l'intégration ping, une intégration qui doit dater des débuts de Home Assistant et qui ne remonte pas l'IP dans les attributs. Et on verra plus loin que ce serait bien !

La première solution

On commence donc par créer un fichier ping.yaml dans nos packages et d'ajouter nos sensors :

binary_sensor:
  - platform: ping
    host: 192.168.210.63
    name: "Ping : Shelly Plug S01 | Sonos SdB"
    count: 3
    scan_interval: 30
  - platform: ping
    host: 192.168.210.78
    name: "Ping : Shelly Plug S02 | Sonos Terrasse"
    count: 3
    scan_interval: 30
  - platform: ping
    host: 192.168.210.104
    name: "Ping : Shelly Plug S03 | Congellateurs"
    count: 3
    scan_interval: 30

Ensuite on crée un groupe, et comme j'en ai beaucoup j'ai cherché quelque chose pour créer un groupe en mode willcard, genre binary_sensor.ping_* . Mais il n'existe pas grand chose et j'ai juste trouvé Groups 2.0 sous AppDaemon (mais vous n'allez pas l'installer juste pour ça !). Enfin voici le code à ajouter pour ceux qui sont habitués à la chose :

###########################################################################################
#                                                                                         #
#  Rene Tode ( [email protected] )                                                            #
#  2017/11/29 Germany                                                                     #
#                                                                                         #
#  wildcard groups                                                                        #
#                                                                                         #
#  arguments:                                                                             #
#  name: your_name                                                                        #
#  device_type: sensor # or any devicetype                                                #
#  entity_part: "any_part"                                                                #
#  entities: # list of entities                                                           #
#    - sensor.any_entity                                                                  #
#  hidden: False # or True                                                                #
#  view: True # or False                                                                  #
#  assumed_state: False # or True                                                         #
#  friendly_name: Your Friendly Name                                                      #
#  nested_view: True # or False                                                           #
#                                                                                         #
###########################################################################################

import appdaemon.plugins.hass.hassapi as hass
class create_group(hass.Hass):

  def initialize(self):
    all_entities = self.get_state(self.args["device_type"])
    entitylist = []
    for entity in all_entities:
      if self.args["entity_part"] in entity:
        entitylist.append(entity.lower())
    if "entities" in self.args:
      for entity in self.args["entities"]:
        entitylist.append(entity.lower())
    hidden = self.args["hidden"]
    view = self.args["view"]
    assumed_state = self.args["assumed_state"]
    friendly_name = self.args["friendly_name"]
    name = "group." + self.args["name"]    
    if not self.args["nested_view"]:
      self.set_state(name,state="on",attributes={"view": view,"hidden": hidden,"assumed_state": assumed_state,"friendly_name": friendly_name,"entity_id": entitylist})
    else:
      self.set_state(name + "2",state="on",attributes={"view": False,"hidden": hidden,"assumed_state": assumed_state,"friendly_name": friendly_name,"entity_id": entitylist})
      self.set_state(name,state="on",attributes={"view": True,"hidden": hidden,"assumed_state": assumed_state,"friendly_name": friendly_name,"entity_id": [name + "2"]})

Et ensuite dans apps.yaml pour créer le groupe :

pings_ip:
  module: groups
  class: create_group
  name: ping_ip
  device_type: binary_sensor
  entity_part: "ping_"
  entities:
    - sensor.group_ping
  hidden: False
  view: True
  assumed_state: False
  friendly_name: Ping IP4
  nested_view: False #creates a second group inside a viewed group  

A partir de là il est possible de créer une auto_entity_card pour visualiser nos sensors :

Quant au groupe il va nous servir à envoyer des messages afin de signaler les objets inactifs. Pour ça on va créer (Merci @mathieu...) un sensor :

    - platform: template
      sensors:
        offline_ping_sensors:
          friendly_name: 'Ping Offline'
          value_template: >
            {% set unavailable_count = states
                                    | selectattr('state','in', ['off', 'disconnected'])
                                    | selectattr('entity_id','in',state_attr('group.ping_ip','entity_id'))
                                    | map(attribute='entity_id')
                                    | list
                                    | length
            %}
            {{ unavailable_count }}
          attribute_templates:
            IP Fail: >
              {% set unavailable_list = states
                                | selectattr('state','in', ['off', 'disconnected'])
                                | selectattr('entity_id','in',state_attr('group.ping_ip','entity_id'))
                                | map(attribute='entity_id')
                                | list
                                | join('\n') 
                                | replace("binary_sensor.ping", "")
                                | replace("_", " ")
                                
              %}
              {{ unavailable_list }}

Et une automation qui sera déclenchée par ce sensor et va nous envoyer une notification :

- alias: 'IP'
  id: fc48c309-63c5-4965-bd87-fbcabc026983
  initial_state: true
  # mode: single
  trigger:
    - platform: state
      entity_id: sensor.offline_ping_sensors
  condition:
    - condition: template
      value_template: "{{ trigger.to_state.state != trigger.from_state.state }}"
  action:
    - delay: 00:01:00
    - service: notify.slack_hass_canaletto 
      data_template: 
        title: "IP Fail" 
        message: "{{now().strftime('%d/%m/%Y, %H:%M')}} > IP FAIL{{ state_attr('sensor.offline_ping_sensors','IP Fail') }} Count: {{ states('sensor.offline_ping_sensors') }}"

Le résultat est basic et se borne à nous envoyer la liste des modules injoignables, c'est intéressant mais ça ne mets pas en évidence un module qui tombe à un instant t :

De plus j'aimerait bien que les notifications soient cliquables afin d'ouvrir la page web des modules Shelly. J'ai donc remis @mathieu à contribution et apres une bonne nuit de sommeil on est partit sur une autre solution complémentaire.

Le plan B

On conserve nos sensors ping, mais on les renomme afin d'y intégrer le dernier digit de l'IP dans le nom, ce qui va nous donner. On est obligé d'écrire cette IP dans le nom car on ne la retrouve pas dans les attributs du sensor :

  - platform: ping
    host: 192.168.210.58
    name: "Ping 058 : Yeelink 01 | Desk"
    count: 3
    scan_interval: 30

On remarque que l'IP 58 devient 058 afin de conserver un alignement correct dans notre auto_entity_card et on va traiter ça dans l'automation ou vois apprécierait le template du message. Cette option nous impose de lister tous nos sensors dans l'automation, en attendant de trouver une façon de faire appel à un groupe ou le fichier des sensors :

- alias: 'Ping Offline'
  id: '08b1556d-d816-4879-b911-bd83213dd150'
  initial_state: true
  mode: single
  trigger:
    - platform: state
      entity_id:
        - binary_sensor.ping_127_shelly_plug_s09_udm
        - binary_sensor.ping_058_yeelight_01_desk
  condition:
    - condition: template
      value_template: "{{ trigger.to_state.state != trigger.from_state.state }}"
  action:
    - service: notify.slack_hass_canaletto 
      data_template: 
        title: "IP Fail" 
        message: '{{now().strftime("%d/%m/%Y, %H:%M")}} > {{ trigger.to_state.state|replace("on", "IP UP")|replace("off", "IP DOWN") }} : {{ trigger.to_state.attributes.friendly_name }} : {{ trigger.entity_id | replace("_0","",1) | replace("_1","1",1) | replace("_2","2",1) | replace("binary_sensor.ping", "http://192.168.210.") | replace("_"," ",1) | truncate(24, True,"") }}'

Au passage une astuce pour récupérer une liste d'entités, vous collez ça dans Outils de développement / Templates, ça vous évitera de vous coller la liste à la main :

{% for state in states %}
{{ state.entity_id }}
{%- endfor -%}

Et le résultat est maintenant uniquement sur le module défaillant, avec son url :

Il reste quelques petites choses d'ordre cosmétique, mais ça correspond maintenant à ce que j'attendais. J'envoie ces messages dans un fil Slack qui me sert de log dynamique que je regarde quand j'ai quelque chose en panne, en opposition aux notification par SMS (urgentes) ou via l'application.

Encore merci à Mathieu qui m'a bien aidé afin de mettre en code mes idées, et à la communauté afin d'améliorer tout ça !

Utiliser un tracker NMAP

Comme me l'a rappelé un utilisateur sur HACF Il y a une autre solution qui pourrait consister à utiliser l'intégration NMAP Tracker, qui, bien que pas faite pour présente l'avantage de remonter l'adresse IP en attribut. Pour faire propre il faudra renommer les trackers ainsi crées tant au niveau de l'entity_id que du friendly_name qui de base reprennent ce que retourne le DNS, qui n'est pas toujours très lisible.

Ca donne ça et il n'est pas utile de bricoler l'IP pour créer un lien car celle ci est disponible en attribut, ainsi que d'autres informations potentielement utiles.

- alias: 'IP Track Offline'
  id: '96017908-d46d-40cc-8d95-6b7997f5a411'
  initial_state: true
  mode: single
  trigger:
    - platform: state
      entity_id:
        - device_tracker.nmap_shelly_1_01
        - binary_sensor.numero_3
        - binary_sensor.numero_4
        - binary_sensor.numero_5
  condition:
    - condition: template
      value_template: "{{ trigger.to_state.state != trigger.from_state.state }}"
  action:
    - service: notify.slack_hass_canaletto 
      data_template: 
        title: "IP Fail" 
        message: '{{now().strftime("%d/%m/%Y, %H:%M")}} > {{ trigger.to_state.state|replace("home", "IP UP")|replace("not_home", "IP DOWN") }} : {{ trigger.to_state.attributes.friendly_name }} : http://{{ trigger.to_state.attributes.ip }}'

Pour ce résultat :

Alternatives

Il est bien sur également possible d'utiliser des outils de monitirig IP de l'IT. Au delà du monde de l'IT pro, j'en ai découvert un de très sympa, Uptime Kuma, et dont l'auteur travaille à une intégration avec Home Assistant. On y reviendra.

Pour surveiller des équipement ou sites sur internet il existe UpTime Robot qui dispose d'une intégration officielle dans HA qui remonte des binary_sensor. C'est très pratique car il y a tout ce qu'il faut pour créer des notification sur mesure.

Conclusion

Aucune de ces solutions n'est parfaite et je continue à chercher... Mais pour trouver une solution au problème de base, je pense qu'il vaudrait mieux surveiller la disponibilité des entités. En effet j'ai remarqué que parfois un Shelly peut être disponible en IP et injoignable en CoIoT. Cela permettrait également de surveiller des équipements non IP, par exemple Zigbee...

Toutes les idées sont bienvenues et avec plaisir pour échanger, ici dans les commentaires ou sur le forum HACF..