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 le type de journée :

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 & Planification, Schedy, la suite !

Encore, allez vous me dire ! Oui car d'une part mon dernier article sur Schedy était un peu fouillis, et surtout en relisant les réponses de son auteur à ceux qui tentent de créer une interface, je me suis aperçu que l'approche que j'avais, qui consistait à faire passer à Schedy des plages horaires, n'était pas la plus simple ni la plus adaptée. D'une part il faut transformer des input_time en sensor ou en input_number (les deux seules possibilité pour transmettre à Schedy un horaire de début et de fin de plage sous la forme du nombre de minutes écoulées depuis minuit). C'est lié à son fonctionnement interne, ce n'est pas très pratique. Son auteur n'a pas le temps de faire évoluer, et surtout c'est un barbu qui déteste les interfaces graphiques...

J'ai donc opté pour une autre approche plus simple et plus concise qui va consister à se servir des input_datetime: de début et de fin de plage pour actionner un binary_sensor: qui passera à ON si on se trouve dans la plage.

A partir de là dans Schedy, on change la température de consigne (input_number:) de la la plage, si la plage est activée (input_boolean:) et la période active (binary_sensor:).

Et bien sur on le fait en fonction des contraintes que l'on s'impose en fonction de la situation de chacun (jour de travail, demie journée de travail, week-end ou télétravail), et tout ça pour chacune des pièces que l'on gère auxquelles correspond un thermostat (climate:) ou un groupe de thermostats.

De cette façon on continue à faire une gestion horaire qui correspondra à la majorité des usages, mais avec ce "mode" binaire on peut très bien imaginer une gestion à base de géolocalisation. Par exemple, en été, si un des membres du logement travaille à une distance de 20 Km, on peut imaginer mettre en route la climatisation s'il se rapproche à moins de 10 Km... Le chauffage (convecteurs + climatiseur) ne se gérant pas comme la climatisation (climatiseur seul) j'y reviendrait prochainement.

Le script

Pour plus de facilité on va utiliser un script afin de créer l'ensemble des entités nécessaires. Dans ce script on crée les entités nécessaires à chaque pièce et le nombre de plages que l'on veut gérer chaque jour que l'on multiplie avec le nombre de jours particuliers à gérer ( jours de semaine, le samedi qui est travaillé le matin et les dimanches et fériés par exemple). 

Pour chaque plage on crée (à chaque plage correspond un fichier qui est stocké dans les pakages) :

  • Deux input_datetime: pour le début et la fin de chaque plage,
  • Un binary_sensor: pour savoir si on est dans la plage,
  • Un input_number: pour définir la température de consigne de la plage,
  • Un input_boolean: pour activer ou désactiver la plage.

