Home Assistant, boost ponctuel du chauffage

Ceux qui avaient lu mes articles précédents consacrés à Schedy ont du remarquer que j'avais introduit un mode BOOST qui permettait de débrayer la planification pendant n minutes et ainsi forcer le thermostat d'une pièce pensant x minutes à une température choisie. Dans la pratique on s'aperçoit que ce mode n'a d'intérêt que dans la salle de bain si l'on déroge aux horaires habituel. Et c'est particulièrement mon cas car je n'ai pas d'horaires figés et je peux très bien avoir envie de me doucher à 3 heures du mat avant de me coucher après avoir végété sur mon canapé face au dernier épisode de la série du moment...

Ma salle de bain est donc toujours en ECO et j'ai créé une automation basée sur un timer: qui va faire le déroulé dès lors qu'il sera lancé. Je démarre le timer directement avec un bouton de télécommande via ControllerX, mais il est également possible de configurer ça dans la liste des triggers. La durée du boost est contenue dans un input_nuber: 

aqara_button:
  module: controllerx
  class: WXKG11LMRemoteLightController
  controller: 00:15:8d:00:01:e7:d5:24
  integration: 
    name: deconz
    listen_to: unique_id
  light: light.off
  mapping:
    1002:
      service: timer.finish
      entity_id: timer.shower    
    1004:
      service: timer.start
      data:
        duration: "{{ states('input_number.sdb_boost_time') | int * 60 }}"
        entity_id: timer.shower

Ensuite la liste des courses...

timer:
  shower:
    name: Boost SdB

input_number:
  sdb_boost_good:
    name: Boost Good
    min: 20
    max: 25
    step: 0.5
    unit_of_measurement: °C
  sdb_boost_max:
    name: Boost Max
    min: 23
    max: 28
    step: 0.5
    unit_of_measurement: °C
  sdb_boost_time:
    name: Boost Time
    min: 15
    max: 90
    step: 5

Et ensuite l'automation avec un choose: avec des actions conditionnées par les id: des trigger:, avec pour objectif de :

  • Au démarrage du timer on passe le thermostat du sèche serviette sur BOOST et on allume le radiateur soufflant et on l'annonce.
  • Si le timer est démarré et que la température de confort est atteinte on l'annonce sur une enceinte Sonos. A la douche !
  • Si la durée du timer est terminée ou que la température maximum est atteinte on éteint le radiateur soufflant et on repasse le sèche serviette en mode ECO.
- id : 'c88f056d-8bbc-40ff-a044-a3b1b733e3c8'
  alias: "RC : Boost SdB"
  description: "Boost SdB by chooser"
  trigger:
    - platform: numeric_state
      entity_id: sensor.rpi_mi_t_sdb
      above: input_number.sdb_boost_good
      id: "t_good"
    - platform: numeric_state
      entity_id: sensor.rpi_mi_t_sdb
      above: input_number.sdb_boost_max
      id: "tmax"
    - platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.shower
      id: "timer.finished"
    - platform: event
      event_type: timer.started
      event_data:
        entity_id: timer.shower
      id: "timer.started"
  condition: []
  action:
    - choose:
        - conditions: # Condition n°1
            - condition: trigger
              id: "timer.started"
          sequence:
            - service: climate.set_preset_mode
              data:
                preset_mode: boost
              target:
                entity_id: climate.thermostat_salle_de_bain
            - service: switch.turn_on
              entity_id: switch.plug_bw_01
            - service: tts.cloud_say
              entity_id: media_player.sonos_cloud_hall
              data_template:
                message: "Préparation de la salle de bain. Je vous dirait quand vous pourrez vous doucher !"
                cache: 'false'
        - conditions: # Condition n°2
            - condition: and
              conditions:
                - condition: trigger
                  id: "timer.started"
                - condition: trigger
                  id: "t_good" # On annonce que la température de confort+ est atteinte
          sequence:
            - service: tts.cloud_say
              entity_id: media_player.sonos_cloud_hall
              data_template:
                message: "La température de la salle de bain est de {{states('sensor.rpi_mi_t_sdb')}}°, vous pouvez vous doucher !"
                cache: 'false'
        - conditions: # Condition n°3
            - condition: or
              conditions:
                - condition: trigger
                  id: "timer.finished" # On arrete de chauffage à la fin du timer
                - condition: trigger
                  id: "t_max"          # On arrete le chauffage si la température max est atteinte.
          sequence:
            - service: switch.turn_off
              entity_id: switch.plug_bw_01
            - service: climate.set_preset_mode
              data:
                preset_mode: eco
              target:
                entity_id: climate.thermostat_salle_de_bain
            - service: tts.cloud_say
              entity_id: media_player.sonos_cloud_hall
              data_template:
                message: "Fin du chauffage de la salle de bain. A bientôt !"
                cache: 'false'
      default:

Rien d'extraordinaire et tout ça reste à affiner selon les besoin de chacun. On peut également ajouter une condition à cette automation pour qu'elle ne s'exécute qu'en hiver en se basant que l'intégration Season. En même temps les saisons ça ne veut plus dire grand chose, donc dans la pratique il vaudra mieux avoir quelque part un input_boolean: qui globalement autorisera ou non les fonctions liés au chauffage. Pour l'anecdote (vécue) ça évitera, que comme la semaine dernière (fin mai), la condition 2 n'annonce à la femme de ménage qu'elle peut se doucher...

A noter que si comme moi vous comptez remplacer Schedy par une automation du genre de ce dont j'ai parlé ici, il faudra prévoir de débrayer l'automatisme en le passant en manuel dans la condition 1 et en automatique dans la condition 3.

- service: input_select.set_options
  data:
    options: Automatique
  target:
    entity_id: input_select.comfort_sdb

Voilà !

 

Home Assistant, nommage, Energy & SAV.

Sous Home Assistant le module Energy est très pratique, mais il est encore un peu jeune. L'objectif ici est de ne pas perdre la continuité des informations si on doit changer un module ou une prise.

Prenons le cas de mon lave linge. Il est branché sur une prise commandée, ce qui me permet de remonter sa consommation. Pas de chance, le relais de cette prise a lâchée et il me faut la changer. Et je ne veux pas perdre la continuité des informations de consommation du module Energy.

Le nommage

Quand on installe une prise ou un module on peut lui donner un nom personnalisé. Chacun sa méthode, mais idéalement, et avec du recul, je conseillerais de donner aux équipements un nom générique lié au matériel et non un nom lié à la fonction. Ainsi au lieu d'avoir switch.lave_linge je vais nommer ma prise Plug BW 03 et ainsi avoir switch.plug_bw_03, sensor.plug_bw_03_energy, etc. Dans certains cas il faudra renommer les entités manuellement mais cela permettra d'avoir une installation propre.

De plus je conseille de maintenir un tableau d'affection des équipements, inutile sur une toute petite installation, mais indispensable quand on a plusieurs douzaines d'équipements. On y renseignera les IP, MAC, Tokens, Key, etc...

Energy

Pour utiliser le module Energy je préconise de passer par des utility_meter:. Dans la pratique ce sont des compteurs. Cette intégration va enregistrer les cumuls de consommation et c'est celui ci que l'on va déclarer dans le module Energy. Avant cela se faisait via le fichier de configuration en yaml. Depuis quelques releases on peut créer des UM depuis l'interface GUI et même modifier ensuite le Friendly Name (celui apparaitra dans Lovelace ou le module Energy) et même la source.

Je vais donc nommer mon compteur sensor.energy_total_yearly_plug_bw_03, avec comme Frienly Name Lave Linge et l'ajouter au module Energy. Et surtout il y restera, faute de quoi le module Energy perdrait l'historique des données.

EDIT 06/02/2023 : Les utility_meter: (UM) sont entre autre destinés à aller dans le module Energy. Leur fonction est de mesurer la consommation d'un appareil et non d'un module ou prise. Donc contrairement à ce que j'ai expliqué et fait ci dessous, un UM pour un lave linge doit s'appeler Lave Linge et son entity_id: être cohérent. Pour ne pas mélanger je métrais un préfixe, donc sensor.um_lave_linge et non le nom de la prise comme je l'ai fait.

En cas de problème avec une source de consommation, il est maintenant possible de changer celle ci en cliquant sur le bouton options. Ca n'a pas toujours été le cas, notamment en yaml ou il m'est arrivé de perdre des données. Pareil en YAML pour les UM précédemment crées, on change la source mais jamais le nom de l'entité. Exemple ou j'avais un Shelly que j'ai remplacé par un Legrand.

  energy_total_yearly_1pm_ecs:
    source: sensor.legrand_contactor_energy_integration
    cycle: yearly

 Et si on veut changer le nom qui s'affiche dans le module Energy, on passe par customize.yaml :

sensor.energy_total_yearly_1pm_ecs:
  friendly_name: 'ECS'

Pour être sur d'assurer la continuité quand on doit remplacer un équipement, le plus simple est de supprimer l'ancien (ou le renommer) avant d'appairer le nouveau. Et ensuite de vérifier que le nouveau a bien toutes les entités avec le même nommage que le précédent, en ajustant le cas échéant (s'il s'agit du même modèle).

Parfois il sera impossible de supprimer un module dans Home Assistant et il faudra taper dans le dur (fichiers cachés...), comme par exemple ce qui remonte de Deconz ou même en supprimant un module dans Phoscon celui-ci ne disparait pas toujours de HA tant que l'on aura pas supprimé l'intégration... (il y a un contournement qui consiste à appairer le module en ZHA, le supprimer de ZHA, ce qui le supprimera de ZHA et Deconz, et ensuite le réappairer. Tordu, mais ça fonctionne).

 

Home Assistant, timers & choose

Il y a bientôt deux ans, peu après mes débuts sous Home Assistant, j'ai mis en place un scénario de confort pour la douche. En gros la salle de bain est toujours en mode ECO et quand je souhaite prendre une douche j'appuie sur un bouton, ça lance le chauffage et un radiateur soufflant et ça l'éteint au bout d'un moment.

