[DefiDIY25] Domotiser vos équipements radiocommandés

Vous possédez des équipements pilotés par des télécommandes sans fils ? Il est très probable de pouvoir les piloter depuis Home Assistant, à l’aide d’un ESP/Pico à travers ESPHome.

Objectif :

Domotiser et piloter vos équipements télécommandés sans fils pour moins de 5€.

Avantages :

  • Permet de se passer de l’achat d’une box constructeur supplémentaire, souvent à cout élevé

  • Si le constructeur ne propose pas de box domotique, permet de domotiser l’équipement, à moindre frais

  • Totalement local, ne passe pas par le cloud

  • Permet d’éviter l’ajout d’un boitier tiers

    • Contact sec pour les portails, garages, PAC, Poele, Alarme, …

    • Module volets roulants

Inconvénients :

  • Risque de détérioration de la télécommande

  • Irréversible sauf grande précaution

  • Pas de retour d’état (quoi que …)

Mon cas :

Mon domicile est équipé de 14 volets, avec chacun une télécommande sans fils, et aussi une télécommande générale.

Le constructeur ne propose pas de box.

Je ne voulais pas laisser « trainer » 14 télécommandes à proximité de chaque volets, ni courir après une télécommande générale.
Je voulais pouvoir gérer mes volets à distance, pour gérer le froid, le chaud, simuler une présence, …

Défi :

C’était mon premier projet HA + électronique + Esphome, en septembre 2023.

Matériels :

Une télécommande (multi channel, pour gérer tous les volets de façon autonome mais d’un unique matériel)

Un Pi Pico

Montage :

Démonter la télécommande et la mettre à nue.

Retirer la pile, et souder la sortie 3V3 et GND du Pico sur la télécommande, ainsi ils possèdent les mêmes références.

Analyser le mode de fonctionnement des GPIO de la télécommande.

Ici en l’occurrence, les boutons mettent au potentiel GND le GPIO du µC. Au repos, ils sont en pull-up à 3V3.

Je vais donc venir souder mes sorties GPIO du Pico, aux entrées GPIO du µC télécommande, en mode pull-up et mise à GND quand activée.

L’utilisation du « OPEN DRAIN » aurait été préférable, mais cette option n’est pas disponible sur le Pico (uniquement ESP).

Ma télécommande possède 6 boutons :

Channel plus → permet d’incrémenter la channel pilotée (grosso modo, l’identifiant du volet)

Channel moins → permet de décrémenter la channel pilotée (grosso modo, l’identifiant du volet)

Mode → Basculer de mode Manuel / Séquentiel

Haut → Commande monter du volet sélectionné

Stop–> Commande arrêt du volet sélectionné

Bas–> Commande descendre du volet sélectionné

Code :

Toute la difficulté réside dans le bon choix de la channel.

Si je viens de piloter le volet #1 et qu’ensuite je souhaite piloter le volet #13, il faut d’abord appuyer 12 fois sur le bouton « channel plus » puis sur « bas ».

Ensuite, il faut interdire à l’utilisateur de piloter un volet si un autre est en cours, car la gestion de la channel est purement prédictive, sans retour réel de la télécommande. D’où le booléen « dispo » dans le code.

#Pi Pico en direct sur µC volets
esphome:
  name: pico-volets
  friendly_name: pico_volets
  project:
    name: "scorpix.volets"
    version: "3.0.0"

button:
  - platform: output
    name: "Volet +"
    id: bouton_volet_plus
    output: gpio_bouton_volet_plus
    internal: True
    duration: 80ms
    on_press: 
        - number.increment: channelVolet
  
  - platform: output
    name: "Volet -"
    id: bouton_volet_moins
    output: gpio_bouton_volet_moins
    internal: True
    duration: 80ms
    on_press: 
        - number.decrement: channelVolet

  - platform: output
    name: "Volet manuel"
    id: bouton_volet_manuel
    output: gpio_bouton_volet_manuel
    duration: 80ms
  
