Création d'un prototype de centrale domotique
Depuis un certain temps, je souhaite domotiser mon logement, mais comme il est trop facile d'acheter des modules tout prêts, j'ai choisi de créer ma propre centrale de domotique.
Après avoir lu l'article publié sur framboise314.fr, j'ai souhaité reprendre le mode de fonctionnement fondé sur des cartes à relais pilotant des appareils conventionnels. L'inconvénient est que le câblage de ce système doit être prévu dès le départ pour permettre cette installation. Chaque équipement doit être câblé directement et individuellement sur la carte à relais.
Ce système permet d'utiliser des interrupteurs conventionnels et autorise, moyennant un recâblage, de repasser aisément dans un mode de fonctionnement plus classique.
Habitant dans un appartement en location, j'ai donc fabriqué un prototype dans une boîte de rangement me permettant de créer la partie logicielle et d'expérimenter le système en attendant de pouvoir câbler mon logement à ma guise.
Vous pourriez me dire que ce serait plus facile en utilisant des modules sans-fil, mais non, j'aime le câblage, alors que cela ne facilite pas la mise en œuvre.
Le but de ce prototype est de simuler l'installation cible que je souhaiterais avoir.
Voici les caractéristiques de mon prototype :
- 8 relais alimentant 5 prises de courant pilotables sur lesquelles sont branchés les équipements. Les 3 relais restants me permettent d'expérimenter le fonctionnement d'appareils futurs (thermostat, volets roulants, etc.).
- 5 interrupteurs simulant ceux installés dans une maison.
- Une sortie permettant de brancher un bandeau LED.
- Le bus I2C est brassé sur une prise RJ45 me permettant de concevoir des modules externes raccordables facilement.
Fonctionnement de la centrale domotique
Le système est séparé en plusieurs briques :
- L'espace de stockage partagé
- Il s'agit d'un système de fichiers partagé avec les différents Raspberry Pi. L'état de chaque entrée et sortie est traduite par la présence ou non d'un fichier dans cet espace de stockage. Ce système permet d'avoir une installation pouvant grandir en ajoutant des Raspberry Pi supplémentaires afin d'augmenter le nombre d'entrées/sorties gérées. Plus précisément, il s'agit d'un tmpfs afin de limiter le nombre de cycles d'écriture sur les cartes SD. Il est partagé en NFS.
- L'API
- Écrite en PHP, elle assure toute l'intelligence du système. Elle est appelée par la brique d'entrée/sortie lorsqu'une entrée est activée et par une tâche cron toutes les minutes afin de déclencher les tâches planifiées. Chaque élément (étage, pièce, équipement, script) peut être appelé par l'API avec une URL dédiée.
- Le démon d'entrée/sortie
- Écrit en Python, ce démon pilote les périphériques (relais, bandeau LED, interrupteurs, etc.) et l'espace de stockage. Il crée ou supprime les fichiers dans le répertoire in en fonction de l'état des entrées et active ou désactive les sorties en fonction de la présence ou non des fichiers dans le répertoire out.
- Le client Web
- Ecrit en HTML, CSS et JS, il interroge l'API et propose une interface utilisateur simple et intuitive.
L'architecture du système se schématise comme suit :
Chaque équipement est piloté en central, ce qui implique un câblage spécifique dans le cas où le système est déployé sur un logement :
Lampes et prises de courant
Chaque lampe et chaque prise de courant sont pilotées par un relais dédié. La phase part du bornier et entre dans la borne COM du relais. La borne NO (normalement ouvert) part vers la lampe ou la prise de courant.
Le câblage obtenu est le suivant :
Volets roulants
Un volet roulant filaire dispose de 4 fils :
- Phase montée du volet
- Phase descente du volet
- Neutre
- Terre
Chaque phase est raccordée individuellement à un relais. Le neutre et la terre sont directement raccordés aux borniers.
L'allumage exclusif de chacune des phases est assuré logiciellement. Lorsque la fermeture est demandée, le relais d'ouverture est éteint, puis le relais de fermeture est allumé. La hauteur d'ouvertue du volet est gérée par temporisation.
Le câblage obtenu est le suivant :
Thermostat
Le contact du thermostat est assuré par un relais. La fermeture et l'ouverture du contact sont déclenchées par la température de consigne à laquelle est ajoutée ou soustraite une marge de 1°C :
La sonde de température est assurée par l'un des capteurs météo de la maison.
Le câblage obtenu est le suivant :
Bandeau LED
Le bandeau LED est composé de plusieurs LED RGB réglables individuellement. Chaque LED est associée à une puce WS2812B pilotable depuis un port GPIO du Raspberry Pi.
Station météo
La sonde météo BME280 connectée en I2C permet d'obtenir des informations concernant la température, l'humidité et la pression atmosphérique. Pour ce prototype, elle est déportée afin que les mesures ne soient pas faussées par la chaleur dégagée par le Raspberry Pi. Pour cela, j'ai utilisé une prise RJ45 sur laquelle est brassé le bus I2C pour faciliter la connexion :
Broche | Code couleur T568B | Usage |
---|---|---|
1 | blanc orange | Inutilisée |
2 | orange | Inutilisée |
3 | blanc vert | VCC |
4 | bleu | GND |
5 | blanc bleu | SDA |
6 | vert | SCL |
7 | blanc brun | Inutilisée |
8 | brun | Inutilisée |
Configuration de l'API
La configuration de l'API se fait dans un fichier JSON. Elle décrit la configuration des étages, des pièces et des équipements. On y retrouve également les scripts et les tâches planifiées.
Voici un exemple de configuration :
{ "floors":{ "rez-chaussee":{ "name":"Rez-de-chaussée", "rooms":{ "cuisine":{ "name":"Cuisine", "icon":"kitchen", "color":"red", "items":{ "spots":{ "type":"Lamp", "name":"Lumière", "relay":{ "label":"lumiereCuisine" }, "ecoMode":true }, "cafetiere":{ "type":"PowerPlug", "name":"Cafetière", "relay":{ "label":"cafetiere" }, "ecoMode":true } } }, "salon":{ "name":"Salon", "icon":"livingroom", "color":"green", "items":{ "spots":{ "type":"Dimmer", "name":"Spots", "label":"spotsSalon", "ecoMode":true }, "television":{ "type":"PowerPlug", "name":"Télévision", "relay":{ "label":"television" }, "ecoMode":true }, "console":{ "type":"PowerPlug", "name":"Console jeux", "relay":{ "label":"console" }, "ecoMode":true }, "meteoSalon":{ "type":"RoomWeather", "label":"meteoSalon", "name":"Sonde météo" }, "interSpots":{ "type":"Input", "call":"/rez-chaussee/salon/spots/tooglePower" }, "interTelevision":{ "type":"Input", "call":"/rez-chaussee/salon/television/tooglePower" }, "buttonPorte":{ "type":"RemotePushButton" } } } } }, "etage":{ "name":"Premier étage", "rooms":{ "chambre":{ "name":"Chambre", "icon":"bedroom", "color":"blue", "items":{ "lampeChevet":{ "type":"Lamp", "name":"Lampe de chevet", "relay":{ "label":"lampeChevet" }, "ecoMode":true }, "ventilateur":{ "type":"PowerPlug", "name":"Ventilateur", "relay":{ "label":"ventilateur" }, "ecoMode":true }, "volet":{ "type":"Shutter", "name":"Store" } } } } } }, "items":{ "thermostat":{ "name":"Thermostat", "type":"Thermostat", "temperatureSensorUrl":"/rez-chaussee/salon/meteoSalon", "relay":{ "label":"relaisThermostat" } }, "telephoneAntoine":{ "type":"Input", "ecoMode":true } }, "scripts":{ "reveil":{ "name":"reveil", "hidden":true, "trigger":{ "and":[ {"==":["/telephoneAntoine/status",true]}, {"==":["/time/hour",6]}, {"==":["/time/minute",30]}, {"==":["/time/holyday",false]} ] }, "script":[ "/etage/chambre/lampeChevet/powerOn", "/rez-chaussee/cuisine/cafetiere/powerOn" ] }, "depart":{ "name":"Scénario absence", "hidden":false, "trigger":{"==":["/rez-chaussee/salon/buttonPorte/status",true]}, "script":[ "/powerOff", "/thermostat/setTemperatureNight" ] }, "coucherSoleil":{ "name":"coucherSoleil", "hidden":true, "trigger":{ "and":[ {"==":["/telephoneAntoine/status",true]}, {"==":["/time/sunset",true]} ] }, "script":[ "/rez-chaussee/salon/spots/sunrise" ] } }, "ecoModeTimeout":300, "latitude":48.855, "longitude":2.2944, "timezone":1 }
Les scripts
Les scripts permettent d'appeler séquentiellement différentes adresses de l'API. Il est également possible de définir une pause entre deux appels.
Programmation d'événements
Dans la configuration du serveur, il est possible de créer des tâches planifiées selon la configuration du système. Les paramètres pouvant être utilisés sont les suivants :
- L'état des entrées/sorties
- Le temps (heure, minute, lever, azimut ou coucher du Soleil)
- La date (jour, mois, année, jours ouvrés)
Il est possible de combiner les conditions d'exécution avec les opérateurs ET et OU, et également de comparer les valeurs avec les opérateurs "égal", "différent", >, >=, <, <=, incluses ou exclues d'une liste de valeurs.
Voici un exemple de configuration permettant d'allumer la lampe de chevet (selon la configuration ci-dessus) à 7:00 tous les jours ouvrés :
"reveil":{ "name":"reveil", "hidden":true, "trigger":{ "and":[ {"==":["/time/hour",7]}, {"==":["/time/minute",0]}, {"==":["/time/holyday",false]} ] }, "script":[ "/etage/chambre/lampeChevet/powerOn" ] }
Calcul de l'heure de lever, d'azimut et de coucher de Soleil
N'ayant pas de box Internet et souhaitant un système le plus indépendant possible des API externes, j'ai donc voulu implémenter un calcul de l'heure de lever, d'azimut et de coucher du Soleil pour, par exemple, fermer les volets en fin de journée.
Pour cela, je me suis basé sur la feuille de calcul fournie par l'Agence américaine d'observation océanique et atmosphérique qui permets de calculer ces heures à partir de la latitude, la longitude et la date du jour.
Le calcul s'effectue comme suit. Les paramètres choisis sont les heures de lever, d'azimut et de coucher du Soleil pour aujourd'hui à la Tour Eiffel, exprimés en heure locale :
Tout d'abord, le calcul se fait avec les jours julien et les siècles juliens. Pour cela, on effectue la conversion en appliquant les règles suivantes :
- Soit Y l'année, M la valeur numérique du mois (1=janvier, 2=février, etc.) et D le jour.
- Si la date est au mois de janvier ou février, on retire 1 à Y et on ajoute 12 à M.
- On effectue les opérations suivantes en ne retenant que la valeur entière des divisions :
- a = Y/100 = 20
- b = a/4 = 5
- c = 2-a+b = -13
- e = 365,25×(Y+4716) = 2461785
- f = 30,6001×(M+1) = 397
- JJ = c+D+e+f-1524,5 = 2460658,5
- SJ = (JJ-2451545)/36525 = 0,24951403148528
Le calcul des heures se fait comme suit. Soit la la latitude, lo la longitude et fh le fuseau horaire du point à calculer. La lettre des variables correspond à la colonne de la feuille de calcul d'origine :
Explication | Formule | Valeur arrondie |
---|---|---|
Jour julien | Voir ci-dessus | JJ = 2460658,5 |
Siècle julien | Voir ci-dessus | SJ = 0,25 |
Moyenne géométrique de la longitude du Soleil (en degrés) | i = (280,46646+SJ×(36000,76983+SJ×0,0003032)) mod 360 | i = 263° |
Moyenne géométrique de l'anomalie du Soleil (en degrés) | j = 357,52911+SJ×(35999,05029-0,0001537×SJ) | j = 9339,8° |
Excentricité de l'orbite terrestre | k = 0,016708634-SJ×(0,000042037+0,0000001267×SJ) | k = 0,02 |
Équation du centre solaire | l = sin(j×π⁄180)×(1,914602-SJ×(0,004817+0,000014×SJ))+sin(2j×π⁄180)×(0,019993-0,000101×SJ)+sin(3j×π⁄180)×0,000289 | l = -0,67 |
Longitude solaire réelle (en degrés) | m = i+l | m = 262,33° |
Anomalie solaire réelle (en degrés) | n = j+l | n = 9339,12° |
Vecteur du rayon du Soleil (UA) | o = (1,000001018×(1-k2))/(1+k×cos(n×π⁄180)) | o = 0,98 |
Longitude apparente du Soleil (en degrés) | p = m-0,00569-0,00478×sin((125,04-1934,136×SJ)×π⁄180)) | p = 262,32° |
Écliptique de l'obliquité moyenne (en degrés) | q = 23+(26+(21,448-SJ×(46,815+SJ×(0,00059-SJ×0,001813)))/60)/60 | q = 23,44° |
Correction de l'obliquité (en degrés) | r = q+0,00256×cos((125,04-1934,136×SJ)×π⁄180) | r = 23,44° |
Ascention solaire réelle (en degrés) | s = (atan2(cos(p×π⁄180),cos(r×π⁄180)×sin(p×π⁄180)))×180⁄π | s = -171,64° |
Déclinaison du Soleil (en degrés) | t = (asin(sin(r×π⁄180)×sin(p×π⁄180)))×180⁄π | t = -23,22° |
var y | u = tan((r/2)×π⁄180)×tan((r/2)×π⁄180) | u = 0,04 |
Équation du temps (en minutes) | v = 4(u×sin(2i×π⁄180)-2k×sin(j×π⁄180)+4k u×sin(j×π⁄180)×cos(2i×π⁄180)-1⁄2u2×sin(4i×π⁄180)-5⁄4k2×sin(2j×π⁄180))×180⁄π | v = 5,4 minutes |
Angle horaire de lever du Soleil (en degrés) | w = (acos(cos(90,833×π⁄180)/(cos(la×π⁄180)×cos(t×π⁄180))-tan(la×π⁄180)×tan(t×π⁄180)))×180⁄π | w = 62,17° |
Azimut (en fraction de jour julien) | x = (720-4lo-v+fh×60)/1440 | x = 0,49 |
Lever (en fraction de jour julien) | y = (1440x-4w)/1440 | y = 0,32 |
Coucher (en fraction de jour julien) | z = (1440x+4w)/1440 | z = 0,66 |
La conversion d'une fraction de jour julien (noté fJJ) en heure lisible se fait comme suit :
- heures = valeur entière(24×fJJ)
- minutes = valeur entière(1440×(fJJ-heures/24))
- secondes = valeur entière(86400×(fJJ-heures/24-minutes/1440))
Pour la journée du samedi 14 décembre 2024, les heures sont les suivantes :
- Lever du Soleil
- 07:36:45
- Azimut du Soleil
- 11:45:25
- Coucher du Soleil
- 15:54:04
L'utilisation de ces heures dans les scripts se fait en vérifiant que les booléens sunrise
(lever de Soleil), sunnoon
(azimut) et sunset
(coucher de Soleil) soient vrais.
Calcul des jours fériés mobiles
En plus des fêtes fixes (8 mai, 14 juillet, 11 novembre, etc.), nous avons des fêtes dites mobiles (car leurs dates varient chaque année). C'est le cas pour Pâques, le Lundi de Pâques, l'Ascension, la Pentecôte et le lundi de Pentecôte (suivant les dispositions prises par votre employeur). Cependant, il est possible de déterminer, à partir de la date de Pâques, la date du Lundi de Pâques (le lendemain), de l'Ascension (40 jours après Pâques), de la Pentecôte (50 jours après Pâques) et du lundi de Pentecôte (51 jours après Pâques).
Pour calculer la date de Pâques, on utilise l'algorithme de Butcher-Meeus. Pour cela, on applique les opérations suivantes. Les valeurs données sont celles pour l'année 2024 :
Dividende | Diviseur | Quotient | Reste | Explication |
---|---|---|---|---|
Année = 2024 | 19 | n = 10 | Cycle de Méton | |
Année = 2024 | 100 | c = 20 | u = 24 | Centaine et rang de l'année |
c | 4 | s = 5 | t = 0 | Siècle bissextile |
c + 8 | 25 | p = 1 | Cycle de proemptose | |
c - p + 1 | 3 | q = 6 | Proemptose | |
19n + c - s - q + 15 | 30 | e = 4 | Epacte | |
u | 4 | b = 6 | d = 0 | Année bissextile |
2t + 2b - e - d + 32 | 7 | L = 5 | Lettre dominicale | |
n + 11e + 22L | 451 | h = 0 | Correction | |
e + L - 7h + 114 | 31 | m = 3 | j = 30 | Mois et quantième du Samedi saint |
- Si m = 3, le dimanche de Pâques est le (j + 1) mars.
- Si m = 4, le dimanche de Pâques est le (j + 1) avril.
Pour l'année 2024, les dates des jours fériés mobiles sont :
- Pâques
- Dimanche 31 mars 2024
- Lundi de Pâques
- Lundi 1 avril 2024
- Ascension
- Jeudi 9 mai 2024
- Pentecôte
- Dimanche 19 mai 2024
- Lundi de Pentecôte
- Lundi 20 mai 2024
Un fichier iCalendar avec les jours fériés français et des changements d'heure est disponible ici : https://antoinepernot.fr/joursFeries.ics
Matériel
Pour concevoir ce prototype, j'ai utilisé les éléments suivants. Je donne cette liste à titre indicatif et vous êtes libres de l'adapter à votre guise :
Produit | Fournisseur | PU | Quantité | Total |
---|---|---|---|---|
Boîte de rangement | Leroy Merlin | 3,95 € | 1 | 3,95 € |
Prise femelle | Leroy Merlin | 2,18 € | 5 | 10,90 € |
Câble électrique | Leroy Merlin | 9,90 € | 1 | 9,90 € |
Fil électrique | Leroy Merlin | 7,55 € | 1 | 7,55 € |
Bornier automatique 5 entrées | Leroy Merlin | 5,90 € | 2 | 11,80 € |
Raspberry Pi 3B | Amazon | 39,98 € | 1 | 39,98 € |
Carte SD 16 Go | Amazon | 9,07 € | 1 | 9,07 € |
Alimentation 5V 8A | Amazon | 20,27 € | 1 | 20,27 € |
Carte de 8 relais 5V avec opto-coupleurs | Amazon | 9,99 € | 1 | 9,99 € |
Modules I/O I2C PCF8574 (lot de 2) | Amazon | 6,38 € | 1 | 6,38 € |
Interrupteurs à bascule (lot de 10) | Amazon | 8,99 € | 1 | 8,99 € |
Fils Dupont (lot de 120) | Amazon | 6,99 € | 1 | 6,99 € |
Prises RJ45 femelles (lot de 5) | Amazon | 7,99 € | 1 | 7,99 € |
Capteur météo I2C (température, humidité, pression) BME280 | Amazon | 7,99 € | 1 | 7,99 € |
Horloge temps réel I2C DS3231 | Amazon | 6,29 € | 1 | 6,29 € |
Bandeau 60 LED RGB 1m WS2812B | Amazon | 13,99 € | 1 | 13,99 € |
Total | 182,03 € |
J'ai également utilisé un cordon d'alimentation standard pour tout alimenter. Le choix de ce modèle de Raspberry Pi est motivé par la présence du module WiFi inclus qui me permet de créer un réseau qui lui est propre (et aussi parce que j'en avais un inutilisé sous la main).
L'outillage nécessaire pour réaliser ce prototype est le suivant :
- Perceuse
- Fer à souder
- Pince à dénuder
- Pince coupante
- Tournevis
- Bistouri
Câblage des composants
Voici le câblage final des différents composants du prototype :
Le code
Le système est composé de trois briques logicielles. La communication entre l'API et le démon d'entrée/sortie est assurée par le système de fichiers partagé. Cela permet de partager simplement l'état du système entre plusieurs Raspberry Pi et avoir ainsi un système extensible à volonté : on ajoute des Raspberry Pi avec des modules ad-hoc pour augmenter le nombre d’éléments pilotés.
Le client Web appelle l'API pour connaître l'état du système et envoyer des ordres. Cela me permet de créer différents systèmes pouvant dialoguer avec le système de domotique en appelant l'API pour ajouter des fonctionnalités.
Le démon d'entrée/sortie
Un script Python est en charge de démarrer différents processus gérant chacun un composant. Les composants gérés sont :
BME280
: sonde atmosphérique I2C.NetworkDevice
: effectue régulièrement une requêteping
sur une adresse IP afin de vérifier sa présence sur le réseau. Utilisé principalement pour le déclenchement du mode éco par la supervision des smartphones (le mode éco est expliqué plus loin).PCF8574
: entrée/sortie I2C.Shutter
: surcouche au module PCF8574 gérant le niveau d'ouvertue et de fermeture des stores avec une temporisation.Sonoff
: appelle les URL de l'API Tasmota intégrée dans des modules Sonoff.WS2812B
: pilote du bandeau LED.
Le démon d'entrée/sortie est également en charge du réveil de l'API pour des tâches planifiées.
Voici un exemple de configuration :
{ "roomUrl":"/appartement/salon", "masterIO":true, "0x20":{ "modeIn":true, "1":"interSpots", "2":"interTelevision", "3":"in3NA", "4":"in4NA", "5":"in5NA", "6":"in6NA", "7":"in7NA", "8":"in8NA" }, "0x21":{ "modeIn":false, "1":"television", "2":"console", "3":"out3NA", "4":"out4NA", "5":"out5NA", "6":"out6NA", "7":"out7NA", "8":"out8NA" }, "roomWeather":"meteoSalon" }
L'API
L'API représente chaque élément (objet, étage, pièce, script, etc.) sous la forme d'objets. Chaque objet est capable d’être appelé par une URL sous la forme suivante : /etage/piece/objet/methode
(exemples : /rez-chaussee/salon/spots/powerOn
pour allumer les spots du salon, /etage/chambre/powerOff
pour éteindre tous les équipements de la chambre). Si la méthode n'est pas mentionnée, la fonction returnData
est appelée. Elle permet de retourner l'état de l'objet.
Il est possible d'obtenir l'état de tous les objets enfants en appelant l'objet parent. Cela limite les appels de l'API.
Voici un exemple de retour fait par l'API. Ici, il s'agit de l'état de l'ensemble du système (/returnData
) :
{ "url": "/", "rez-chaussee": { "url": "/rez-chaussee/", "name": "Rez-de-chaussée", "type": "Floor", "cuisine": { "url": "/rez-chaussee/cuisine/", "name": "Cuisine", "type": "Room", "icon": "kitchen", "color": "red", "spots": { "url": "/rez-chaussee/cuisine/spots/", "name": "Lumière", "groups": [], "hidden": false, "type": "Lamp", "relay": { "label": "lumiereCuisine", "status": false }, "status": false }, "cafetiere": { "url": "/rez-chaussee/cuisine/cafetiere/", "name": "Cafetière", "groups": [], "hidden": false, "type": "PowerPlug", "relay": { "label": "cafetiere", "status": true }, "status": true } }, "salon": { "url": "/rez-chaussee/salon/", "name": "Salon", "type": "Room", "icon": "livingroom", "color": "green", "spots": { "url": "/rez-chaussee/salon/spots/", "name": "Spots", "groups": [], "hidden": false, "type": "Dimmer", "dimmerPower": 100, "dimmerSpeed": 1 }, "television": { "url": "/rez-chaussee/salon/television/", "name": "Télévision", "groups": [], "hidden": false, "type": "PowerPlug", "relay": { "label": "television", "status": true }, "status": true }, "console": { "url": "/rez-chaussee/salon/console/", "name": "Console jeux", "groups": [], "hidden": false, "type": "PowerPlug", "relay": { "label": "console", "status": false }, "status": false }, "meteoSalon": { "url": "/rez-chaussee/salon/meteoSalon/", "name": "Sonde météo", "groups": [], "hidden": false, "type": "RoomWeather", "temperature": 19.1, "humidity": 51.5, "pressure": 1005.1, "measuresHistory": { "dateTime": [ "2021-10-30 19:00", "2021-10-30 20:00", "2021-10-30 21:00", "2021-10-30 22:00", "2021-10-30 23:00", "2021-10-31 00:00", "2021-10-31 01:00", "2021-10-31 02:00", "2021-10-31 03:00", "2021-10-31 04:00", "2021-10-31 05:00", "2021-10-31 06:00", "2021-10-31 07:00", "2021-10-31 08:00", "2021-10-31 09:00", "2021-10-31 10:00", "2021-10-31 11:00", "2021-10-31 12:00", "2021-10-31 13:00", "2021-10-31 14:00", "2021-10-31 15:00" ], "temperature": [ 23.3, 21.4, 21.4, 21.6, 21.1, 20.7, 20.6, 20.5, 20.5, 20.4, 20.4, 20.3, 19.6, 21, 21.3, 21.4, 21.4, 21.8, 21.6, 21.5, 21.4 ], "humidity": [ 59, 65.4, 65.6, 65.4, 70.5, 72.4, 72.4, 72, 71.2, 70.4, 69.7, 69, 63, 60.3, 60.7, 60.5, 60.2, 59.7, 59.5, 59.6, 60.3 ], "pressure": [ 990.7, 991.2, 991.6, 992, 991.8, 991.6, 991.3, 990.7, 989.9, 989.2, 988.3, 987.7, 987, 986.3, 985.1, 984, 983.3, 982.4, 981.8, 982.1, 983.2 ] } }, "interSpots": { "url": "/rez-chaussee/salon/interSpots/", "type": "Input", "status": false, "ecoMode": false }, "interTelevision": { "url": "/rez-chaussee/salon/interTelevision/", "type": "Input", "status": false, "ecoMode": false }, "buttonPorte": { "url": "/rez-chaussee/salon/buttonPorte/", "type": "RemotePushButton", "status": false, "ecoMode": false } } }, "etage": { "url": "/etage/", "name": "Premier étage", "type": "Floor", "chambre": { "url": "/etage/chambre/", "name": "Chambre", "type": "Room", "icon": "bedroom", "color": "blue", "lampeChevet": { "url": "/etage/chambre/lampeChevet/", "name": "Lampe de chevet", "groups": [], "hidden": false, "type": "Lamp", "relay": { "label": "lampeChevet", "status": false }, "status": false }, "ventilateur": { "url": "/etage/chambre/ventilateur/", "name": "Ventilateur", "groups": [], "hidden": false, "type": "PowerPlug", "relay": { "label": "ventilateur", "status": false }, "status": false }, "volet": { "url": "/etage/chambre/volet/", "name": "Store", "groups": [], "hidden": false, "type": "Shutter", "currentOpening": 80 } } }, "thermostat": { "url": "/thermostat/", "name": "Thermostat", "groups": [], "hidden": false, "type": "Thermostat", "relay": { "label": "relaisThermostat", "status": true }, "temperatureSet": 21, "temperatureSensorUrl": "/rez-chaussee/salon/meteoSalon", "dayTemperature": 21, "nightTemperature": 18, "frostProofTemperature": 5 }, "telephoneAntoine": { "url": "/telephoneAntoine/", "type": "Input", "status": false, "ecoMode": true }, "reveil": { "url": "/reveil/", "name": "reveil", "type": "Script", "timerTrigger": true, "hidden": true, "trigger": { "and": [ { "==": [ "/telephoneAntoine/status", true ] }, { "==": [ "/time/hour", 6 ] }, { "==": [ "/time/minute", 30 ] }, { "==": [ "/time/holyday", false ] } ] }, "script": [ "/etage/chambre/lampeChevet/powerOn", "/rez-chaussee/cuisine/cafetiere/powerOn" ] }, "depart": { "url": "/depart/", "name": "Scénario absence", "type": "Script", "timerTrigger": false, "hidden": false, "trigger": { "==": [ "/rez-chaussee/salon/buttonPorte/status", true ] }, "script": [ "/powerOff", "/thermostat/setTemperatureNight" ] }, "coucherSoleil": { "url": "/coucherSoleil/", "name": "coucherSoleil", "type": "Script", "timerTrigger": true, "hidden": true, "trigger": { "and": [ { "==": [ "/telephoneAntoine/status", true ] }, { "==": [ "/time/sunset", true ] } ] }, "script": [ "/rez-chaussee/salon/spots/sunrise" ] }, "hash": "33abb64d6562b483b7f5635f75500c0edcfadffa", "time": { "timestamp": 1647700958, "dow": 6, "dom": 19, "doy": 77, "month": 3, "year": 2022, "leapYear": 0, "hour": 15, "minute": 42, "seconde": 38, "holyday": true, "sunnoon": false, "sunset": false, "sunrise": false, "sunnoonTime": "12:58:53", "sunsetTime": "19:0:0", "sunriseTime": "6:57:45", "day": true } }
Ce mode de fonctionnement permet à n'importe quel script ou application de s'interfacer avec le système de domotique sans avoir à recréer d'interface particulière.
Le mode éco est déclenché quand tous les éléments d'entrée identifiés avec le champ ecoMode
sont désactivés. Les objets identifiés avec ce même champ sont alors éteints. Ma configuration actuelle vérifie la présence de mon téléphone sur le réseau. Si celui-ci est injoignable pendant 15 minutes consécutives, la majorité des appareils sont éteints.
Le client Web
Le client Web est optimisé pour une utilisation sur mobile. La mise à jour de l'état des objets (allumage d'une lampe, fermeture d'un volet, etc.) est assurée par un appel régulier à l'API.
Les icônes utilisées par le client sont de ma création. Elles sont disponibles au téléchargement au format SVG et sous licence CC BY-SA :
Création de modules Wi-Fi
Afin de pallier au principal défaut de ce prototype, à savoir la nécessité de prévoir le câblage en amont de ce système, j'ai entrepris de créer des modules Wi-Fi basés sur le microcontrôleur ESP8266.
Le choix de ce microcontrôleur a été motivé par son prix modeste et ses grandes possibilités d'utilisation.
Sonde de température extérieure
Afin de mesurer la température extérieure, j'ai entrepris de créer une sonde de température et d'humidité à partir d'une sonde AM2320 que j'avais utilisée pour la centrale, avant de passer au BME280 m'offrant en plus la pression atmosphérique.
Voici un schéma du câblage :
Une fois connecté au réseau Wi-Fi émis par ma centrale domotique, le microcontrôleur fournit une interface HTTP qui renvoie au format JSON les mesures de température (en °C) et d'humidité :
{'temperature':19.54,'humidity':54.21}
Voici le code source du microcontrôleur. Ne pas oublier d'inclure la librairie pour l'IDE Arduino de la puce AM2320 :
#include <AM2320.h> #include <ESP8266WiFi.h> AM2320 sensor; const char* ssid = "SSIDduWiFi"; const char* password = "MotDePasseDuWiFi"; WiFiServer server(80); void setup() { sensor.begin(0,2); // Initie le bus I2C (GPIO 0 : SDA, GPIO 2 : SCL) delay(10); WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(500); } server.begin(); } void loop() { WiFiClient client = server.available(); if (!client) { return; } while(!client.available()){ delay(1); } String request = client.readStringUntil('\r'); client.flush(); if (sensor.measure()) { client.println("HTTP/1.1 200 OK"); client.println("Content-Type: application/json"); client.println(""); client.print("{'temperature':"); client.print(sensor.getTemperature()); client.print(",'humidity':"); client.print(sensor.getHumidity()); client.println("}"); } else { int errorCode = sensor.getErrorCode(); switch (errorCode) { case 1: client.println("HTTP/1.1 500 Internal Server Error"); client.println("Content-Type: application/json"); client.println(""); client.println("{'error':'Sensor offline'}"); break; case 2: client.println("HTTP/1.1 500 Internal Server Error"); client.println("Content-Type: application/json"); client.println("{'error':'CRC failed'}"); break; } } delay(1); }
Bouton connecté
Pour effectuer certaines opérations sans à avoir à utiliser son smartphone, j'ai utilisé un bouton poussoir en saillie dans lequel sont câblées une carte ESP-01 et deux piles AAA pour l'alimenter. Lorsque le bouton poussoir est pressé, le microcontrôleur ESP8266 est réinitialisé. Le programme effectue les opérations suivantes :
- Connexion au réseau Wi-Fi émis par la centrale domotique
- Appel de l'URL sur l'API
- Mise en veille prolongée (deep sleep mode) afin d'économiser un maximum d'énergie
Voici le câblage du bouton connecté :
Voici le code source du microcontrôleur :
#include <ESP8266WiFi.h> const char* ssid = "SSIDduWiFi"; const char* password = "MotDePasseDuWiFi"; const char* host = "10.250.0.1"; void setup() { WiFi.begin(ssid, password); while (WiFi.status() != WL_CONNECTED) { delay(5); } WiFiClient client; if (client.connect(host, 8080)) { client.print(String("GET /button1/callInput") + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n" + "\r\n" ); while (client.connected() || client.available()) { if (client.available()) { String line = client.readStringUntil('\n'); } } client.stop(); } ESP.deepSleep(0); } void loop() {}
Variateur de lumière connecté
Une possibilité que j'ai souhaitée ajouter à mon système est de pouvoir modifier l'intensité lumineuse des éclairages. C'est pourquoi j'ai choisi d'utiliser un variateur à courant continu en PWM avec détection du passage à zéro (lien vers la fiche produit fournie à titre indicatif).
Le principe de fonctionnement de ce système est de générer une tension de sortie en fonction d'un signal de consigne dit signal PWM. Ce signal alterne un niveau haut et un niveau bas en fonction de la puissance voulue. Dans notre cas, le signal alternatif de la prise électrique est modulé par le signal PWM généré par l'ESP8266 pour obtenir la tension de sortie désirée. De plus, le signal PWM est, ici, synchronisé avec la sinusoïde du courant alternatif de la prise de courant grâce à l'impulsion émise par le module depuis sa broche Z-C lors du passage à 0. Cette impultion va déclencher une interruption du processeur du microcontrôleur et exécutera la fonction zeroCross
du code ci-dessous.
Le schéma ci-dessous montre le fonctionnement de ce montage avec une puissance à 25 % du maximum :
Le montage final est le suivant :
Le code du microcontrôleur doit donc générer un signal PWM synchronisé à partir d'une consigne reçue de la centrale domotique :
//void ICACHE_RAM_ATTR zeroCross(); #include <ESP8266WiFi.h> #include <ESP8266WebServer.h> #include <Scheduler.h> int pwmPin = 16; // D0 int zcPin = 14; // D5 int dimming = 0; int targetDimming = 0; int offTime = 10000; int dimmerCount = 0; int dimmerTrigger = 1; const char* ssid = "SSIDduWiFi"; const char* password = "MotDePasseDuWiFi"; ESP8266WebServer server(80); void ICACHE_RAM_ATTR zeroCross() { if (dimming < 5){ digitalWrite(pwmPin, LOW); } else if (dimming == 100){ digitalWrite(pwmPin, HIGH); } else { digitalWrite(pwmPin, LOW); delayMicroseconds(offTime); digitalWrite(pwmPin, HIGH); } } class SmoothDimming : public Task { public: void loop() { if (dimmerTrigger == 0) { dimmerCount = 0; dimming = targetDimming; offTime = 10000-(100*dimming); } else if (dimmerCount >= dimmerTrigger){ dimmerCount = 0; if (targetDimming < dimming){ dimming--; offTime = 10000-(100*dimming); }else if (targetDimming > dimming){ dimming++; offTime = 10000-(100*dimming); } } dimmerCount++; delay(100); } } smooth_dimming; class HTTPClient : public Task { public: void loop() { server.handleClient(); } } http_client; void setup() { pinMode(pwmPin, OUTPUT); digitalWrite(pwmPin, LOW); WiFi.begin(ssid, passwd); while (WiFi.status() != WL_CONNECTED) { delay(500); } server.on("/", [](){ if (server.arg("power") != ""){ targetDimming = server.arg("power").toInt(); } if (server.arg("speed") != ""){ dimmerTrigger = server.arg("speed").toInt(); } String s = "{\"power\":"; s += targetDimming; s += ", \"speed\":"; s += dimmerTrigger; s += "}"; server.send(200, "application/json", s); }); server.begin(); attachInterrupt(digitalPinToInterrupt(zcPin), zeroCross, RISING); Scheduler.start(&smooth_dimming); Scheduler.start(&http_client); Scheduler.begin(); } void loop() {}