Aujourd'hui j'ai voulut l'améliorer afin de le lancer quand je dois me lever à une heure fixe, ce qui heureusement est plutôt rare. J'ai donc fait une automation qui se déclenche à partir d'un input_datetime: qui va chauffer la salle de bain, lancer la clim en mode chauffage via un script de dérogation de Schedy, éclairer quelques lampes et ouvrir quelques volets. Rien d'extraordinaire à ce stade et il faudrait y ajouter de la musique et allumer la bouilloire pour le thé....

automation:
- id: 'fab3sdfty-bgg6-4ddc-a23f-ee589300012c'
  alias: "RC : Réveil + douche"
  description: ''
  trigger:
    platform: time
    at: input_datetime.alarm_clock_date_time
  condition:
  action:
  - service: timer.start              # On lance le chauffage de la SdB
    data:
      duration: 00:25:30
    target:
      entity_id: timer.shower
  - service: input_boolean.turn_off   # On passe en mode jour (utile pour le chauffage)
    target:
      entity_id: input_boolean.to_sleep 
  - service: script.heating_ac_boost
  - delay: 00:10:00                   # On se prélasse encore un peu au lit...
  - service: cover.set_cover_position # On entreouvre le volet de la chambre
    data:
      position: 22
    target:
      entity_id:
        - cover.vr_lionel
  - service: cover.set_cover_position # On ouvre le volet de la cuisine
    data:
      position: 60
    target:
      entity_id:
        - cover.vr_cuisine      
  - service: light.turn_on            # On allume quelques lampes
    data:
      color_name: yellowgreen
    target:
      entity_id: light.groupe_d_ampoules_led

Là ou ça va devenir intéressant c'est que j'ai refondu mes automations de douche en une seule qui va utiliser un timer: et la fonction choose:. Je n'avais jamais utilisé de timer et encore moins le chooser, j'ai donc un peu galéré, mais voici le résultat, largement simplifié avec les trigger_id:, ce qui évite des templates...

automation:
- id : 'c88f056d-8bbc-40ff-a044-a3b1b733e3c8'
  alias: "RC : Boost SdB"
  description: "Boost SdB by chooser"
  trigger:
    - platform: numeric_state
      entity_id: sensor.rpi_mi_t_sdb
      above: input_number.sdb_boost_good
      id: "t_good"
    - platform: numeric_state
      entity_id: sensor.rpi_mi_t_sdb
      above: input_number.sdb_boost_max
      id: "tmax"
    - platform: event
      event_type: timer.finished
      event_data:
        entity_id: timer.shower
      id: "timer.finished"
    - platform: event
      event_type: timer.started
      event_data:
        entity_id: timer.shower
      id: "timer.started"
  condition: []
  action:
    - choose:
        - conditions:
            - condition: trigger
              id: "timer.started" # Mode ON sur la base su timer qui est lancé depuis une télécommande ou une autre automation
          sequence:
            - service: climate.set_preset_mode
              data:
                preset_mode: boost
              target:
                entity_id: climate.thermostat_salle_de_bain
            - service: switch.turn_on
              entity_id: switch.plug_bw_01
            - service: notify.slack_hass_canaletto
              data:
                message: "{{now().strftime('%d/%m/%Y, %H:%M')}} > BOOST | START | Salle de bain | Température : {{ states('sensor.rpi_mi_t_sdb') }}°"
            - service: tts.cloud_say
              entity_id: media_player.sonos_cloud_hall
              data_template:
                message: "Préparation de la salle de bain. Je vous dirait quand vous pourrez vous doucher !"
                cache: 'false'
        - conditions:
            - condition: trigger
              id: "t_good" # On annonce que la température de confort+ est atteinte
          sequence:
            - service: tts.cloud_say
              entity_id: media_player.sonos_cloud_hall
              data_template:
                message: "La température de la salle de bain est de {{states('sensor.rpi_mi_t_sdb')}}°, vous pouvez vous doucher !"
                cache: 'false'
            - service: notify.slack_hass_canaletto
              data:
                message: "{{now().strftime('%d/%m/%Y, %H:%M')}} > BOOST | READY | Salle de bain | Température : {{ states('sensor.rpi_mi_t_sdb') }}°"
        - conditions:
            - condition: or
              conditions:
                - condition: trigger
                  id: "timer.finished" # On arrete de chauffage à la fin du timer
                - condition: trigger
                  id: "t_max"          # On arrete le chauffage si la température max est atteinte.
          sequence:
            - service: switch.turn_off
              entity_id: switch.plug_bw_01
            - service: climate.set_preset_mode
              data:
                preset_mode: eco
              target:
                entity_id: climate.thermostat_salle_de_bain
            - service: notify.slack_hass_canaletto    
              data:
                message: "{{now().strftime('%d/%m/%Y, %H:%M')}} > BOOST | END | Salle de bain | Température : {{ states('sensor.rpi_mi_t_sdb') }}°"
            - service: tts.cloud_say
              entity_id: media_player.sonos_cloud_hall
              data_template:
                message: "Fin du chauffage de la salle de bain. A bientôt !"
                cache: 'false'
      default:

Le log dans Slack me sert au debug. Le TTS via l'intégration Sonos Cloud permet de baisser la musique et de la réactiver une fois l'annonce terminée.

On notera également que par défaut les conditions du chooser son and et qu'il est possible de les passer en or.

Si aucun trigger n'est utilisé on peut également définir une action par défaut. J'ai fait le choix de ne pas l'utiliser et d'activer ou désactiver le chauffage en lançant le timer pour une certaine durée, depuis un bouton (via ControlerX ou un BluePrint) ou l'automation du réveil : 

  - service: timer.start
    data:
      duration: 00:25:30
    target:
      entity_id: timer.shower

ou en forçant sa fin...

  - service: timer.finish
    target:
      entity_id: timer.shower

Et pour paramétrer une ligne sur Lovelace, à noter que j'ai oublié d'intégrer un template pour la durée dans les commandes ci dessus.

 

Home Assistant & Keypad

Notre serveur domotique préféré inclus un système de sécurité intrusion, communément appelé alarme. Attention, c'est du DIY, ça fait le job, mais ça ne répond pas aux normes en vigueur :

Les systèmes d'alarmes sont évalués en fonction de divers critères donnant lieu à l'attribution d'une certification appelée "norme alarme". En France, les alarmes sont certifiées par le CNPP, qui leur attribue des normes : NFA2P bouclier 1, NFA2P bouclier 2, NFA2P bouclier 3, suivant le degré de sécurité. Au niveau européen, il existe également une norme alarme : la norme EN 50131.

Ceci étant posé, rappelons de Home Assistant intègre un panneau d'alarme basic que l'intégration Alarmo vient avantageusement compléter, et dans l'absolu ça fait mieux que beaucoup de produit du marché.

Pour une bonne efficacité on part du principe que l'installation Home Assistant est fiable et secourue par un UPS.

Dans cet article je ne vais pas détailler le fonctionnement de cette alarme mais m'intéresser aux différentes façons de l'activer / désactiver :

  • Via l'application mobile : facile, mais fastidieux à l'usage. Tous les occupants ne disposent pas nécessairement de l'application mobile Home Assistant.
  • Avec un bouton ou une télécommande : facile, mais il faut transporter l'objet et les distribuer.
  • Avec un badge ou tag RFID sur un lecteur : lecteur esp32 à configurer, il faut transporter l'objet. Facile à distribuer et à révoquer.
  • Avec un badge et un téléphone mobile : facile et sécurisé, il faut enregistrer au préalable les mobiles qui devront disposer de l'application Home Assistant..
  • Avec un clavier numérique : le mode classique et universel, changement des codes faciles, encore faut t'il trouver un clavier, et c'est l'objet de cet article.

Un keypad Zigbee compatible

