Gestion des horaires de chauffage avec Schedy

Bonjour,

Avec plusieurs têtes de radiateur pilotées (Fibaro FGT-001 et Danfoss 014G0013), cela devenait un enfer de gérer les périodes de chauffe (heat/off) et les températures associées à coup d’Automation et de trigger de type time.
Exemples : chaud tôt le matin dans les SDB mais plus tard le week-end, couper la chauffe en dehors, température dans le bureau, dans les chambres, en journée, la nuit, etc

Ma solution a été d’utiliser Schedy : Schedy - The Concept
Assez complexe de prime abord, pas d’interface de configuration mais ça vaut le coup de s’accrocher.

  • Définition des plages de chauffe avec une syntaxe YAML bien lisible (start: « 07:00 », end: « 20:00 »)
  • Templating pour partager du paramétrage
  • Ré-évaluation des rules à chaque sauvegarde (rechargement automatiquement)
  • Renvoi automatique de la commande au thermostat si le retour n’est pas arrivé ou correct
  • Gestion des erreurs bien claire dans le logs
  • Détection/correction les modifications manuelles (ex: les enfants qui jouent à appuyer sur les boutons)
  • Surveillance de paramètre externe (ex: chez moi j’utilise un input_boolean.homeoffice)
  1. Installer AppDaemon et la package hass-apps

Supervisor > Addons store > appdaemon > install

system_packages: []
python_packages:
- hass-apps
init_commands: []

Save et Start l’add-on AppDaemon
Dans les logs, on peut le voir récupérer la package hass-apps.

  1. Déployer le fichier hass_apps_loader.py
    Dans un terminal
cd /config/appdaemon/apps
wget https://raw.githubusercontent.com/efficiosoft/hass-apps/master/hass_apps_loader.py
  1. Faire sa configuration avec le module thermostat

Avec le File editor, créer un fichier /config/appdaemon/apps/schedy_heating.yaml

Une partie de ma configuration sera plus claire qu’une longue explication :wink:

schedy_heating:
  module: hass_apps_loader
  class: SchedyApp

  actor_type: thermostat
  
  actor_templates:
    default:
      send_retry_interval: 30
      send_retries: 10
  
  watched_entities:
  - input_boolean.homeoffice
  
  expression_environment: |
    def homeoffice():
      return is_on("input_boolean.homeoffice")

  schedule_append:
  - v: "off"
  
  rooms:
    bureau:
      actors:
        climate.danfoss_z_thermostat_014g0013_heating_1:
          supports_hvac_modes: false
          off_temp: 2
          delta: 2
      schedule:
      - rules:
        - weekdays: 1-5
          rules:
          - rules:
            - x: "Next() if homeoffice() else Break()"
            - { v: 22, start: "07:30", end: "08:30" }
            - { v: 21.5, start: "08:30", end: "12:00" }
            - { v: 21, start: "14:00", end: "18:00" }
          - rules:
            - x: "Next() if not homeoffice() else Break()"
            - { v: 20, start: "07:00", end: "20:00" }
        - weekdays: 6-7
          rules:
          - { v: 20, start: "08:30", end: "11:30" }
    sdb_1er:
      actors:
        climate.fibaro_system_fgt001_heat_controller_mode:
      schedule:
      - { v: 22, start: "06:30", end: "08:00", weekdays: 1-5 }
      - { v: 21, start: "17:30", end: "19:30", weekdays: 1-7 }
      - { v: 22, start: "07:30", end: "09:00", weekdays: 6-7 }
    sdb_rdc:
      actors:
        climate.fibaro_system_fgt001_heat_controller_mode_2:
      schedule:
      - { v: 22, start: "05:15", end: "05:40", weekdays: 1-7 }
      - { v: 22, start: "06:45", end: "07:30", weekdays: 1-7 }
      - { v: 21, start: "17:30", end: "19:30", weekdays: 1-7 }

Documentation : Schedy - Tutorial et Fichier exemple

  1. (optionnel) Solution de contournement pour le Fibaro FGT-001 qui ne donne pas de retour d’état après commande

Sans quoi, Schedy va répéter la commande dans l’attente du bonne prise en compte.
Créer une Automation :

- alias: FGT001 rafraichit la temperature de chauffe
  description: ''
  trigger:
  - event_data:
      domain: climate
      service_data:
        entity_id: climate.fibaro_system_fgt001_heat_controller_mode
    event_type: call_service
    platform: event
  - event_data:
      domain: climate
      service_data:
        entity_id: climate.fibaro_system_fgt001_heat_controller_mode_2
    event_type: call_service
    platform: event
  condition: []
  action:
  - delay: '10'
  - data_template:
      entity_id: '{{ trigger.event.data.service_data.entity_id }}'
    service: zwave.refresh_entity