On va également créer un fichier heating_global.yaml qui va contenir :

  • Un binary_sensor: s'appuyant sur workday: pour savoir si on est un samedi travaillé,
  • Deux input_number: pour définir la température de consigne pour les plages inactives (deux ou trois car ce n'est pas pareil pour une chambre vide ou un climatiseur),
  • Deux input_boolean: un premier pour activer un mode télétravail, et un autre pour activer / désactiver le bazard.

On pense à éditer les lignes 39 pour définir les pièces et 41 pour le nombre de plages par pièces. On est pas obligé de faite tout d'un coup bien sur (et je vous conseille de commencer light).

/bin/bash
test -d /config/packages || mkdir /config/packages
cd /config/packages
cat >heating_global.yaml<<EOF
input_boolean:
  heating_enabled:
    name: Heating Global
    icon: mdi:toggle-switch
  homeoffice:
    name: Home Office
    icon: mdi:toggle-switch
input_number:
  off_temperature:
    name: Off Température
    min: 14
    max: 26
    step: 1.0
    unit_of_measurement: °C
    icon: 'mdi:thermometer-lines'
  off_temperature_ac:
    name: Off Température
    min: 14
    max: 26
    step: 1.0
    unit_of_measurement: °C
    icon: 'mdi:thermometer-lines'
binary_sensor:
  - platform: template
    sensors:
      workday_saturday_working:
        friendly_name: "Samedi travaillé"
        value_template: >
          {% if now().isoweekday() in (5,) and states.binary_sensor.workday_sensor.state == 'on' %}
            true
          {%else%}
            false
          {% endif %}
EOF
for room in hall_ac
do
for period in {1..12}
do
cat >${room}_heating_period_${period}.yaml<<EOF
input_datetime:
  ${room}_heating_period_${period}_start:
    name: "Heating Period ${period} Start Time"
    has_date: false
    has_time: true
  ${room}_heating_period_${period}_end:
    name: "Heating Period ${period} End Time"
    has_date: false
    has_time: true
binary_sensor:
  - platform: template
    sensors:
      ${room}_slot_${period}:
        entity_id: sensor.time
        friendly_name: ${room}_Slot ${period}
        value_template: >
          {% set t = states('sensor.time') %}
          {% set start = states('input_datetime.${room}_heating_period_${period}_start') [0:5] %}
          {% set stop = states('input_datetime.${room}_heating_period_${period}_end') [0:5] %}
          {{ start <= t < stop if start < stop else (start <= t or t < stop) }}
input_number:
  ${room}_heating_period_${period}_temperature:
    name: Heating Period ${period} Temperature
    min: 18
    max: 25
    step: 1
    unit_of_measurement: °C
    icon: 'mdi:thermometer-lines'
input_boolean:
  ${room}_heating_period_${period}:
    name: Heating Period ${period} Enabled
    icon: mdi:toggle-switch
EOF
done
done
exit

On teste la configuration (check configuration) et on recharge ce qui est nécessaire (reload input booleans, reload input date times, reload input numbers, reload templates entities).

Les cartes Lovelace

Ici on a deux approches possibles. Un mode normal ou un mode compact. J'ai déjà évoqué le mode normal, mais quand on 12 plages par pièces on va essayer de compacter la chose. C'est un peu plus fastidieux pour saisir les valeurs, mais en général on ne change pas ces valeurs tous les jours.

type: entities
entities:
  - entity: input_boolean.heating_enabled
    name: Activation AC mode chauffage
  - entity: binary_sensor.heating_enabled
    name: Etat global du chauffage
  - entity: binary_sensor.workday_sensor
    name: Jour de semaine
  - entity: binary_sensor.workday_saturday_working
  - entity: input_boolean.homeoffice
  - entity: input_number.off_temperature
  - entity: schedy_room.schedy_heating_hall_ac
    name: Retour Schedy AC
  - label: SEMAINE
    type: section
  - entities:
      - entity: binary_sensor.hall_ac_slot_1
        name: false
      - entity: input_number.hall_ac_heating_period_1_temperature
        name: false
        unit: °
        format: precision1
      - entity: input_datetime.hall_ac_heating_period_1_start
        name: false
        format: relative
      - entity: input_datetime.hall_ac_heating_period_1_end
        name: false
    entity: input_boolean.hall_ac_heating_period_1
    name: Plage 1
    show_state: false
    toggle: false
    icon: 'mdi:calendar-range'
    type: 'custom:multiple-entity-row'
    state_color: true
# >>> Ici les autres avec un séparateur...
show_header_toggle: false
state_color: true
theme: teal
title: Schedy AC
footer:
  type: graph
  entity: sensor.daikin_inside_temperature
  hours_to_show: 24

Et voici le résultat...

Schedy

S'agissant de la configuration de Schedy il a bien sur fallut l'adapter. Vous dire que j'ai tout compris serait un mensonge et il y a certainement moyen de simplifier, et je suis preneur d'idées. Voici donc ce que ça donne (j'ai volontairement laissé quelques commentaires).

schedy_heating:
  module: hass_apps_loader
  class: SchedyApp

  actor_type: thermostat
  actor_templates:
    default:
      send_retry_interval: 15
      send_retries: 20
      supports_hvac_modes: true
      off_temp: 17
  
  watched_entities:
  - input_boolean.homeoffice
  - binary_sensor.workday_sensor
  - binary_sensor.workday_saturday_working
  - input_boolean.heating_enabled

  schedule_prepend:
  - x: "14 if is_off('input_boolean.heating_enabled') else Next()" # Le HorsGel... Possible aussi de créer un input_number...
  
  rooms:
    hall_ac:
      allow_manual_changes: true  # On autorise le changement manuel de consigne ou le passage en off
      rescheduling_delay: 90      # Délais après lequel Schedy repassera sur les valeurs prédéfinies
      actors:
        climate.thermostat_x:
          template: default
      watched_entities:
      # COMMON
      - input_boolean.homeoffice
      - binary_sensor.workday_sensor
      - binary_sensor.workday_saturday_working
      - input_number.off_temperature
      # ROOM SPECIFIC
      - input_number.hall_ac_heating_period_1_temperature
      - input_boolean.hall_ac_heating_period_1
      - binary_sensor.hall_ac_slot_1
      - input_number.hall_ac_heating_period_2_temperature
      - input_boolean.hall_ac_heating_period_2
      - binary_sensor.hall_ac_slot_2
      - input_number.hall_ac_heating_period_3_temperature
      - input_boolean.hall_ac_heating_period_3
      - binary_sensor.hall_ac_slot_3
      - input_number.hall_ac_heating_period_4_temperature
      - input_boolean.hall_ac_heating_period_4
      - binary_sensor.hall_ac_slot_4
      schedule:
      - rules:
        # WORKDAY
        - rules:
          - x: "Next() if is_on('binary_sensor.workday_sensor') else Break()"
          - x: >
              state("input_number.hall_ac_heating_period_1_temperature")
              if (is_on("input_boolean.hall_ac_heating_period_1") and (is_on("binary_sensor.hall_ac_slot_1")))
              else Next()
          - x: >
              state("input_number.hall_ac_heating_period_2_temperature")
              if (is_on("input_boolean.hall_ac_heating_period_2") and (is_on("binary_sensor.hall_ac_slot_2")))
              else Next()
          - x: >
              state("input_number.hall_ac_heating_period_3_temperature")
              if (is_on("input_boolean.hall_ac_heating_period_3") and (is_on("binary_sensor.hall_ac_slot_3")))
              else Next()
          - x: "Break(2)"
        # WEEK-END
        - rules:
          - x: "Next() if is_off('binary_sensor.workday_sensor') else Break()"        
          - x: >
              state("input_number.hall_ac_heating_period_4_temperature")
              if (is_on("input_boolean.hall_ac_heating_period_4") and (is_on("binary_sensor.hall_ac_slot_4")))
              else Next()
          - x: "Break(2)"

      # - v: 13 # On remplace par un input_number >>> Attention : v = valeur, x = expression
      - x: state("input_number.off_temperature") # Consigne entre les plages

A suivre...

Il existe dans Schedy un système d'events qui vont permettre de créer des dérogations ponctuelles. Par exemple un bouton ou une commande vocale quand je vais aller prendre une douche qui va lancer une dérogation pour augmenter sensiblement la température de la salle de bain pendant un temps défini ou jusqu'à ce qu'on lui envoie un contre ordre. On peu aussi imaginer chauffer une chambre d'amis lorsqu'on reçoit ou le boudoir lors d'une galante visite (encore que ces temps-ci la fonction ne servirait pas à grand chose...).

Et bien sur la gestion des ouvertures est gérée pour couper le chauffage... Plus tard...

Pour en finir...

Dans cet usage Schedy n'est plus vraiment le planificateur mais un moteur qui va surveiller (re planification) les thermostats et leur donner les bons ordres en fonction des évènements, états  et ordres venus de Home Assistant. Si Schedy peut également gérer des lampes (light:) et des actionneurs (switch:) c'est sans internet, sauf peut être pour superviser la machinerie d'une piscine...

 

Home Assistant & Notifications d'état

Avant, je planifiait mon chauffage avec des automations qui s'appuyaient sur des input_date, input_time pour actionner et input_number pour sélectionner la température de consigne souhaitée et je notifiait dans un journal sur Slack, ce qui me permet un debug facile. J'en avait largement parlé ici, mais maintenant il existe un vrai scheduler et je vais changer ma façon de faire. Sauf que ce scheduler, dont je vous parlerais bientôt, ne dispose pas d'une option de notification. Ca viendra peut être, et ce travail sera peut être à jeter, bien que le principe puisse être adapté à d'autre notifications.

Le scheduler pilotant dans mon cas les thermostats, je vais donc me baser sur les changement d'était du thermostat pour notifier l'état à Slack (voire faire autre chose...). Pour cela je vais stocker les états qui ont changés dans des input_number et des input_text et m'en servir pour notifier avec les bonnes variables.

La liste des courses

J'ai essayé de simplifier en minimisant les besoins, mais il nous faut des input_number: et des input_text:

input_number
  trigger_source_notify_temp_current:
    name: Température Ambiante
    mode: box
    min: 0
    max: 30
    unit_of_measurement: "°C"

  trigger_source_notify_temp_target:
    name: Température de consigne
    mode: box
    min: 0
    max: 30
    unit_of_measurement: "°C"

input_text:
  trigger_source_notify_name:
    name: Trigger Source Name

  trigger_source_notify_hvac_mode:
    name: Trigger Source HVAC Mode

  trigger_source_notify_hvac_action:
    name: Trigger Source HVAC Action
  
  trigger_source_notify_preset_mode:
    name: Trigger Source Preset Mode

  trigger_source_notify_state:
    name: Trigger Source State

Ces "inputs" vont servir à stocker temporairement les valeurs que je vais afficher dans mes notifications.

Une seule automation

Ensuite on va avoir besoin d'une automation qui va :

  1. Se déclencher (trigger) sur les changements d'état pour lesquels on souhaite envoyer une notification. Ici les attributs de deux thermostats.
  2. Ecrire (action) les valeurs liées à la source de déclenchement dans les input_number: et input_text:
  3. Notifier (action) en se servant des valeurs écrite précédemment.
- alias: Notifications des Thermostats
  trigger:
  - platform: template
    value_template: "{{ state_attr('climate.daikin','temperature')  }}"
  - platform: template
    value_template: "{{ state_attr('climate.daikin','preset_mode')  }}"
  - platform: template
    value_template: "{{ state_attr('climate.daikin','hvac_modes')  }}"

  - platform: template
    value_template: "{{ state_attr('climate.thermostat_antoine','temperature')  }}"
  - platform: template
    value_template: "{{ state_attr('climate.thermostat_antoine','preset_mode')  }}"
  - platform: template
    value_template: "{{ state_attr('climate.thermostat_antoine','hvac_modes')  }}"

  action:
  - data_template:
      entity_id: input_number.trigger_source_notify_temp_target
      value: '{{ trigger.to_state.attributes.temperature }}'
    service: input_number.set_value
  - data_template:
      entity_id: input_number.trigger_source_notify_temp_current
      value: '{{ trigger.to_state.attributes.current_temperature }}'
    service: input_number.set_value
  - data_template:
      entity_id: input_text.trigger_source_notify_name
      value: '{{ trigger.to_state.attributes.friendly_name }}'
    service: input_text.set_value
  - data_template:
      entity_id: input_text.trigger_source_notify_hvac_action
      value: '{{ trigger.to_state.attributes.hvac_action }}'
    service: input_text.set_value
  - data_template:
      entity_id: input_text.trigger_source_notify_preset_mode
      value: '{{ trigger.to_state.attributes.preset_mode }}'
    service: input_text.set_value
  - data_template:
      entity_id: input_text.trigger_source_notify_state
      value: '{{ trigger.to_state.state }}'
    service: input_text.set_value

  - service: notify.slack_hass_canaletto
    data:
      message: "{{ states.sensor.date_time.state}} > {{ states.input_text.trigger_source_notify_name.state }} : Consigne à {{ states.input_number.trigger_source_notify_temp_target.state }}° |  
                Température ambiante : {{ states('input_number.trigger_source_notify_temp_current') }}° | 
                Etat : {{ states.input_text.trigger_source_notify_hvac_action.state }} / {{ states.input_text.trigger_source_notify_preset_mode.state }} / {{ states.input_text.trigger_source_notify_state.state }}

Et on obtient une notification dans le journal Slack

2020-12-06, 20:24 > Thermostat : Antoine : Consigne à 13.0° | Température ambiante : 17.1° | Etat : idle / away / heat

Bonus

On peut s'amuser à traduire les messages d'état et ainsi les insérer en clair et en français dans la notification... En bricolant quelque chose du genre :

  {% if is_state('input_text.trigger_source_notify_hvac_action', 'heating') %}
    Etat : Chauffe
  {%-elif is_state('input_text.trigger_source_notify_hvac_action', 'cooling') %}
    Climatise
  {%-elif is_state('input_text.trigger_source_notify_hvac_action', 'dry') %}
    Désumidifie
  {%-elif is_state('input_text.trigger_source_notify_hvac_action', 'fan_only') %}
    Ventile
  {%-elif is_state('input_text.trigger_source_notify_hvac_action', 'heat_cold') %}
    Auto
  {% else %}
    Etat : Inactif
  {% endif %}

  {% if is_state('input_text.trigger_source_notify_preset_mode', 'away') %}
    Préréglage : Absent
  {%-elif is_state('input_text.trigger_source_notify_preset_mode', 'eco') %}
    Préréglage : Eco
  {%-elif is_state('input_text.trigger_source_notify_preset_mode', 'boost') %}
    Préréglage : Boost
  {% else %}
    Préréglage : Normal
  {% endif %}

  {% if is_state('input_text.trigger_source_notify_state', 'heat') %}
    Mode : Chauffage
  {%-elif is_state('input_text.trigger_source_notify_state', 'cold') %}
    Mode : Climatisation
  {%-elif is_state('input_text.trigger_source_notify_state', 'dry') %}
    Mode : Désumidifiation
  {%-elif is_state('input_text.trigger_source_notify_state', 'fan_only') %}
    Mode : Ventilation
  {%-elif is_state('input_text.trigger_source_notify_state', 'heat_cold') %}
    Mode : Auto
  {% else %}
    Mode : Arrêt
  {% endif %}

Voilà, bien entendu tout ça est adaptable à d'autres besoins....

 

Home Assistant & Planification

ARTICLE EN COURS d'ECRITURE, N’HÉSITEZ PAS A REVENIR...

 

Comme je l'ai déjà dit une des forces de Jeedom était son agenda, ici il n'y en a pas pour l'instant. Il existe bien Schedy mais j'ai le choix de ne pas y recourir. J'avais commencé à ébaucher la chose dans l'article sur le chauffage, on va continuer ici de façon plus poussée, sachant que si je poursuit avec mes thermostats, cette partie pourra s'appliquer à d'autres choses comme l’arrosage ou la gestion d'une piscine par exemple.

Si on utilise Home Assistant tout seul et que l'on maîtrise son code, dans l'absolu il n'y a pas besoin de ce que je vais expliquer. Par contre si on déploie pour un tiers, ou si l'ont veut que toute la famille puisse ajuster le fonctionnement, il vaut mieux penser à une interface graphique.

J'ai donc commencé à dessiner cette interface et ensuite je me suis attaqué au codage correspondant. Dans ce projet j'ai plusieurs thermostats à gérer, il suffira simplement de dupliquer le premier ainsi que toutes les ressources qui lui sont nécessaire. Je vous conseille de bien finaliser le premier thermostat, de tester dans tous les sens avant de de dupliquer.

Dans le cas présent je vais travailler sur le HA de mon frère afin de remplacer son vieux Jeedom, il me faut donc à minima que le résultat final ne souffre d'aucune régression et si possible que ça apporte plus de confort et de facilité d'usage. Par exemple sous Jeedom il devait aller changer ses consignes manuellement, jouer avec l'agenda, facile pour moi mais pas vraiment à la portée de l'utilisateur final. L'objectif ici c’est de lui limiter l'accès, mais qu'il puisse pour autant faire tout ce qui a été défini lui même. En plus il m'a collé un challenge, faire en sorte que sa salle de bain soit à la bonne température quand son réveil sonne. Sauf que son réveil est un EchoShow 5 avec Alexa, qu'il change l'heure avant de se coucher et qu'il n’est pas question de payer pour la passerelle Alexa officielle... A Suivre !

On ne parlera pas ici de capteur de température, ni d'actionneur, ça a déjà été évoqué dans d'autres articles.

On va gérer 3 journée type, les jours de semaine, le samedi car il travaille le samedi matin, et le dimanche ou jour férié. L’application des conditions utilisera le sensor Workday afin de pouvoir affecter dynamiquement les jours fériés.

Liste des courses

    • des input_datetime: un START un STOP afin de définir le début et la fin d'une plage horaire.
      # input_datetime: SEMAINE - THERMOSTAT SEJOUR
       sejour_start_semaine_matin:
       has_date: false
       has_time: true
       sejour_stop_semaine_matin:
       has_date: false
       has_time: true
       sejour_start_semaine_midi:
       has_date: false
       has_time: true
       sejour_stop_semaine_midi:
       has_date: false
       has_time: true
       sejour_start_semaine_soir:
       has_date: false
       has_time: true
       sejour_stop_semaine_soir:
       has_date: false
       has_time: true
      ...
    • des input_number: un par consigne de température. Afin de simplifier je suis parti sur des consignes globales, mais il est tout à fait possible d'avoir des consignes indépendantes pour chaque thermostat à gérer. Je n'ai pas de Hors Gel car celui ci est géré statiquement au niveau de chaque thermostat. J'ai ajouté Confort+ et Boost, le mode Boost servira essentiellement à surchauffer la salle de bain, comme on le verra plus loin.
      consigne_confort_plus:
       name: Confort +
       min: 14
       max: 24
       step: 0.1
       unit_of_measurement: °C
      ...
    • des input_boolean: un par plage horaire, ils serviront de drapeau pour chaque plage horaire si on souhaite activer la consigne Confort+ ou Boost pour une plage spécifique.
      boost_sejour_semaine_matin:
      boost_sejour_semaine_midi:
      boost_sejour_semaine_soir:
      ...
    • des binary_sensor: un par plage horaire, que je vais utiliser sur les conseils d'un collègue afin de convertir une heure de début de plage horaire, les utiliser en trigger: et récupérer celui qui est la source du déclenchement afin d'y affecter un flag pour activer le mode Confort+ ou Boost à une plage spécifique. On trouve également ici workday: pour gérer les jours fériés.
      - platform: template
        sensors:
          sejour_start_semaine_matin:
          friendly_name: 'Séjour : Start Semaine Matin'
          delay_off:
            minutes: 2
          value_template: >-
            {%- if states.sensor.time.state == states.input_datetime.sejour_start_semaine_matin.state[0:5] %}
              true
            {% else %}
             false
           {%- endif %}
      ...
      - platform: workday
        country: FRA
        workdays: [mon, tue, wed, thu, fri]
        excludes: [sun, holiday]
        add_holidays:
        - '2020-02-24'
      
    • des sensor: en fait surtout time_date: que l'on va utiliser pour comparer la date et l'heure courante avec nos interactions. J'y ai ajouté Season pour éviter de chauffer en été (...joke).

      - platform: time_date
        display_options:
        - 'time'
        - 'date'
        - 'date_time'
        - 'date_time_utc'
        - 'date_time_iso'
        - 'time_date'
        - 'time_utc'
        - 'beat'
      - platform: season

Définitions des cartes

On commence par le code de la carte entities:. Ce thermostat gère 3 convecteurs dans une pièce ouverte. La carte reprend également les détecteurs d'ouverture qui serviront à désactiver le thermostat quand on aère... (j'ai volontairement coupé le code sur la partie semaine).

entities:
  - entity: climate.thermostat_sejour
  - entity: switch.ipx800_1_sejour
  - entity: switch.ipx800_5_cuisine
  - entity: switch.ipx800_4_hall
  - entity: binary_sensor.window_door_sensor
  - label: SEMAINE
    type: section
  - entities:
      - entity: input_boolean.boost_sejour_semaine_matin
        name: false
        toggle: true
      - entity: input_datetime.sejour_start_semaine_matin
        name: false
      - entity: input_datetime.sejour_stop_semaine_matin
        name: false
    entity: input_fake
    name: Matin
    show_state: false
    type: 'custom:multiple-entity-row'
  - entities:
      - entity: input_boolean.boost_sejour_semaine_midi
        name: false
        toggle: true
      - entity: input_datetime.sejour_start_semaine_midi
        name: false
      - entity: input_datetime.sejour_stop_semaine_midi
        name: false
    entity: input_fake
    name: Midi
    show_state: false
    type: 'custom:multiple-entity-row'
  - entities:
      - entity: input_boolean.boost_sejour_semaine_soir
        name: false
        toggle: true
      - entity: input_datetime.sejour_start_semaine_soir
        name: false
      - entity: input_datetime.sejour_stop_semaine_soir
        name: false
    entity: input_fake
    name: Soir
    show_state: false
    type: 'custom:multiple-entity-row'
show_header_toggle: false
state_color: true
theme: teal
title: Thermostat Séjour
type: entities

Automation

Pour gérer l'ensemble des plages (3 ici) d'une journée type il nous faut 1 automation START et une automation STOP. On pourrait bien sur utiliser un delay:, mais ce n'est vraiment pas conseillé car ça obligerait l'automation à rester active et ça résisterait mal à une coupure / redémarrage (encore que...). J'aurais pu choisir de gérer toutes les journées type dans une même automation, mais ça voudrait dire de se passer des conditions: pour tout gréer dans un monstrueux template: dans action:. Simplifions sans complexifier.

Automation Start Confort Séjour Semaine :

  • Un bloc trigger: avec 3 binary_sensor:
  • Un bloc condition: en mode and avec :
    • Un input_boolean: qui va déterminer si le chauffage est ON ou OFF dans la maison.
    • Un input_boolean: qui va déterminer le mode absent (auquel cas les thermostats sont en mode hors gel et on ne poursuit pas). Le mode absent peut être géré manuellement ou par une règle liée de géolocalisation.
    • Une condition time: pour poursuivre uniquement les jours de semaine dans le cas de cette journée type.
    • Une condition workday: afin de s'assurer que l'on n'est pas en présence d'un jour férié.
  • Un bloc action avec :
    • Un sevice: basé sur un data_emplate: pour affecter la bonne consigne de température de Confort ou Confort+ si la plage horaire comporte ce flag.
    • Un notify: basé sur un data_emplate: de notification que je redirige vers un canal Slack et qui constituera le journal de mon thermostat.
- alias: '100 : START CONFORT Séjour Semaine'
  description: 'Confort et Confort+ Séjour Semaine sauf jours fériés'
  trigger:
    - entity_id: binary_sensor.sejour_start_semaine_matin
      platform: state
      to: 'on'
    - entity_id: binary_sensor.sejour_start_semaine_midi
      platform: state
      to: 'on'
    - entity_id: binary_sensor.sejour_start_semaine_soir
      platform: state
      to: 'on'

  condition:
    condition: and
    conditions:
      - condition: state
        entity_id: input_boolean.thermostats_on_off
        state: 'on'
      - condition: state
        entity_id: input_boolean.thermostats_away
        state: 'off'

      - condition: time
        weekday:
          - mon
          - tue
          - wed
          - thu
          - fri

      - condition: state
        entity_id: binary_sensor.workday_sensor
        state: 'on'

  action:
  - service: climate.set_temperature
    entity_id: climate.thermostat_sejour
    data_template:
      temperature: '{% if trigger.entity_id == "binary_sensor.sejour_start_semaine_matin" and is_state("input_boolean.boost_sejour_semaine_matin", "on") %}
                      {{ states.input_number.consigne_confort_plus.state }}
                    {% elif trigger.entity_id == "binary_sensor.sejour_start_semaine_midi" and is_state("input_boolean.boost_sejour_semaine_midi", "on") %}
                      {{ states.input_number.consigne_confort_plus.state }}
                    {% elif trigger.entity_id == "binary_sensor.sejour_start_semaine_soir" and is_state("input_boolean.boost_sejour_semaine_soir", "on") %}
                      {{ states.input_number.consigne_confort_plus.state }}
                    {% else %}
                      {{ states.input_number.consigne_confort.state }}
                    {% endif %}'

  - service: notify.slack_hass_canaletto
    data_template:
      message: >
        {% if trigger.entity_id == "binary_sensor.sejour_start_semaine_matin" and is_state("input_boolean.boost_sejour_semaine_matin", "on") %}
          {{ states.sensor.date_time.state}} > Confort+ ({{ states.input_number.consigne_confort_plus.state }}°), Séjour : {{ states('sensor.mi_t_clock') }}°, Extérieur : {{ states('sensor.ext_temperature') }}°
        {% elif trigger.entity_id == "binary_sensor.sejour_start_semaine_midi" and is_state("input_boolean.boost_sejour_semaine_midi", "on") %}
          {{ states.sensor.date_time.state}} > Confort+ ({{ states.input_number.consigne_confort_plus.state }}°), Séjour : {{ states('sensor.mi_t_clock') }}°, Extérieur : {{ states('sensor.ext_temperature') }}°
        {% elif trigger.entity_id == "binary_sensor.sejour_start_semaine_soir" and is_state("input_boolean.boost_sejour_semaine_soir", "on") %}
          {{ states.sensor.date_time.state}} > Confort+ ({{ states.input_number.consigne_confort_plus.state }}°), Séjour : {{ states('sensor.mi_t_clock') }}°, Extérieur : {{ states('sensor.ext_temperature') }}°
        {% else %}
          {{ states.sensor.date_time.state}} > Confort ({{ states.input_number.consigne_confort.state }}°), Séjour : {{ states('sensor.mi_t_clock') }}°, Extérieur : {{ states('sensor.ext_temperature') }}°
        {% endif %}

Améliorations à envisager : EJP ou Tempo à gérer dans le template d'affection des consignes afin de réduire la température de 1° les jours rouges et de démontrer ainsi la nécessité de la domotique. Prévoir un boolean global afin d'activer ou pas cette option.

Automation Stop Confort Séjour Semaine :

  • Un bloc trigger: avec 3 templates afin de récupérer les heures STOP. Inutile de créer des binary_sensor ici car on n'a pas besoin de connaitre la source de déclenchement dans action: ou on se contenter d'affecter la consigne Eco dans tous les cas.
  • Un bloc condition: ou l'on reprend les mêmes conditions que pour START.
  • Un bloc action: avec
    • Une action ou on défini la consigne de température Eco.
    • Une action de notification pour le journal.

INSERT CODE

Autre méthode

En fait la méthode classique consiste à créer un trigger: basé sur un template qui va comparer une ou plusieurs fois l'heure locale avec l'heure de l'input_datetime:

  trigger:
  - platform: template
    value_template: '{{ states.sensor.time.state == states.input_datetime.pompe_stop_p1.state[0:5] }}'
  - platform: template
    value_template: '{{ states.sensor.time.state == states.input_datetime.pompe_stop_p2.state[0:5] }}'

Ça fonction très bien, mais imaginons que nous ayons défini plusieurs plages et qu'on décide de ne pas utiliser certaines.

Si on les laisse bêtement à 00:00 00:00 on pourrait imaginer qu'il ne se passe rien. En fait si, on se retrouve avec un ON/OFF et parfois même un OFF/ON ce qui est plus gênant. Pour y remédier, et ça m'a pris un peu de temps car je n'ai aucune notion de développement, j'ai fini par améliorer mon template en associant une condition AND, en gros, si l'heure locale est égale à l'heure définie ET si l'heure définie pour le START est différente de l'heure définie en STOP, alors TRUE...

  trigger:
  - platform: template
    value_template: '{{ (states.sensor.time.state == states.input_datetime.pompe_stop_p1.state[0:5]) and (states.input_datetime.pompe_start_p1.state != states.input_datetime.pompe_stop_p1.state) }}'
  - platform: template
    value_template: '{{ (states.sensor.time.state == states.input_datetime.pompe_stop_p2.state[0:5]) and (states.input_datetime.pompe_start_p1.state != states.input_datetime.pompe_stop_p2.state) }}'

Alternatives

Home Asssitant ne dispose toujours pas du véritable agenda qui fait la force de Jeedom. En général je préfère le faire à la main ce qui permet de réaliser exactement ce que l'on souhaite. Mais j'ai trouvé deux embryons d'agenda. Pour l'instant ça ne va pas très loin, mais ça permettra de planifier des choses simples sans se plonger dans le code...

Conclusion

Voilà, il ne vous reste plus qu'à dupliquer ça à l'infini pour gérer autant de journées type, plages horaires et thermostats. Merci à Phillip et Mathieu dans notre fil Telegram pour leurs idées et aide, notamment sur les templates qui m'ont pris pas mal de temps.

Sources