Il y a quelques jours j'ai vu passer une vidéo qui parle d'un kit alarme Linkind qui utlise Zigbee. Ce kit est composé d'une sirène qui est en fait un hub Zigbee qui se connecte au cloud du fabricant chinois pour gérer l'ensemble, d'un clavier et de quelques détecteurs. L'auteur de la vidéo à utilisé zigbee2mqtt et Node Red (je n'aime pas) pour l'intégrer à Home Assistant, moi je vais essayer de faire ça via ZHA.

J'ai commandé ce kit chez Amazon (28 €) en me disant qu'au pire ce serait un retour de plus, mon idée étant d'utiliser le clavier. Il y a des travaux en cours sur le Hub / Sirène mais c'est loin d'être aboutit. En ce qui concerne les capteurs il est possible de les associer via ZHA (ou z2m et Deconz).

Premier problème que je n'ai pas résolu, le code (1234 par défaut) qui se change via l'application du constructeur. Sauf que quand on l'intègre avec ZHA ça passe par un reset et on se retrouve avec le code par défaut. Bref un clavier avec comme code 1234 ça ne servirait pas à grand chose. Mais il y a une astuce, et la voici : (et une info à la fin de cet article)

Quand on entre une information sur le clavier, celle ci est envoyée à Home Assistant via ZHA (ça doit fonctionner à l'identique avec Deconz, sauf que pour l'instant il n'est pas reconnu) et on peut la récupérer via les "events". Voici un exemple si je saisit Disarm + 1234 + Valid sur ce clavier :

{
    "event_type": "zha_event",
    "data": {
        "device_ieee": "69:0a:x2:ff:fe:xa:x8:22",
        "unique_id": "69:0a:e2:xf:xe:xa:88:2x:1:0x05xx",
        "device_id": "c036fgqd qfdqs56hshs56shsdd06152267ab",
        "endpoint_id": 1,
        "cluster_id": 1281,
        "command": "arm",
        "args": {
            "arm_mode": 0,
            "arm_mode_description": "Disarm",
            "code": "1111",
            "zone_id": 0
        }
    },
    "origin": "LOCAL",
    "time_fired": "2022-02-21T23:27:45.169891+00:00",
    "context": {
        "id": "edada8770ddfd7045b1835acb0888bad",
        "parent_id": null,
        "user_id": null
    }
}

A partir de là il est facilement possible de traiter cette information dans une automation et de générer une action, ici un message :

automation:
- alias: Keypad Test
  description: 'Triggers an Event When code 1111 is entered into any keypad.
  trigger:
    - platform: event
      event_type: zha_event
      event_data:
        command: 'arm'
        args:
          arm_mode: 0
          arm_mode_description: 'Disarm'
          code: '1111'
          zone_id: 0
  condition: []
  action:
    - service: notify.slack_hass_canaletto
      data:
        message: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > ENTER HOME | Code 1111 | State : {{ states.alarm_control_panel.alarmo.state }}"

Et là ou ça devient intéressant, c'est que l'on peut saisir n'importe quoi et que ça sera toujours remonté via un event. A partir de là on peu faire passer tous les codes possibles via 2 pseudos modes d’armement, voire même le disarm.

  • Arm_All_Zones
  • Arm_Day_Home_Only

Bon c’est un peu du bricolage… mais ça fait le taff. Dans la pratique, de base le désarmement demande le code enregistré dans le clavier (1234 par défaut) on peu armer les deux modes à la volée avec n'importe quel code, par exemple :

Touche Arm Home + 4444 + Valid va envoyer un event avec :

"args": {
            "arm_mode": 1,
            "arm_mode_description": "Arm_Day_Home_Only",
            "code": "4444",
            "zone_id": 0

Touche Arm Away + 5555 + Valid va envoyer un event avec :

"args": {
            "arm_mode": 3,
            "arm_mode_description": "Arm_All_Zones",
            "code": "5555",
            "zone_id": 0

A partir de là on interprète le code avec une automation et on lui fait faire ce que l’on veut très simplement.

automation:
  alias: Keypad Test
  description: 'Triggers an Event When code 1111 is entered into any keypad.'
  trigger:
    - platform: event
      event_type: zha_event
      event_data:
        command: 'arm'
        args:
          arm_mode: 0
          arm_mode_description: 'Disarm'
          code: '1111'
          zone_id: 0
  condition: []
  action:
  - service: alarm_control_panel.alarm_disarm
    data:
      code: !secret alarm_code
    entity_id: alarm_control_panel.alarmo
  - service: alarm_control_panel.alarm_disarm
    data:
      code: !secret alarm_code_visonic
    entity_id: alarm_control_panel.visonic_alarm
  - service: notify.slack_hass_canaletto
    data:
      message: "{{now().strftime('%d/%m/%Y, %H:%M:%S')}} > ENTER HOME | DISARM ALARM's | State : {{ states.alarm_control_panel.alarmo.state }}" 

Bonus

Rester appuyé 3 secondes sur SOS, ça active le Panel alarme Linkind dans HA et on peut traiter l'information.

Edit 05/07/2022

Je ne m'étais pas repenché sur la question de ce code 1234 par défaut depuis l'écriture de cet article. Mais Aurel RV a creusé un peu plus et à découvert par hasard que ZHA sait gérer ce code par défaut. Depuis quand je ne sais pas.

Le clavier étant vu comme un panneau de contrôle d'alarme, on pourra interagir avec Alarmo ou continuer à se servir des autres codes vie les events pour commander d'autres actions...

Conclusion

Ce résultat n'est pas totalement satisfaisant (mais qui le devient depuis que ZHA gère ce code), mais ça permet de faire passer les bons codes à Alarmo et c'est utilisable. De plus on peut utiliser d'autres codes pour déclencher d'autres actions, ouvrir un portail, allumer des projecteurs, etc....

 

 

 

 
 

 

 

Home Assistant & Volets roulants

Voilà un chapitre que je n'avais pas encore abordé, et pour cause je n'(avait pas de volets roulants. J'ai donc pensé mon installation de volets roulants avec la contrainte (ou la présence) de la domotique, tout en excluant pas un fonctionnement autonome.

Dans le cadre d'une isolation par l'extérieur (ITE), j'ai fait le choix de volets roulants filaires, d'une par afin d'éviter un surcout, mais surtout afin de pouvoir les commander à ma guise. Pour la commande j'ai choisit de faire ramener tous les câbles au tableau électrique afin que les modules Shelly 2.5 soient accessibles et surtout qu'ils ne soient pas exposés à la chaleur, le froid et l'humidité s'ils étaient placés dans les caissons. Les 6 Shelly 2.5 sont montés sur des supports DIN imprimés en 3D et j'ai trouvé des câbles en 4 fils souples en 0.75, ce qui est tout de même facile pour câbler l'ensemble dans le tableau.

Vous remarquerez l'utilisation d'embout de câblage pour câble souple multibrins. Utiliser du câble souple en 0.75 est largement suffisant, mais on ne câble jamais du fil souple multibrin directement sur un module ou un disjoncteur, soit on utilise ce genre d'embouts, soit on étame.

J'ai pensé le tout afin que mon installateur (qui a joué le jeu et c'est rare pour le préciser) n'ait qu'à poser les volets et les raccorder dans les boites de dérivation qu'il a au préalable posées dans les combles. Vous l'aurez compris il n'y a pas d'inverseur sous les volets, il était hors de question de poser des baguettes en plastique dans une maison ou tout est encastré. La commande de ces volets se fera donc soit par des scénarios, soit par des télécommandes sans fil via Home Assistant.

Un mode autonome

Il faut bien sur penser à la possibilité ou Home Assistant est défaillant, ou simplement au jour ou je ne serais plus là pour le maintenir. Pour ça les Shelly 2.5 permettent le câblage d'inverseurs filaires et je vais installer 6 inverseurs au tableau (voir plus bas en bonus).

Une alternative serait de piloter les 6 Shelly 2.5 par des poussoirs sur des Shelly E3, mais on ne s'affranchit pas d'une panne de WI-FI, et tant qu'à penser une commande autonome autant qu'elle le soit à 100%.

Le mode assisté

Pour le mode sans fil je vais utiliser des télécommandes on/off de chez Ikea à 5.99 € pour les chambres et une télécommande à 5 boutons pour les pièces de vie. En Zigbee elle sont appairées sous Home Assistant avec ZHA, mais le process est identique avec Z2M ou Deconz.

Quant au contrôle de ces télécommandes je vais simplement utiliser l'excellent ControlerX sous appDaemon que j'utilise déjà avec satisfaction pour les éclairages.

Ca reste très simple et le contrôle est dynamique, voici un exemple pour la télécommande on/off Ikea, comme vous pouvez le voir, en 6 lignes c'est géré :

volet_zha_ikea_05:
  module: controllerx
  class: E1743CoverController
  controller: "8c:f6:81:ff:fe:51:d0:b4"
  integration: zha
  cover: cover.vr_marie

Attention, certaines version de cette télécommande on on problème qui conduit sous ZHA ou Zigbee2MQTT à un rapide (24 h.) vidage des piles. J'ai donc été contraint de toutes les repasser sous Deconz...

volet_rc_ikea_05:
  module: controllerx
  class: E1743CoverController
  controller: 8c:f6:81:ff:fe:51:d0:b4
  integration: 
    name: deconz
    listen_to: unique_id
  cover: cover.vr_marie

Pour la télécommande à 5 boutons on va commencer par gérer le choix du volet à commander via un input_select:, et donc créer celui ci :

input_select:
  rc_ikea_vr:
    name: Select
    icon: mdi:light-switch
    options:
      - vr_baie
      - vr_sejour
      - vr_cuisine
      - vr_antoine
      - vr_lionel
      - vr_marie

Ensuite on va créer une commande pour ControlerX qui va nous permettre de sélectionner un volet avec les touches < et >. Inconvénient ça se fait à l'aveugle, mais ça permet de tout rassembler dans une seule télécommande :

select_vr_zha:
  module: controllerx
  class: Controller
  controller: 84:2e:14:ff:fe:a9:4d:bd
  integration: zha
  mapping:
    press_257_13_0:
      service: input_select.select_previous
      data:
        entity_id: input_select.rc_ikea_vr
    press_256_13_0:
      service: input_select.select_next
      data:
        entity_id: input_select.rc_ikea_vr

Ensuite on va les commandes propres à chaque volet (voir ici pour les mappings propres à chaque source Zigbee), on y place des contraintes (la position de notre input_select) et on exclu les actions droite et gauche que l'on utilise pour le choix du volet à commander. Il faudra bien sur dupliquer cette commande en fonction du nombre de volets. :

vr_app_1_zha:
  module: controllerx
  class: CoverController
  controller: 84:2e:14:ff:fe:a9:4d:bd
  integration: zha
  cover: cover.vr_cuisine
  constrain_input_select: input_select.rc_ikea_vr,vr_cuisine
  excluded_actions: [press_257_13_0, press_256_13_0]
  mapping:
    step_with_on_off_0_43_5: toggle_open
    step_1_43_5: toggle_close

Il nous reste le bouton central de cette télécommande qui est inexploité. On va s'en servir pour ouvrir ou fermer l'ensemble des volets de la maison en créant un groupe de volets avec l'intégration Cover Group :

cover:
  - platform: group
    entities:
      - cover.vr_baie
      - cover.vr_sejour
      - cover.vr_cuisine
      - cover.vr_antoine
      - cover.vr_lionel
      - cover.vr_marie

Et on poursuit avec la commande idoine pour ControlerX, il n'y a pas de contrainte sur l'input_select car le bouton central est utilisable dans toutes les positions :

vr_app_0_zha:
  module: controllerx
  class: CoverController
  controller: 84:2e:14:ff:fe:a9:4d:bd
  integration: zha
  cover: cover.volets
  excluded_actions: [press_257_13_0, press_256_13_0]
  mapping:
    press_2_0_0 : toggle_close
    toggle: toggle_open

Commandes

Pour l'affichage je me suis simplement inspiré de de post. Merci à lui !

L'avantage c'est qu'il y a un preset à x%. On pourrait facilement faire plusieurs. Voici le code de la carte en Vertical Stack :

type: grid
cards:
  - type: button
    show_name: false
    entity: cover.volets
    show_state: true
    show_icon: true
    hold_action:
      action: none
    tap_action:
      action: more-info
    theme: teal
  - type: button
    tap_action:
      action: call-service
      service: cover.open_cover
      service_data: {}
      target:
        entity_id: cover.volets
    show_name: false
    name: Ouvrir
    icon: mdi:arrow-up-bold
  - type: button
    tap_action:
      action: call-service
      service: cover.stop_cover
      service_data: {}
      target:
        entity_id: cover.volets
    show_name: false
    name: Stop
    icon: mdi:pause
  - type: button
    tap_action:
      action: call-service
      service: cover.close_cover
      service_data: {}
      target:
        entity_id: cover.volets
    show_name: false
    name: Fermer
    icon: mdi:arrow-down-bold
  - type: button
    tap_action:
      action: call-service
      service: cover.set_cover_position
      service_data:
        position: 20
      target:
        entity_id: cover.volets
    show_name: false
    name: Soleil
    icon: mdi:weather-sunny
  - type: picture-elements
    image: /local/images/1px2.png
    elements:
      - type: state-label
        entity: cover.volets
        attribute: current_position
        suffix: '%'
        tap_action:
          action: more-info
        style:
          top: 50%
          left: 50%
          font-size: 16px
          font-weight: bold
          color: '#44739E'
    view_layout:
      position: sidebar
columns: 6
square: true

Il ne faut pas oublier une petite image de  pixel. Et la seconde partie à dupliquer par le nombre de volets :

type: grid
cards:
  - type: button
    show_name: true
    name: Baie
    show_state: false
    tap_action:
      action: more-info
    entity: cover.vr_baie
    hold_action:
      action: none
    theme: teal
  - type: button
    tap_action:
      action: call-service
      service: cover.open_cover
      service_data: {}
      target:
        entity_id: cover.vr_baie
    icon: mdi:arrow-up-bold
    name: Ouvrir
    show_icon: true
    show_name: false
  - type: button
    tap_action:
      action: call-service
      service: cover.stop_cover
      service_data: {}
      target:
        entity_id: cover.vr_baie
    icon: mdi:pause
    name: Stop
    show_name: false
  - type: button
    tap_action:
      action: call-service
      service: cover.close_cover
      service_data: {}
      target:
        entity_id: cover.vr_baie
    icon: mdi:arrow-down-bold
    name: Fermer
    show_name: false
  - type: button
    tap_action:
      action: call-service
      service: cover.set_cover_position
      service_data:
        position: 70
      target:
        entity_id: cover.vr_baie
    icon: mdi:weather-sunset-up
    name: Soleil
    show_name: false
  - type: picture-elements
    image: /local/images/1px2.png
    elements:
      - type: state-label
        entity: cover.vr_baie
        attribute: current_position
        suffix: '%'
        tap_action:
          action: more-info
        style:
          top: 50%
          left: 50%
          font-size: 16px
          font-weight: bold
          color: '#44739E'
    view_layout:
      position: sidebar
square: true
columns: 6

Optimisation

Disposer de volets roulants électriques permet également une optimisation du confort thermique. Pour résumer on peut gagner en température en gérant leurs positions en fonction de l'ensoleillement. Il existait sous d'autres solutions domotiques des "choses" permettant cette optimisation, et je sais que quelque chose se prépare sous Home Assistant, on en reparlera donc bientôt.

Sécurisation

Les volets roulants étant moins sécurisants que mes vieux volets en vrai bois, je vais installer des contacteurs filaires afin de détecter un éventuel arrachage. J'avais pensé à un détecteur de vibration, mais je pense qu'avec le Mistral cela va créer trop de faux positifs.

Je ne vais pas utiliser l'aimant fournit avec ces contacteurs magnétiques, mais un aimant fin collé sur la dernière lame du volet. Et comme j'ai déjà sur chaque fenêtre un détecteur d'ouverture Visonic qui dispose d'une entrée filaire, ça devrait être un jeu d'enfant (le fil sera noyé dans l'isolant du tableau des fenêtres). Ensuite l'information remonte dans la centrale Visonic et dans Home assistant, je pourrais ainsi allumer les projecteurs extérieurs si un malotru tente d'arracher un de mes volets...

Bonus

Toute ces automatisations et commandes à distance c'est bien beau, mais imaginez que le WI-FI déraille ou que Home Assistant, que je trouve pourtant hyper fiable, se plante ? Vous risquez de vous retrouver dans le noir car je n'ai pas mis d'interrupteur filaire sous les volets.

Il y a deux façons d'aborder la chose, j'ai d'abord pensé à installer un panneau un peu caché au centre de la maison avec ces interrupteurs, dans un placard par exemple, sauf qu'il n'y en pas et qu'il aurait fallut y emmener un multipaire connecté aux Shelly's 2.5. Le faire en WI-FI avec des modules I3 était une option qui permet de s'affranchir de Homme Assistant mais pas du WI-FI. Au final j'ai trouvé ces modules DIN de chez Schneider, 3 modules feront l'affaire pour 6 volets en configurant correctement le Shelly (ça monte ou descend et un second appui fait le stop). Attention, si le prix catalogue de ces modules est de 27 € HT, je les ai trouvé à 82.50 € chez Amazon, à environ 40 chez les boutiquiers de la domotique pour finir ici à 9.70 TTC (et pour une fois je vous mets le lien).

Un prochain article parlera de l'automatisation intégrale des volets en fonction des températures, de l'ensoleillement et des contraintes de vie de chacun. A suivre...

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..

 

Home Assistant & Thermostats

Sous Home Assistant le thermostat de base (climate:) est plutôt "basic", et c'est le moins que l'on puisse dire ! Fort heureusement il y a des personnes géniales qui savent coder et se sont attelées à le modifier.

Simple Thermostat

Ce premier fork reprend simplement le thermostat de base en y ajoutant les "presets" que tout le monde attendait. En gros il y a deux façons d'utiliser un thermostat, soit on modifie dynamiquement la valeur de consigne avec des automations, le Scheduler ou Schedy et c'est ce que je fais, soit on joue la carte des presets à la française avec des modes ECO, CONFORT, Etc.. Et c'est ce que fait Simple Thermostat de façon très simple. Par exemple si on se positionne en BOOST et que l'on lui indique une consigne de 22°, il la gardera en mémoire. Ensuite on pourra rappeler le preset avec une automation

service: climate.set_preset_mode
target:
  entity_id: climate.thermostat_lionel
data:
  preset_mode: boost

Ou encore via l'interface :

EDIT 04/02/2022 : à partir de la version 2022-02 le thermostat générique de base évolue. Il est maintenant possible de définir des préréglages et de les rappeler via un service. Peut être pas aussi smart que Simple Thermostat, mais ça a le mérite d'être intégré dans le core et de ne pas faire appel à un composant externe qui peut toujours être délaissé par son développeur :

climate:
  - platform: generic_thermostat
    name: "Thermostat : Hall"
    heater: switch.sw03_hall
    target_sensor: sensor.rpi_mi_t_hall
    min_temp: 12
    max_temp: 24
    ac_mode: false
    cold_tolerance: 0.3
    hot_tolerance: 0
    min_cycle_duration:
      seconds: 360
    keep_alive:
      minutes: 3
    away_temp: 10
    comfort_temp: 20
    home_temp: 21
    sleep_temp: 18
    activity_temp: 19
    precision: 0.1

Smart Thermostat

On est ici face à un thermostat intelligent (j'ai pas non plus dit AI !) qui se base sur un contrôleur PID afin de déterminer la période pendant laquelle le convecteur devra être ON afin de profiter de l'inertie de celui ci. Le capteur cible mesure la température ambiante tandis que l'interrupteur contrôle un système de chauffage ON/OFF. Le contrôleur PID calcule la durée pendant laquelle le convecteur doit rester allumé pendant la période PWM pour atteindre le point de consigne, par exemple avec PWM réglé sur 15 minutes, si la sortie est de 100 %, le réchauffeur restera allumé pendant les 15 prochaines minutes. Si la sortie PID est de 33 %, le réchauffeur sera allumé pendant 5 minutes seulement.

Vous n'avez pas tout compris ? J'avoue que moi non plus. Mais le fait est que ça fonctionne, et d'ailleurs pour preuve en voici la formule :

Plus sérieusement, ce système sera plus particulièrement efficace avec des convecteurs (ou un autre système de chauffage) avec une forte inertie. Donc exit les convecteurs du genre grille pain...

Pour ceux qui aimerait en savoir plus je vous conseille la lecture du projet ou le mainteneur nous donne pas mal d'explications ainsi que quelques liens :

Ensuite voici la configuration type. On notera qu'ici l'idée des presets est reprise, mais qu'il faut les figer dans la configuration alors qu'ils sont dynamiques dans Simple Thermostat (Adrien si tu nous suit..).

climate:
  - platform: smart_thermostat
    name: "Thermostat : Lionel"
    heater: switch.sw01_lionel
    target_sensor: sensor.mi_lionel_temp
    min_temp: 10
    max_temp: 26
    ac_mode: False
    target_temp: 19
    keep_alive:
      seconds: 60
    precision: 0.1
    away_temp: 14
    eco_temp: 18
    boost_temp: 22
    comfort_temp: 20
    home_temp: 21
    sleep_temp: 17
    activity_temp: 20
    kp : 75
    ki : 0.001
    kd : 70000
    pwm : 00:15:00

Bien sur ces réglages sont à affiner en fonction du convecteur et de la pièce à chauffer. Pour cela il existe un mode AutoTune :

    autotune: "ciancone-marlin"

Dixit son auteur ce mode n'est pas encore tout à fait sur. J'y reviendrait donc plus tard au fil de mes tests.

En attendant on peu constater que là ou le convecteur aurait été ON jusqu'à attendre la température de consigne avec le thermostat standard (la première ligne), il y a eu plusieurs interruptions avec ce thermostat (la seconde ligne). Et  vous l'aurez compris, plus les interruptions sont importantes, moins la consommation le sera, et la facture d'autant plus réduite.

Multizone Thermostat

Il existe une autre variante qui intègre ces différents modes et bien qu'utilisable par zones indépendantes permet en plus d'ajuster la température voulue sur plusieurs zones adjacentes. Je vous laisse lire la description compète sur son Git, je l'ai quand à moi installé dans mon bureau afin d'essayer de déterminer que mode sera le plus adapté entre cout et confort. et je verrais dans quelques jours si le résultat est probant.

Il propose plusieurs modes de fonctionnement :

  • ON/OFF : proche du thermostat de base mais en prenant compte des valeurs d'hystérésis. Dans ce mode il peut également gérer les modes chauffage et refroidissement.
  • Proportionnel : PID avec possibilité de compensation météo et gestion de plusieurs valves. Les possibilité sont énormes mais la mise au point sera délicate.

Autres

Ici on va trouver une autre approche que je n'ai pas explorée.

Conclusion

Ces thermostats évolués sont très intéressants sur le papier et remplacent avantageusement le thermostat de base en fonction des besoins. Par contre ils restent encore complexes à mettre au point et il faudra peut être tout l'hiver pour en tirer des conclusions...

J'y reviendrais dans les prochains jours, mais ces deux approches constituent une avancée importante pour Home Assistant et j'espère que tout cela sera un jour intégré au Core.

Echanger

Voici deux fils ou échanger sur ce sujet :

 

Home Assistant & Schedy, encore...

L'hiver approche et on ouvre à nouveau ce sujet...

Pour mémoire, afin de planifier un peu finement pièce par pièce un chauffage électrique par convecteurs, on a principalement trois possibilités dans Home Assistant :

  • Faire une multitude de d'automations en YAML, j'ai fait lors de mes débuts avec Home Assistant et j'en ai parlé ici et .
  • Utiliser le Scheduler de Niels, j'en ai parlé ici et c'est une bonne solution GUI, mais il y a des lacunes (pas de replanification à la volée, pas de gestion ECO entre les plages).
  • Et enfin Schedy dont j'ai parlé plusieurs fois ( 1 | 2 ), qui lui ne dispose pas d'interface. On va donc s'en servir de moteur et lui construire une interface Lovelace. En fait j'ai géré ma clim avec tout l'été sur la base de ce que j'avais fait et c'est parfait. Je vais maintenant l'adapter afin d'en faire profiter mon frère, avec d'autres contraintes et le souhait qu'il soit le plus autonome possible.

Cahier des charges

On veut pouvoir gérer :

  • 4 plages d'horaires par jour pour 3 types de journées :
    1. Jour de semaine travaillé.
    2. Samedi matin travaillé.
    3. Dimanche, jours fériés et vacances à la maison...
  • Activation / désactivation possible de chacune des plages et faire en sorte qu'elles ne soient affichées que si elles sont actives. Le but étant de ne pas afficher les plages inutiles pour ne pas prêter à confusion. Par exemple, le dimanche si on chauffe le séjour de 10:00 à minuit, on a besoin que d'une seule plage.
  • Une température différente pour chacune des plages, en opposition à un classique ECO/CONFORT à la française. 
  • Une température ECO commune à toutes les périodes entre les plages; mais propre à chaque pièce et une température ABSCENT (HG).
  • La présence de l'occupant de la pièce (en entrant ses dates d'arrivée et de départ ou par géolocalisation, présence dans un rayon de...). Cette fonction ne sera utile que pour les chambres des enfants qui vont et viennent...
  • La planification des vacances de l'occupant principal, avec passage en mode week-end si présent.
  • Boost dynamique : température t pendant n minutes. Utile pour une dérogation temporaire, genre un occupant ne va pas travailler un lundi après-midi, il serait dommage qu'il se gèle...
  • Désactivation en cas d'ouverture d'une fenêtre pendant n minutes. Inutile de chauffer quand on aère les pièces...
  • Un mode télétravail, je ne l'ai pas encore vraiment défini, mais il s'agira probablement d'un script qui va apporter quelques dérogation sur le principe du boost.
  • Un mode dormir (utile pour passer en ECO les pièce de vie quand on va se coucher plus tôt que ce qui est programmé, s'insère dans une automation plus globale qui désactive également les éclairages).
  • Un mode absence temporaire (on ne passe pas le chauffage en mode ABSCENT HG comme une absence de longue durée, mais en mode ECO (binary sur géo loc ou via les fonction de l'alarme).

Philosophie

Toutes les plages sont gérés par des binary_sensor via différentes sources (input_datetime, input_number, input_boolean, et d'autres binary_sensor...) et Schedy ne fera qu'affecter la température de consigne correspondant à la plage au thermostat (climate:) quand le binary passera à ON. Il nous faut donc 1 binary par plage, ce qui fait 3 groupes de 4 = 12 pour une pièce. Ces binary constituent la partie centrale, ils risquent donc d'évoluer et je publierait les mise à jour sur GitHub. Ne cherchez pas à recopier ces bouts de code, il ne sont là que pour expliquer la philosophie et proviennent de plusieurs tests, vous trouverez mes sources sur GitHub. et ici pour un second projet plus complet. (je vais essayer de les tenir à jour).

Si pour gérer ces binary_sensor les variables que sont les input_boolean sont simple, il faut préparer les binary_sensor qui vont nous servir à déterminer le type de journée :

binary_sensor:
  - platform: template
    sensors:
      workday_saturday_working:
        friendly_name: "Samedi travaillé"
        value_template: >
          {% if now().isoweekday() in (6,) and states.binary_sensor.workday_sensor.state == 'on' and states.input_boolean.holidays_andre.state == 'off' %}
            true
          {%else%}
            false
          {% endif %}

      workday_weekday_working:
        friendly_name: "Jour de semaine travaillé"
        value_template: >
          {% if now().isoweekday() in (1,2,3,4,5,) and states.binary_sensor.workday_sensor.state == 'on' and states.input_boolean.holidays_andre.state == 'off'%}
            true
          {%else%}
            false
          {% endif %}

      workday_weekend_holidays:
        friendly_name: "Week-End, Vacances ou jour Férié"
        value_template: >
          {% if states.binary_sensor.workday_sensor.state == 'off' or states.input_boolean.holidays_andre.state == 'on'%}
            true
          {%else%}
            false
          {% endif %}

Mais à y réfléchir on peut envisager quelque chose de plus simple avec un template sensor :

template:
  - sensor:
      - name: "Day Type"
        state: >
          {% if now().isoweekday() in (6,) and states.binary_sensor.workday_sensor.state == 'on' and states.input_boolean.holidays_andre.state == 'off' %}
            saturday_working
          {% elif now().isoweekday() in (1,2,3,4,5,) and states.binary_sensor.workday_sensor.state == 'on' and states.input_boolean.holidays_andre.state == 'off'%}
            weekday_working
          {% elif states.binary_sensor.workday_sensor.state == 'off' or states.input_boolean.holidays_andre.state == 'on'%}
            weekend_and_holidays
          {% else %}
            failed
          {% endif %}

Ce qui va nous donner :

    binary_sensor:
      heating_antoine_2_s:
        entity_id: sensor.time    <<< IL FAUT SUPPRIMER CETTE LIGNE DESORMAIS OBSOLETE
        friendly_name: Mode Confort
        value_template: >
          {% set t = states('sensor.time') %}
          {% set start = states('input_datetime.heating_antoine_start_2_s') [0:5] %}
          {% set stop = states('input_datetime.heating_antoine_end_2_s') [0:5] %}
          {{ (start <= t < stop if start < stop else (start <= t or t < stop)) == true   # Slot horaire
            and states('sensor.day_type') == 'saturday_working'                          # Type de journée
            and states('input_boolean.presence_antoine') == 'on'                         # Présence de l'occupant
            and states('input_boolean.heating_antoine_enabled_2_s') == 'on'              # Activation de la plage    
          }}

Ce qui va se traduire dans Schedy par :

  - x: state("input_number.heating_antoine_temperature_confort_2_s") if (is_on("binary_sensor.heating_antoine_2_s")) else Next()

Et bien sur dans Schedy on va gérer d'autres contraintes plus globales :

  - x: "Break() if is_off('input_boolean.thermostats_on_off') else Next()"    # Etat général du chauffage
  - x: "Break() if is_on('binary_sensor.antoine_window_delayed') else Next()" # Fenêtre ouverte

Ainsi que le mode ECO si aucune plage confort n'est active : 

  - x: state("input_number.heating_antoine_temperature_eco") if (is_on("input_boolean.presence_antoine")) else Next()

Et ABSCENT si les contraintes générales son validées :

  - x: state("input_number.heating_antoine_temperature_away")

Et il ne faut pas oublier l'entête, avec deux types de thermostats. A noter que dans le cas du climatiseur on va gérer deux modes HVAC : off et heat_cool.

schedy_heating:
  module: hass_apps_loader
  class: SchedyApp

  actor_type: thermostat
  actor_templates:
    ac:
      # send_retry_interval: 15
      send_retries: 20
      supports_hvac_modes: true 
      # off_temp: 17
      # delta: 0
      hvac_mode_off: "off"
      hvac_mode_on: heat_cool  # J'ai demandé à pouvoir gérer ce mode via un input_select: mais le développeur ne semble pas très chaud...
      # max_temp: 95
      # min_temp: 45

    convecteur:
      send_retry_interval: 30
      send_retries: 10
      supports_hvac_modes: true

Exemples

Et voici la programmation d'une chambre avec un convecteur :

      schedule:
      - rules:
        - rules:
          # CONTRAINTES
          - x: "Break() if is_off('binary_sensor.marie_home') else Next()"          # Absence temporaire, on passe en ECO
          - x: "Break() if is_off('input_boolean.thermostats_on_off') else Next()"  # ON / OFF global du chauffage, on passe le thermostat à OFF
          - x: "Break() if is_on('input_boolean.thermostats_away') else Next()"     # Absence globale prolongée (on ferme la maison), on passe en consigne AWAY
          - x: "Break() if is_on('binary_sensor.marie_window_delayed') else Next()" # Contrainte ouverture de fenêtre, on passe en consigne AWAY
          # CONFORT SEMAINE
          - x: state("input_number.heating_marie_temperature_confort_1") if (is_on("binary_sensor.heating_marie_1")) else Next()
          - x: state("input_number.heating_marie_temperature_confort_2") if (is_on("binary_sensor.heating_marie_2")) else Next()
          - x: state("input_number.heating_marie_temperature_confort_3") if (is_on("binary_sensor.heating_marie_3")) else Next()
          - x: state("input_number.heating_marie_temperature_confort_4") if (is_on("binary_sensor.heating_marie_4")) else Next()
          # CONFORT SAMEDI
          - x: state("input_number.heating_marie_temperature_confort_1_s") if (is_on("binary_sensor.heating_marie_1_s")) else Next()
          - x: state("input_number.heating_marie_temperature_confort_2_s") if (is_on("binary_sensor.heating_marie_2_s")) else Next()
          - x: state("input_number.heating_marie_temperature_confort_3_s") if (is_on("binary_sensor.heating_marie_3_s")) else Next()
          - x: state("input_number.heating_marie_temperature_confort_4_s") if (is_on("binary_sensor.heating_marie_4_s")) else Next()
          # CONFORT DIMANCHE & FERIE
          - x: state("input_number.heating_marie_temperature_confort_1_d") if (is_on("binary_sensor.heating_marie_1_d")) else Next()
          - x: state("input_number.heating_marie_temperature_confort_2_d") if (is_on("binary_sensor.heating_marie_2_d")) else Next()
          - x: state("input_number.heating_marie_temperature_confort_3_d") if (is_on("binary_sensor.heating_marie_3_d")) else Next()
          - x: state("input_number.heating_marie_temperature_confort_4_d") if (is_on("binary_sensor.heating_marie_4_d")) else Next()
          # ECO
          - x: state("input_number.heating_marie_temperature_eco") if (is_on("input_boolean.presence_marie")) else Next()
          - x: "Break(2)"
      # REGLES LIES AUX CONTRAINTES (Attention à l'ordre des règles)
      - x: Mark(OFF, Mark.OVERLAY) if (is_off("input_boolean.thermostats_on_off")) else Next()
      - x: state("input_number.heating_marie_temperature_away") if (is_on("input_boolean.thermostats_away")) or (is_on('binary_sensor.marie_window_delayed')) else Next()
      - x: state("input_number.heating_marie_temperature_eco") if (is_on("input_boolean.presence_marie")) and (is_off("binary_sensor.marie_home")) else Next()

La contrainte fenêtre ouverte est gérée par un binary_sensor: avec une temporisation :

binary_sensor:
  - platform: template
    sensors:
      marie_window_delayed:
        friendly_name: "Fenêtre (delayed)"
        device_class: window
        #window_room: bedroom
        delay_on: 
          seconds: 300
        delay_off:
          seconds: 300
        value_template: >-
          {{ is_state('binary_sensor.fenetre_marie', 'on') }}
        icon_template: >-
          {% if is_state('binary_sensor.fenetre_marie', 'on') %}
            mdi:window-open
          {% else %}
            mdi:window-closed
          {% endif %}  

Quant à l'absence temporaire on va la gérer avec l'intégration proximity: qui va facilement nous donner la distance entre la maison et ma fille, ainsi et surtout sa direction. L'idée est de passer en ECO lorsque ma fille est ici pour quelques jours mais qu'elle est de sortie. Il me faudra temporiser la sortie afin de ne pas passer en ECO trop rapidement (genre elle va chercher du pain...), mais également le retour afin que sa chambre soit chauffée lors du retour effectif... 

- id: '5e1b6bdd-dee7-4a63-b8cb-3b9d01719b2b'
  alias: GEO - Lionel en depart
  mode: single
  trigger:
  - platform: numeric_state
    entity_id:
      - proximity.lionel
    above: 3
  condition:
  - condition: and
    conditions:
    - condition: template
      value_template: '{{ trigger.to_state.attributes.dir_of_travel  == "away_from" }}'
  action:
  - service: input_boolean.turn_off
    target:
      entity_id: input_boolean.presence_lionel_geo
  - service: notify.slack_hass_canaletto
    data:
      message: "{{ states.sensor.date_time.state}} > Lionel s'éloigne de sa maison | Distance : {{ states.proximity.lionel.state }} Km."

Et le même en approche. On bascule un input_boolean: que l'on pourra basculer manuellement en cas de défaillance...

- id: 'a6bbcd6c-8c18-4ea5-8992-8dae0d63c24c'
  alias: GEO - Lionel en approche
  mode: single
  trigger:
  - platform: numeric_state
    entity_id:
      - proximity.lionel
    below: 3
  condition:
  - condition: and
    conditions:
    - condition: template
      value_template: '{{ trigger.to_state.attributes.dir_of_travel  == "towards" }}'
  action:
  - service: input_boolean.turn_on
    target:
      entity_id: input_boolean.presence_lionel_geo
  - service: notify.slack_hass_canaletto
    data:
      message: "{{ states.sensor.date_time.state}} > Lionel s'approche de sa maison | Distance : {{ states.proximity.lionel.state }} Km."

Et pour plus d'élégance et pour ne pas afficher le boolean dans le Lovelace de l'utilisateur on en fait un binary (pour les test le boolean reste plus pratique que d'aller faire un tour en voiture...) :

binary_sensor:
  - platform: template
    sensors:
       lionel_geo:
         friendly_name: Lionel (Présence relative sur Géoloc)
         device_class: presence
         icon_template: >-
           {% if is_state('binary_sensor.antoine_away_from_home','on') %} mdi:home-account
           {% else %} mdi:home-outline
           {% endif %}
         value_template: >-
           {{ is_state('input_boolean.presence_lionel_geo', 'on') }}

Un autre exemple est celui du climatiseur. Dans mon cas il est dans un large couloir et arrose plus ou moins toutes les pièces. C'est bien sur à adapter à la région, l'installation et au mode de vie, mais dans mon cas on va interagir différemment selon la saison .Afin de ne pas tout dupliquer il sera nécessaire d'adapter les consignes en début d'été et en début d'hiver.

  • En été on va avoir des périodes de refroidissement et des périodes ou le climatiseur sera OFF.
  • En hiver on va avoir des périodes CONFORT et des périodes ECO. 

En fait Schedy est fait pour faire fonctionner les climatiseurs en mode Heat/Cool. Ca ne me convient pas car en mode demie saison parfois il se mets en route alors que c'est inutile. J'ai demandé au développeur de faire des modifications, mais ce n'est pas sa préoccupation du moment. J'ai donc dupliqué deux rooms pour lui en faisant de sorte que seule l'une puisse fonctionner. C'est un pis aller en attendant mieux. Ca génère une erreur dans le log, mais elle n'est qu'indicative.

    ac_heat:
      allow_manual_changes: true
      rescheduling_delay: 1
      actors:
        climate.daikin:
          send_retries: 20
          supports_hvac_modes: true
          hvac_mode_off: "off"
          hvac_mode_on: heat
      watched_entities:
      - input_number.heating_ac_temperature_confort_1
      - input_number.heating_ac_temperature_confort_2
      - input_number.heating_ac_temperature_confort_3
      - input_number.heating_ac_temperature_confort_4
      - input_number.heating_ac_temperature_confort_1_d
      - input_number.heating_ac_temperature_confort_2_d
      - input_number.heating_ac_temperature_confort_3_d
      - input_number.heating_ac_temperature_confort_4_d
      - input_number.heating_ac_temperature_eco
      - input_number.heating_ac_temperature_away
      - binary_sensor.heating_ac_1
      - binary_sensor.heating_ac_2
      - binary_sensor.heating_ac_3
      - binary_sensor.heating_ac_4
      - binary_sensor.heating_ac_1_d
      - binary_sensor.heating_ac_2_d
      - binary_sensor.heating_ac_3_d
      - binary_sensor.heating_ac_4_d
      - input_boolean.presence_ac
      - binary_sensor.life_windows_and_doors_delayed
      - input_boolean.heating_enabled
      - input_boolean.cooling_enabled
      - input_boolean.thermostats_ac_on_off

      schedule:
      - rules:
        - x: "Break() if is_off('input_boolean.thermostats_ac_on_off') else Next()"         # Etat général du chauffage
        - x: "Abort() if is_off('input_boolean.heating_enabled') else Next()"               # On stop toute planification si on n'est pas dans ce mode
        - rules:
          # CONTRAINTES
          - x: "Break() if is_off('input_boolean.presence_ac') else Next()"                   # Présence au sens callendrier actif
          - x: "Break() if is_on('binary_sensor.life_windows_and_doors_delayed') else Next()" # Fenêtre ouverte
          
          - rules:
            - x: "Break() if is_on('input_boolean.to_sleep') else Next()"                       # Je vais dormir...
            - x: "Break() if is_off('binary_sensor.lionel_geo') else Next()"                    # Je vais au ciné...
            # CONFORT SEMAINE
            - x: state("input_number.heating_ac_temperature_confort_1") if (is_on("binary_sensor.heating_ac_1")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_2") if (is_on("binary_sensor.heating_ac_2")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_3") if (is_on("binary_sensor.heating_ac_3")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_4") if (is_on("binary_sensor.heating_ac_4")) else Next()
            # CONFORT WEEK-END & FERIE
            - x: state("input_number.heating_ac_temperature_confort_1_d") if (is_on("binary_sensor.heating_ac_1_d")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_2_d") if (is_on("binary_sensor.heating_ac_2_d")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_3_d") if (is_on("binary_sensor.heating_ac_3_d")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_4_d") if (is_on("binary_sensor.heating_ac_4_d")) else Next()
            # ECO
            - x: state("input_number.heating_ac_temperature_eco") if (is_on("input_boolean.presence_ac")) and (is_on("input_boolean.heating_enabled")) else Next()
            - x: "Break(2)"
        # REGLES LIES AUX CONTRAINTES
        - x: state("input_number.heating_ac_temperature_eco") if (is_on("input_boolean.to_sleep")) and (is_on("input_boolean.heating_enabled")) else Next()
        - x: state("input_number.heating_ac_temperature_eco") if (is_off("binary_sensor.lionel_geo")) and (is_on("input_boolean.heating_enabled")) else Next()

      - x: Mark(OFF, Mark.OVERLAY) if (is_off("input_boolean.thermostats_ac_on_off")) else Next()
      - x: Mark(OFF, Mark.OVERLAY) if (is_off("input_boolean.presence_ac")) else Next()      
      - x: Mark(OFF, Mark.OVERLAY) if (is_on("binary_sensor.life_windows_and_doors_delayed")) else Next()


    ac_cool:
      allow_manual_changes: true
      rescheduling_delay: 1
      actors:
        climate.daikin:
          send_retries: 20
          supports_hvac_modes: true
          hvac_mode_off: "off"
          hvac_mode_on: cool
      watched_entities:
      - input_number.heating_ac_temperature_confort_1
      - input_number.heating_ac_temperature_confort_2
      - input_number.heating_ac_temperature_confort_3
      - input_number.heating_ac_temperature_confort_4
      - input_number.heating_ac_temperature_confort_1_d
      - input_number.heating_ac_temperature_confort_2_d
      - input_number.heating_ac_temperature_confort_3_d
      - input_number.heating_ac_temperature_confort_4_d
      - input_number.heating_ac_temperature_eco
      - input_number.heating_ac_temperature_away
      - binary_sensor.heating_ac_1
      - binary_sensor.heating_ac_2
      - binary_sensor.heating_ac_3
      - binary_sensor.heating_ac_4
      - binary_sensor.heating_ac_1_d
      - binary_sensor.heating_ac_2_d
      - binary_sensor.heating_ac_3_d
      - binary_sensor.heating_ac_4_d
      - input_boolean.presence_ac
      - binary_sensor.life_windows_and_doors_delayed
      - input_boolean.heating_enabled
      - input_boolean.cooling_enabled
      - input_boolean.thermostats_ac_on_off

      schedule:
      - rules:
        - x: "Break() if is_off('input_boolean.thermostats_ac_on_off') else Next()"
        - x: "Abort() if is_off('input_boolean.cooling_enabled') else Next()"
        - rules:
          # CONTRAINTES
          - x: "Break() if is_off('input_boolean.presence_ac') else Next()"
          - x: "Break() if is_on('binary_sensor.life_windows_and_doors_delayed') else Next()"
          
          - rules:
            - x: "Break() if is_on('input_boolean.to_sleep') else Next()"
            - x: "Break() if is_off('binary_sensor.lionel_geo') else Next()"
            # CONFORT SEMAINE
            - x: state("input_number.heating_ac_temperature_confort_1") if (is_on("binary_sensor.heating_ac_1")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_2") if (is_on("binary_sensor.heating_ac_2")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_3") if (is_on("binary_sensor.heating_ac_3")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_4") if (is_on("binary_sensor.heating_ac_4")) else Next()
            # CONFORT WEEK-END & FERIE
            - x: state("input_number.heating_ac_temperature_confort_1_d") if (is_on("binary_sensor.heating_ac_1_d")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_2_d") if (is_on("binary_sensor.heating_ac_2_d")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_3_d") if (is_on("binary_sensor.heating_ac_3_d")) else Next()
            - x: state("input_number.heating_ac_temperature_confort_4_d") if (is_on("binary_sensor.heating_ac_4_d")) else Next()
            # ECO
            - x: Mark(OFF, Mark.OVERLAY) if (is_on("input_boolean.presence_ac")) and (is_on("input_boolean.cooling_enabled")) else Next()
            - x: "Break(2)"
        # REGLES LIES AUX CONTRAINTES
        - x: Mark(OFF, Mark.OVERLAY) if (is_on("input_boolean.to_sleep")) and (is_on("input_boolean.cooling_enabled")) else Next()
        - x: Mark(OFF, Mark.OVERLAY) if (is_off("binary_sensor.lionel_geo")) and (is_on("input_boolean.cooling_enabled")) else Next()

      - x: Mark(OFF, Mark.OVERLAY) if (is_off("input_boolean.thermostats_ac_on_off")) else Next()
      - x: Mark(OFF, Mark.OVERLAY) if (is_off("input_boolean.presence_ac")) else Next()      
      - x: Mark(OFF, Mark.OVERLAY) if (is_on("binary_sensor.life_windows_and_doors_delayed")) else Next()

Les deux modes du climatiseur sont gérés par deux input_boolean: et deux automations de façon à ce qu'un seul mode puisse être ON à un instant t.

  - id: 'd1bdf1ac-4d43-4f04-9b09-03e7a5e9ff59'
    alias: 'CLIMATE - AC Chaud'
    description: ''
    trigger:
      - platform: state
        entity_id: input_boolean.heating_enabled
        to: 'on'
      - platform: state
        entity_id: climate.daikin
        # attribute: hvac_mode
        to: 'heat'
    action:
    - service: input_boolean.turn_off
      target:
        entity_id: input_boolean.cooling_enabled
    - service: input_boolean.turn_on
      target:
        entity_id: input_boolean.heating_enabled
    - service: climate.set_hvac_mode
      target:
        entity_id: climate.daikin
      data:
        hvac_mode: heat

  - id: '8fbf1387-04db-44e8-a210-472e4ff80501'
    alias: 'CLIMATE - AC Froid'
    description: ''
    trigger:
      - platform: state
        entity_id: input_boolean.cooling_enabled
        to: 'on'
      - platform: state
        entity_id: climate.daikin
        # attribute: hvac_mode
        to: 'cool'
    action:
    - service: input_boolean.turn_off
      target:
        entity_id: input_boolean.heating_enabled
    - service: input_boolean.turn_on
      target:
        entity_id: input_boolean.cooling_enabled
    - service: climate.set_hvac_mode
      target:
        entity_id: climate.daikin
      data:
        hvac_mode: cool

On pourrait compléter ces deux automations avec pour chacune 12 input_number.set_value afin de d'adapter les consigne à la saison.... On pourrait

service: input_number.set_value
target:
  entity_id: input_number.heating_ac_temperature_confort_1
data:
  value: 23

Et tant qu'à y être on pourrait aussi déclencher tout ça avec un trigger basé sur la saison ou appliquer un condition...

  trigger:
    - platform: state
      entity_id: sensor.season
      to: summer  # A vérifier, je ne suis pas certain que le "to" passe

# Ou en condition....

  condition:
    - condition: state
      entity_id: sensor.season
      state: summer

Ne cherchez pas à recopier ces bouts de code, il ne sont là que pour expliquer la philosophie, vous trouverez mes sources sur GitHub. (je vais essayer de les tenir à jour).

L'interface

C'est là toute la difficulté, créer une interface simple pour un utilisateur lambda. Personnellement taper dans le YAML de Schedy me va bien, mais pour mon frère que je souhaite autonome dans sa maison il n'en est pas question.

Il ne faut pas que cette interface soit compliquée. J'ai donc choisit de masquer tout ce que n'est pas utile au quotidien. Pour y parvenir j'ai utilisé des cartes conditionnelles, l'intégration Fold-Entity et bien sur Multiple-Entity-Row que j'utilise régulièrement.. Un onglet sera dédié à chaque pièce, onglet dans lequel on trouve deux cartes Vertical Stack. Une première pour les fonctions et informations générales et la seconde ou vont se déplier les plages actives

Outre la visualisation du thermostat graphique, on va retrouver ici des informations telles une fenêtre ouverte ou la puissance de l'appareil :

Et au passage un sensor pour obtenir le graphe des consignes successives :

sensor:
- platform: template
  sensors:
    temp_up_th_ac: # Pour History Graph
      friendly_name: "TH AC"
      value_template: > 
        {% if is_state('climate.daikin', 'heat') %}
          Heat {{state_attr ('climate.daikin', 'temperature')}}°
        {% elif is_state('climate.daikin', 'cool') %}
          Cool {{state_attr ('climate.daikin', 'temperature')}}°
        {% else %}
          Off
        {% endif %}

Ensuite si on déroule les options, on retrouve les consignes ECO et ABSENCE, le retour Schedy n'est là que pour du debug, il nous indique l'information reçue par Schedy. Les 3 commutateurs suivant seront des contraintes générales que nous pourrons utiliser ultérieurement dans Schedy.

On va ensuite déplier le boost. On choisit ici la température et la durée. Un reset est prévu afin d'annuler un boost lancé malencontreusement.

Le boost est géré par un script qui va demander à Schedy de passer outre les planifications prévues pendant la durée choisie. De base j'ai demandé à Schedy de replanifier toute les minutes, en actionnant ce script on va mettre en pause la replanification interne.

script:
  heating_antoine_boost:
    alias: Boost
    icon: mdi:timer-outline
    sequence:
    - event: schedy_set_value
      event_data:
        app_name: schedy_heating
        room: chambre_antoine
        v: '{{ states.input_number.heating_antoine_temperature_boost.state }}'
        rescheduling_delay: '{{ states.input_number.heating_antoine_time_boost.state }}'

La carte suivante de notre stack est consacrée à la présence de l'occupant (avec ses dates d'arrivée et de départ cachées, sachant que ces dates, principalement liées aux chambres des enfants, pourront être gérées autrement (calendrier scolaire, géoloc sur approche, ...) ainsi que le choix des plages à activer pour chaque type de journée :

Ensuite on passe à la Vertical Stack des différentes plages. Il s'agit ici d'une suite de 12 cartes conditionnelles qui ne s'affichent que lorsqu'elle sont choisies dans la carte précédente. J'ai nommé ces plages NUIT, MATIN, MIDI et SOIR, mais c'est purement informel, 1, 2, 3, et 4 aurait pu faire l'affaire et rien n'empêche d'en créer plus ou moins... Si une plage n'est pas activée elle ne sera ni visible, ni fonctionnelle, et quand une plage passe en mode confort un indicateur est présent.

Attention : Je n'ai pas fait de contrôle pour prévenir une éventuelle superposition des plages. On part donc du principe qu'elles sont consécutives (plus pratique visuellement, mais sans conséquence), mais surtout qu'elles ne se superposent pas. Il doit être possible de gérer ça, mais ça me semble bien compliqué...

Déploiement

J'ai déjà expliqué comment installer Schedy dans les articles précédents.

Pour le reste j'ai mis le code sur GitHub. Il y a d'une part le fichier de configuration schedy_heating.yaml qui trouvera sa place dans le répertoire /config/appdaemon et la partie HA avec un fichier global et fichier propre à chaque pièce fait l'objet d'un fichier dans le répertoire /packages. On y trouvera donc heating_global.yaml et heating_antoine.yaml pour cette première pièce.

Pour créer une nouvelle pièce on va dupliquer heating_antoine.yaml en heating_cuisine.yaml par exemple, et ensuite faire un cherche et remplace (case sensitive) dans un éditeur afin de remplacer toutes occurrences antoine par cuisine et Antoine par Cuisine. Et vous l'aurez compris, même punition pour les cartes Lovelace.

Je vous conseille toutefois de bien fignoler et debugger une première pièce avant de déployer les suivantes. J'ai joué la carte du fonctionnel sans trop me préoccuper de l'esthétique, et chaque pièce va comporter des particularités. Inutile par exemple de déployer les fonctions Arrivée / Départ sur les pièces communes. On fait ici du sur mesure, ça sous entend également d'y passer un peu de temps....

Voilà ! Et j'espère ne pas y revenir....

 

Home Assistant : Alertes

Les cordonniers sont bien souvent les plus mal chaussés. Une adage qui se vérifie une fois de plus. J'ai laissé la porte d'un congélateur ouverte et malgré le fait qu'il y ait une sonde connectée à l'intérieur je ne m'en suis aperçu que deux heures plus tard et j'ai du jeter pas mal de choses... Pourquoi ? Simplement parce que je je n'ai jamais pris le temps de configurer les alertes ! Alors on va le faire, rien de sorcier mais il faut le faire !

On par du principe que l'on a des sondes en place. Il faut éviter les Aqara ou Xiaomi sur piles. Si cela convient pour un réfrigérateur, à -18 °C les piles ne tiennent pas très longtemps. J'ai donc opté pour une extension Shelly avec des sondes DS18B20. Après cette source peut être différente mais il faut que la sonde soit déportée. On peut aisément utiliser un câble plat de type téléphone pour passer la porte. On pourait également installer un détecteur d'ouverture, mais ça n'aura aucun intérêt en cas de panne de l'appareil...

On commence par faire des binary_sensor pour gérer les seuil et on recharge les templates sans avoir à redémarrer. On pourrait régler le seuil dynamiquement avec un input_number si on voulait faire genre.... On configure un seuil un peu plus haut afin de ne pas déclencher des faux positifs à chaque ouverture.

- platform: template
  sensors:
    alert_freezer_haier_2:
      value_template: "{{ states('sensor.shelly1_55eaea_temperature_3') | float > -12 }}"
      friendly_name: "Alerte Congelateur Haier 2"

    alert_freezer_haier_1:
      value_template: "{{ states('sensor.shelly1_55eaea_temperature_1') | float > -12 }}"
      friendly_name: "Alerte Congelateur Haier 1"

    alert_fridge_garage:
      value_template: "{{ states('sensor.shelly1_55eaea_temperature_2') | float > 8 }}"
      friendly_name: "Alerte Réfrigérateur Garage"

    alert_fridge_kitchen:
      value_template: "{{ states('sensor.mi_refrigerateur_temp') | float > 9 }}"
      friendly_name: "Alerte Réfrigérateur Cuisine"

On pourrait aussi créer nos binary_sensor avec la plateforme Threshold. C'est plus smart, mais pour tester il me fallait redémarrer On pourrait également s'intéresser à la plateforme Derivative...

- platform: threshold # will switch state not at 0°C/min but 0.1°C/min or -0.1°C/min depending on the current state of the sensor, respectively
  entity_id: sensor.shelly1_55eaea_temperature_3
  upper: -10
  hysteresis: 0.1 # sensor
  name: Temperature rising   

Ensuite il faut gérer les alertes et ça on va le faire avec la plateforme Alert que j'utilise déjà pour les coupures électriques :

fridge_garage:
  name: 'Alerte Réfrigérateur Garage'
  entity_id: binary_sensor.alert_fridge_garage
  state: 'on'
  repeat:
    - 5
    - 10
    - 30
    - 45
    - 60
    - 90
    - 120
    - 240
    - 300
  can_acknowledge: true  # Optional, default is true
  skip_first: true  # Optional, false is the default
  message: "{{ states.sensor.date_time.state}} > Réfrigérateur Garage | Température excessive : {{ states.sensor.shelly1_55eaea_temperature_2.state }}°" 
  done_message: "{{ states.sensor.date_time.state}} > Réfrigérateur Garage | Température normale : {{ states.sensor.shelly1_55eaea_temperature_2.state }}°" 
  notifiers:
    - slack_hass_canaletto
    - Free_Mobile

A partir de là on reçoit une alerte (ici deux) dès lors que la température grimpe. J'ai toutefois éliminé la première alerte afin d'éviter les faux positifs. Et rien n'empêcherait en complément de diffuser une annonce vocale en TTS ou de faire clignoter en rouge quelques ampoules...

 

Home Assistant & Hygrostat

Longtemps j'ai utilisé l'intégration custom Hygrostat pour forcer la VMC lorsque quelqu'un prend une douche. C'est un détournement en ce sens qu'un hygrostat est plutôt fait pour fonctionner de façon contestante, comme par exemple dans une cave à vin. 

La dernière version de Home Assistant (2021-08) apporte dans son lot de nouveautés un hygrostat intégré. J'ai donc naturellement voulut l'utiliser. Mais celui ci, bien que plous évolué, ne me semble pas fait pour travailler sur un écart rapide d'humidité. Hors le niveau d'humidité dans une maison varie en fonction des saisons et de la la météo. Donc, à moins de l'ajuster manuellement tous les jours, si je règle la consigne sur une valeur arbitraire, le résultat sera aléatoire.

Mon idée, il y a surement d'autres façons de faire, est de calculer la moyenne de la valeur humidité sur les 8 heures passées et d'ajuster dynamiquement la consigne de mon hygrostat toutes les heures (ces deux valeurs restent à affiner).

Mise en œuvre

On commence par déclarer l'intégration dans le fichier de configuration (attention à bien supprimer le custom hygrostat si vous l'utilisiez avant car il a le même nom) :

generic_hygrostat:
  - name: "Hygrostat : SdB"
    humidifier: switch.ipx800_7_vmc
    target_sensor: sensor.rpi_mi_h_sdb
    min_humidity: 20
    max_humidity: 80
    target_humidity: 67
    dry_tolerance: 3
    wet_tolerance: 0
    device_class: "dehumidifier"
    min_cycle_duration:
      seconds: 5
    keep_alive:
      minutes: 3
    initial_state: true
    away_humidity: 35
    away_fixed: True
    sensor_stale_duration: 00:15:00

Pour les détails de sa configuration ça se passe ici.

Je ne me suis pas vraiment intéressé aux autres valeurs possibles, mais dans notre cas on veut déshumidifier, donc attention du device class...

Ensuite je vais utiliser l'intégration Statistics afin de calculer la moyenne du taux d'humidité de la salle de bain durant les 8 dernières heures :

    - platform: statistics
      name: 'Humidité Salle de Bain (moyenne)'
      entity_id: sensor.rpi_mi_h_sdb
      sampling_size: 700     
      max_age:
        hours: 8

Et pour terminer je vais créer une petite automation qui va ajuster la consigne en fonction de cette moyenne en ajoutant + 10 :de façon à ce que la VMC se mette en route si l'humidité monte lors d'une douche :

- alias: '000 : Set Hygrostat'
  trigger:
  - platform: time_pattern
    hours: '1'
  action:
  - service: humidifier.set_humidity
    target:
      entity_id: humidifier.hygrostat_sdb
    data_template:
      humidity: "{{ states('sensor.humidite_salle_de_bain_moyenne') | float + 10 }}"

Il ne reste plus qu'à tester... Et surement à affiner les valeurs.

Alternatives

L'objectif étant de forcer la VMC lors d'une douche il y a d'autres alternatives à explorer :

  • Détecter si quelqu'un prend une douche : ça pourrait se faire en détectant le débit de l'eau... un peu compliqué ! Ou encore avec un détecteur de présence dans la cabine de douche. A creuser.
  • Par un scénario : en hiver quand je dis à Alexa que j'ai l'intention d'aller me doucher, elle lance un script qui va lancer une play list sur Sonos et un radiateur soufflant, ensuite elle me dit (c'est bien la seule à répondre à touys mes souhaits !) quand la température est idéale pour me doucher. Je pourrait inclure la VMC.

Enjoy !

PS : idées et corrections bienvenues...