Bonus: si ça intéresse quelqu’un, sur le même principe, Schedy pilote AdGuardHome pour filtrer mon accès Internet aux services de streaming et jeux en ligne selon des plages horaires. Il utilise l’Actor appelé generic2 (Generic Actor Version 2).

2 « J'aime »

@mycanaletto cela pourrait peut-être t’aider concernant https://forum.hacf.fr/t/comment-utiliser-le-trigger-source-dans-une-action/158

Extra ça :v:

Si je résume maintenant tu n as plus qu’à mettre cet input - input_boolean.homeoffice à true et schedy s occupe du reste ?

Après avoir regardé j’ai fait le choix de ne pas utiliser Schedy qui est un composant externe Appdaemon lourd. C’est sérieux, mais ça peut sauter lors d’une mise à jour. Après je sais qu’ils bossent sur un vrai scheduler avec interface graphique.

input_boolean.homeoffice c’est un exemple pour montrer que Schedy peut surveiller un état pour conditionner / ré-évaluer les règles.
Dans mon cas si j’active le bouton « homeoffice » sur mon accueil Home Assistant alors la température devient agréable pour travailler dans le bureau, sinon c’est à l’économie.

il faut que je vois comment intégrer schedy avec Google agenda car mon but est vraiment de me passer un maximum d un « agenda interne »

@pitp2 par rapport à ton post https://forum.hacf.fr/t/gestion-horaire-filtration-piscine/160 tu pourrais utiliser l’« Actor » switch de Schedy

  1. ajouter un input_boolean dans configuration.yml
input_boolean:
  filtration_piscine:
  1. changer le trigger de l’automation qui déclenche le filtrage
    de
    trigger:
  • entity_id: calendar.filtration_piscine
    platform: state
    to: ‹ off ›

vers
trigger:

  • entity_id: input_boolean.filtration_piscine
    platform: state
    to: ‹ off ›
  1. créer un tel fichier /config/appdaemon/apps/schedy_piscine.yaml

Ici filtration_piscine à « off » sauf sur les plages horaires 8-12h et 18-22h

switch_piscine:
  module: hass_apps_loader
  class: SchedyApp
  actor_type: switch
          
  rooms:
    piscine:
      actors:
        calendar.filtration_piscine:
      schedule:
      - v: "on"
        rules:
        - weekdays: 1-7
          rules:
          - { start: "08:00", end: "12:00" }
          - { start: "18:00", end: "22:00" }
      - v: "off"
1 « J'aime »

Heu tu veux te passer d’un Agenda interne pour uniquement passer par Google Agenda ?
Tu l’as deja fait dans ce cas.

Schedy ca a l’air top, mais suis un peu comme @mycanaletto, c’est appdameon, et en cas de maj bye bye eventuellement le chauffage lol

Mais j’ai envie de regarder cela.

A voir s’il existe un scheduler interne, ou alors comme @pitp2 tout passer sur Google Agenda, qui avouons le, ne plante jamais

C’est tout de même excellent, envie de me laisser tenter. Très lisible et

schedule_snippets:
  kids:
  - v: 20
    rules:
    - weekdays: 1-5
      rules:
      - { start: "06:00", end: "07:30" }
      - { start: "15:00", end: "19:00" }
    - weekdays: 6-7
      rules:
      - { start: "07:30", end: "20:00" }

Now, we include that snippet in the schedules of the kids rooms:

schedule:
- x: "IncludeSchedule(schedule_snippets['kids'])"

Quiqu’on en dise, ca a l’air d’etre très complet :