output:
  - platform: gpio
    pin:
      number: 9
      inverted: true
    id: gpio_bouton_volet_plus

  - platform: gpio
    pin:
      number: 8
      inverted: true
    id: gpio_bouton_volet_moins

  - platform: gpio
    pin:
      number: 11
      inverted: true
    id: gpio_bouton_volet_manuel

number:
  - platform: template
    name: "Channel volet"
    icon: "mdi:identifier"
    id: channelVolet
    optimistic: true
    restore_value: true
    internal: False
    min_value: 1
    max_value: 16
    step: 1
        
script:
  - id: checkChannel
    parameters:
      channel: float
    then:
      - repeat:
          count: !lambda return abs(channel);
          then:
            - logger.log:
                format: "Delta channel est de %.1f"
                args: [ 'channel' ]
            - lambda: |-
                if (channel>0) {
                return id(bouton_volet_moins).press();
                } else {
                return id(bouton_volet_plus).press();
                }
            - delay: 120ms

globals:
  - id: dispo
    type: bool
    restore_value: no
    initial_value: 'true'

switch:
  - platform: gpio
    name: "Volet up"
    id: bouton_volet_up
    pin: 
      number: 7
      inverted: true
    internal: True
    on_turn_on:
      - delay: 700ms
      - switch.turn_off: bouton_volet_up
    on_turn_off: 
      - globals.set:
          id: dispo
          value: 'true'

  - platform: gpio
    name: "Volet stop"
    id: bouton_volet_stop
    pin: 
      number: 10
      inverted: true
    internal: True
    on_turn_on:
      - delay: 700ms
      - switch.turn_off: bouton_volet_stop
    on_turn_off: 
      - globals.set:
          id: dispo
          value: 'true'

  - platform: gpio
    name: "Volet down"
    id: bouton_volet_down
    pin: 
      number: 6
      inverted: true
    internal: True
    on_turn_on:
      - delay: 700ms
      - switch.turn_off: bouton_volet_down
    on_turn_off: 
      - globals.set:
          id: dispo
          value: 'true'


