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.