schedy_heating:  # This is our app instance name.
  module: hass_apps_loader
  class: SchedyApp

  actor_type: thermostat

  expression_environment: |
    def heating_mode():
        return state("input_select.heating_mode")

  schedule_snippets:
    kids:
    - v: 20
      rules:
      - weekdays: 1-5
        rules:
        - rules:
          - x: "Next() if heating_mode() != 'All Home' else Break()"
          - { start: "06:00", end: "07:30" }
          - { start: "15:00", end: "19:00" }
        - rules:
          - x: "Next() if heating_mode() == 'All Home' else Break()"
          - { start: "07:30", end: "20:00" }
      - weekdays: 6-7
        rules:
        - { start: "07:30", end: "20:00" }

  watched_entities:
  - input_select.heating_mode

  schedule_prepend:
  - x: "Mark(OFF, Mark.OVERLAY) if not is_empty(filter_entities('binary_sensor', state='on', window_room=room_name)) else Next()"

  schedule_append:
  - v: "OFF"

  rooms:

    living:
      rescheduling_delay: 120
      actors:
        climate.living_1:
        climate.living_2:
      watched_entities:
      - binary_sensor.living_window_1
      schedule:
      - v: 20
        rules:
        - weekdays: 1-5
          rules:
          - rules:
            - x: "Next() if heating_mode() == 'Normal' else Break()"
            - { start: "06:00", end: "07:30" }
            - { start: "15:00", end: "22:30" }
          - rules:
            - x: "Next() if heating_mode() != 'Normal' else Break()"
            - { start: "08:00", end: "23:30" }
        - weekdays: 6-7
          rules:
          - { start: "08:00", end: "23:30" }

    bed:
      rescheduling_delay: 120
      actors:
        climate.bed_1:
      watched_entities:
      - binary_sensor.bed_window_1
      schedule:

    kids1:
      allow_manual_changes: false
      actors:
        climate.kids1_1:
      watched_entities:
      - binary_sensor.kids1_window_1
      schedule:
      - x: "IncludeSchedule(schedule_snippets['kids'])"

    kids2:
      allow_manual_changes: false
      actors:
        climate.kids2_1:
      watched_entities:
      - binary_sensor.kids2_window_1
      schedule:
      - x: "IncludeSchedule(schedule_snippets['kids'])"

@pyg

Est-ce que c’est stable ? Tu n’as pas rencontré de souci lors d emise à jour, car le rythme d emaj est plutot élevé.

Utilisant Home Assistant seulement depuis la version 106, je peux juste dire que sur 3 releases je n’ai pas rencontré de problème lors des mises à jour.

1 « J'aime »

Question :

Comment tu gères Jours fériés, Vacances et Vacances scolaires ?

Sur le même principe qu’avec le booléen homeoffice, en ajoutant des sensors de la platform workday dans le fichier configuration.yml.

binary_sensor:
  - platform: workday
    country: LU
    name: Jours de travail
  - platform: workday
    country: LU
    name: Vacances
    workdays: [holiday]
    excludes: []
    add_holidays: []

Puis dans Schedy :

  expression_environment: |
    def jours_de_travail():
      return is_on("binary_sensor.jours_de_travail")
    def vacances():
      return is_on("binary_sensor.vacances")

Qu’on peut utiliser dans une rules

...
        - rules:
          - x: "Next() if jours_de_travail() else Break()"
          - { v: 21, start: "08:00", end: "18:00" }
1 « J'aime »

Sais tu si on peut utiliser des input_xx dans schedy ? Si oui il devient facile de lui faire une interface…

Oui, les input_select ou input_boolean que Schedy suit avec la directive watched_entities

Mais pas les input_datetime ?

en fait avec n’importe quelle entité du moment que Schedy suit les changements d’état grâce à « watched_entities » et que tu en fais quelque chose dans les tests "- x: " avec ou sans l’aide d’une fonction « def »

2 « J'aime »

J’ai ajouté à la fin de cet article un template intéressant à propos de la gestion des input_datetime à zéro… (pour éviter qu’un start et un stop avec une heure identique soit en 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) }}'

1 « J'aime »

Hello !

Je me suis replongé dans Schedy qui reste un très bon scheduler mais sans interface.

Sais tu si on peu utiliser des input_xx pour ses variables, genre :

schedule_snippets:
  kids:
  - v: input_number.confort
    rules:
    - weekdays: 1-5
      rules:
      - rules:
        - x: "Next() if heating_mode() != 'All Home' else Break()"
        - { start: "input_datetime.start_kids", end: "input_datetime.stop_kids" }

L’idée serait de pouvoir gérer une interface utilisateur adaptée…

EDIT : A priori c’est possible…

Hello,
Je dois avouer que j’ai abandonné Schedy, et même AppDaemon et les Automations au profit de NodeRed (merci @pitp2 pour le conseil). Marre de devoir redémarrer à chaque changement (ok la plupart des reloads sont arrivés depuis), d’éditer à plusieurs endroits (fichiers de config, automations, scripts, blueprints), de perdre les automations en cas de renommage d’entity.
NodeRed apporte la lecture facile/graphique, le debugging puissant (noeud debug), l’export/import de la configuration (ou juste flows, nodes) et bien sûr le rechargement rapide de la configuration.
Il peut même créer des entities à chaud si on veut interagir depuis le Dashboard HA.