cover:
  - platform: template
    name: "Salon Est"
    device_class: shutter
    id: voletEst
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-1);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-1);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-1);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Salon Sud"
    device_class: shutter
    id: voletSud
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-2);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-2);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-2);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Bowindow gauche"
    device_class: shutter
    id: voletBowGauche
    assumed_state: True
    optimistic: true
    open_action:
        - if:
            condition:
              lambda: 'return id(dispo);'
            then:
              - globals.set:
                  id: dispo
                  value: 'false'
              - lambda: id(checkChannel)->execute(id(channelVolet).state-3);
              - script.wait: checkChannel
              - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-3);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-3);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Bowindow grand"
    device_class: shutter
    id: voletBowGrand
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-4);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-4);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-4);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Bowindow droit"
    device_class: shutter
    id: voletBowDroit
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-5);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-5);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-5);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Porte salle à manger"
    device_class: shutter
    id: voletSAM
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-6);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-6);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-6);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Cuisine"
    device_class: shutter
    id: voletCuisine
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-7);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-7);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-7);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Buanderie"
    device_class: shutter
    id: voletBuanderie
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-8);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-8);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-8);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Bureau"
    device_class: shutter
    id: voletBureau
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-9);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-9);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-9);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Chambre 1"
    device_class: shutter
    id: voletChambre1
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-10);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-10);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-10);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Chambre 2"
    device_class: shutter
    id: voletChambre2
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-11);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-11);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-11);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Chambre 3"
    device_class: shutter
    id: voletChambre3
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-12);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-12);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-12);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Salle d'ô"
    device_class: shutter
    id: voletSalleDo
    assumed_state: True
    optimistic: true
    open_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-13);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_up
    close_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-13);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_down
    stop_action:
      - if:
          condition:
            lambda: 'return id(dispo);'
          then:
            - globals.set:
                id: dispo
                value: 'false'
            - lambda: id(checkChannel)->execute(id(channelVolet).state-13);
            - script.wait: checkChannel
            - switch.turn_on: bouton_volet_stop

  - platform: template
    name: "Rez-de-chaussée"
    device_class: shutter
    id: voletBas
    assumed_state: True
    optimistic: true
    open_action:
      - cover.open: voletEst
      - delay: 3.5s
      - cover.open: voletSud
      - delay: 1.3s
      - cover.open: voletBowGauche
      - delay: 1.3s
      - cover.open: voletBowGrand
      - delay: 1.3s
      - cover.open: voletBowDroit
      - delay: 1.3s
      - cover.open: voletSAM
      - delay: 1.3s
      - cover.open: voletCuisine
      - delay: 1.3s
      - cover.open: voletBuanderie
    close_action:
      - cover.close: voletEst
      - delay: 3.5s
      - cover.close: voletSud
      - delay: 1.3s
      - cover.close: voletBowGauche
      - delay: 1.3s
      - cover.close: voletBowGrand
      - delay: 1.3s
      - cover.close: voletBowDroit
      - delay: 1.3s
      - cover.close: voletSAM
      - delay: 1.3s
      - cover.close: voletCuisine
      - delay: 1.3s
      - cover.close: voletBuanderie
    stop_action:
      - cover.stop: voletEst
      - delay: 3.5s
      - cover.stop: voletSud
      - delay: 1.3s
      - cover.stop: voletBowGauche
      - delay: 1.3s
      - cover.stop: voletBowGrand
      - delay: 1.3s
      - cover.stop: voletBowDroit
      - delay: 1.3s
      - cover.stop: voletSAM
      - delay: 1.3s
      - cover.stop: voletCuisine
      - delay: 1.3s
      - cover.stop: voletBuanderie

  - platform: template
    name: "Etage"
    device_class: shutter
    id: voletHaut
    assumed_state: True
    optimistic: true
    open_action:
      - cover.open: voletBureau
      - delay: 3.5s
      - cover.open: voletChambre1
      - delay: 1.3s
      - cover.open: voletChambre2
      - delay: 1.3s
      - cover.open: voletChambre3
      - delay: 1.3s
      - cover.open: voletSalleDo
    close_action:
      - cover.close: voletBureau
      - delay: 3.5s
      - cover.close: voletChambre1
      - delay: 1.3s
      - cover.close: voletChambre2
      - delay: 1.3s
      - cover.close: voletChambre3
      - delay: 1.3s
      - cover.close: voletSalleDo
    stop_action:
      - cover.stop: voletBureau
      - delay: 3.5s
      - cover.stop: voletChambre1
      - delay: 1.3s
      - cover.stop: voletChambre2
      - delay: 1.3s
      - cover.stop: voletChambre3
      - delay: 1.3s
      - cover.stop: voletSalleDo

  - platform: template
    name: "TOUT"
    device_class: shutter
    id: voletTout
    assumed_state: True
    optimistic: true
    open_action:
      - cover.open: voletBas
      - delay: 11.3s
      - cover.open: voletHaut
    close_action:
      - cover.close: voletBas
      - delay: 11.3s
      - cover.close: voletHaut
    stop_action:
      - cover.stop: voletBas
      - delay: 11.3s
      - cover.stop: voletHaut


RETEX :

Après quelques semaines d’ajustement au tout début, ça fait maintenant deux ans que le code et le montage fonctionnent correctement.

La dernière channel utilisée est conservée en mémoire, c’est robuste au redémarrage, aux mises à jour ESPHome ainsi qu’aux coupures de courant.

Le montage n’est pas parfait, il mériterait une protection (isolation galvanique) … mais c’est tellement bon quand c’est si simple.

Comme il y avait plein de GPIO encore dispo sur le pico, je l’ai placé à côté d’une fenêtre, j’ai ajouté un DHT22, un contact pour détecter l’ouverture/fermeture de la fenêtre, un petit HP sur un rtttl, une autre télécommande pour l’ouverture du portail, une autre télécommande pour le store banne, …

1 Pico/ESP, et tous les équipements télécommandés sont devenus sous HA.

10 « J'aime »

Ce sujet a été automatiquement fermé après 30 jours. Aucune réponse n’est permise dorénavant.