La palette Light Scheduler peut facilement être détourner pour gérer les périodes de chauffe.

Exemple

[{« id »:« 77b84be6.5b67e4 »,« type »:« server-events »,« z »:« e9e2de71.9ee88 »,« name »:«  »,« server »:« 7e0e34a0.85669c »,« event_type »:« call_service »,« exposeToHomeAssistant »:false,« haConfig »:[{« property »:« name »,« value »:«  »},{« property »:« icon »,« value »:«  »}],« waitForRunning »:true,« x »:150,« y »:180,« wires »:[[« 832432e4.fd8e5 »]]},{« id »:« 832432e4.fd8e5 »,« type »:« switch »,« z »:« e9e2de71.9ee88 »,« name »:« domain==climate? »,« property »:« payload.event.domain »,« propertyType »:« msg »,« rules »:[{« t »:« eq »,« v »:« climate »,« vt »:« str »}],« checkall »:« true »,« repair »:false,« outputs »:1,« x »:350,« y »:180,« wires »:[[« ee9cbef.b1a814 »]]},{« id »:« ee9cbef.b1a814 »,« type »:« switch »,« z »:« e9e2de71.9ee88 »,« name »:« service==set_temperature? »,« property »:« payload.event.service »,« propertyType »:« msg »,« rules »:[{« t »:« eq »,« v »:« set_temperature »,« vt »:« str »}],« checkall »:« true »,« repair »:false,« outputs »:1,« x »:580,« y »:180,« wires »:[[« fffd13bc.c09bb »]]},{« id »:« fb4330b8.1597d8 »,« type »:« api-call-service »,« z »:« e9e2de71.9ee88 »,« name »:« Refresh temperature »,« server »:« 7e0e34a0.85669c »,« version »:1,« debugenabled »:false,« service_domain »:« zwave »,« service »:« refresh_entity »,« entityId »:«  »,« data »:« {"entity_id":"{{ payload.event.service_data.entity_id }}"} »,« dataType »:« json »,« mergecontext »:«  »,« output_location »:«  »,« output_location_type »:« none »,« mustacheAltTags »:false,« x »:1000,« y »:180,« wires »:[]},{« id »:« fffd13bc.c09bb »,« type »:« delay »,« z »:« e9e2de71.9ee88 »,« name »:«  »,« pauseType »:« delay »,« timeout »:« 5 »,« timeoutUnits »:« seconds »,« rate »:« 1 »,« nbRateUnits »:« 1 »,« rateUnits »:« second »,« randomFirst »:« 1 »,« randomLast »:« 5 »,« randomUnits »:« seconds »,« drop »:false,« x »:800,« y »:180,« wires »:[[« fb4330b8.1597d8 »]]},{« id »:« aa0b4c98.53f6a »,« type »:« light-scheduler »,« z »:« e9e2de71.9ee88 »,« settings »:«  »,« events »:« [{"start":{"dow":1,"mod":420},"end":{"dow":1,"mod":1080}},{"start":{"dow":2,"mod":420},"end":{"dow":2,"mod":1080}},{"start":{"dow":3,"mod":420},"end":{"dow":3,"mod":1080}},{"start":{"dow":4,"mod":420},"end":{"dow":4,"mod":1080}},{"start":{"dow":5,"mod":420},"end":{"dow":5,"mod":1080}},{"start":{"dow":6,"mod":510},"end":{"dow":6,"mod":630}},{"start":{"dow":0,"mod":510},"end":{"dow":0,"mod":630}},{"start":{"dow":6,"mod":810},"end":{"dow":6,"mod":960}},{"start":{"dow":0,"mod":810},"end":{"dow":0,"mod":960}}] »,« topic »:« heating »,« name »:« Temp bureau »,« onPayload »:« 23 »,« onPayloadType »:« num »,« offPayload »:« 18 »,« offPayloadType »:« num »,« onlyWhenDark »:false,« scheduleRndMax »:0,« sunElevationThreshold »:6,« sunShowElevationInStatus »:false,« outputfreq »:« output.statechange.startup »,« x »:130,« y »:100,« wires »:[[« 5f039f02.f27578 »]]},{« id »:« 875b9dfb.0bc838 »,« type »:« api-call-service »,« z »:« e9e2de71.9ee88 »,« name »:« Radiateur fenêtre »,« server »:« 7e0e34a0.85669c »,« version »:1,« debugenabled »:false,« service_domain »:« climate »,« service »:« set_temperature »,« entityId »:« climate.danfoss_heating »,« data »:« {"temperature": {{temperature}}} »,« dataType »:« json »,« mergecontext »:«  »,« output_location »:« payload »,« output_location_type »:« msg »,« mustacheAltTags »:false,« x »:970,« y »:120,« wires »:[]},{« id »:« b16285ce.81f788 »,« type »:« delay »,« z »:« e9e2de71.9ee88 »,« name »:«  »,« pauseType »:« delay »,« timeout »:« 5 »,« timeoutUnits »:« seconds »,« rate »:« 1 »,« nbRateUnits »:« 1 »,« rateUnits »:« second »,« randomFirst »:« 1 »,« randomLast »:« 5 »,« randomUnits »:« seconds »,« drop »:false,« x »:800,« y »:120,« wires »:[[« 875b9dfb.0bc838 »]]},{« id »:« 26d2347b.d02994 »,« type »:« api-current-state »,« z »:« e9e2de71.9ee88 »,« name »:« Not homeoffice? »,« server »:« 7e0e34a0.85669c »,« version »:1,« outputs »:2,« halt_if »:« false »,« halt_if_type »:« bool »,« halt_if_compare »:« is »,« override_topic »:false,« entity_id »:« input_boolean.homeoffice »,« state_type »:« habool »,« state_location »:« payload »,« override_payload »:« msg »,« entity_location »:« data »,« override_data »:« msg »,« blockInputOverrides »:false,« x »:590,« y »:140,« wires »:[[« b16285ce.81f788 »],]},{« id »:« 81068f7c.a8959 »,« type »:« light-scheduler »,« z »:« e9e2de71.9ee88 »,« settings »:«  »,« events »:« [{"start":{"dow":6,"mod":510},"end":{"dow":6,"mod":630}},{"start":{"dow":0,"mod":510},"end":{"dow":0,"mod":630}},{"start":{"dow":6,"mod":810},"end":{"dow":6,"mod":960}},{"start":{"dow":0,"mod":810},"end":{"dow":0,"mod":960}}] »,« topic »:«  »,« name »:« Temp bureau »,« onPayload »:« 23 »,« onPayloadType »:« num »,« offPayload »:« 18 »,« offPayloadType »:« num »,« onlyWhenDark »:false,« scheduleRndMax »:0,« sunElevationThreshold »:6,« sunShowElevationInStatus »:false,« outputfreq »:« output.statechange.startup »,« x »:130,« y »:140,« wires »:[[« d886719b.86d838 »]]},{« id »:« 1e4ecc29.6fddd4 »,« type »:« api-current-state »,« z »:« e9e2de71.9ee88 »,« name »:« Homeoffice? »,« server »:« 7e0e34a0.85669c »,« version »:1,« outputs »:2,« halt_if »:« true »,« halt_if_type »:« bool »,« halt_if_compare »:« is »,« override_topic »:false,« entity_id »:« input_boolean.homeoffice »,« state_type »:« habool »,« state_location »:« payload »,« override_payload »:« msg »,« entity_location »:« data »,« override_data »:« msg »,« blockInputOverrides »:false,« x »:550,« y »:100,« wires »:[[« b16285ce.81f788 »],]},{« id »:« 5f039f02.f27578 »,« type »:« function »,« z »:« e9e2de71.9ee88 »,« name »:« keep target temperature »,« func »:« msg.temperature = msg.payload;\nreturn msg; »,« outputs »:1,« noerr »:0,« initialize »:«  »,« finalize »:«  »,« x »:330,« y »:100,« wires »:[[« 1e4ecc29.6fddd4 »]]},{« id »:« d886719b.86d838 »,« type »:« function »,« z »:« e9e2de71.9ee88 »,« name »:« keep target temperature »,« func »:« msg.temperature = msg.payload;\nreturn msg; »,« outputs »:1,« noerr »:0,« initialize »:«  »,« finalize »:«  »,« x »:350,« y »:140,« wires »:[[« 26d2347b.d02994 »]]},{« id »:« 7e0e34a0.85669c »,« type »:« server »,« name »:« Home Assistant »,« legacy »:false,« addon »:true,« rejectUnauthorizedCerts »:true,« ha_boolean »:« y|yes|true|on|home|open »,« connectionDelay »:true,« cacheJson »:true}]

1 « J'aime »