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.

Système de domotique

Voici les caractéristiques de mon prototype :

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 :

Architecture du système

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 :

Câblage des prises

Volets roulants

Un volet roulant filaire dispose de 4 fils :

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 :

Câblage des volets

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 :

Thermostat à hystérésis

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 :

Câblage du thermostat

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
1blanc orangeInutilisée
2orangeInutilisée
3blanc vertVCC
4bleuGND
5blanc bleuSDA
6vertSCL
7blanc brunInutilisée
8brunInutilisée
Câblage du capteur météo

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 :

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 :

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 = 2460398,5
Siècle julien Voir ci-dessus SJ = 0,24
Moyenne géométrique de la longitude du Soleil (en degrés) i = (280,46646+SJ×(36000,76983+SJ×0,0003032)) mod 360 i = 6°
Moyenne géométrique de l'anomalie du Soleil (en degrés) j = 357,52911+SJ×(35999,05029-0,0001537×SJ) j = 9083,54°
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 = 1,91
Longitude solaire réelle (en degrés) m = i+l m = 7,91°
Anomalie solaire réelle (en degrés) n = j+l n = 9085,45°
Vecteur du rayon du Soleil (UA) o = (1,000001018×(1-k2))/(1+k×cos(n×π180)) o = 1
Longitude apparente du Soleil (en degrés) p = m-0,00569-0,00478×sin((125,04-1934,136×SJπ180)) p = 7,9°
É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 = 82,75°
Déclinaison du Soleil (en degrés) t = (asin(sin(r×π180)×sin(p×π180)))×180π t = 3,13°
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)-12u2×sin(4i×π180)-54k2×sin(2j×π180))×180π v = -5,02 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 = 94,86°
Azimut (en fraction de jour julien) x = (720-4lo-v+fh×60)/1440 x = 0,5
Lever (en fraction de jour julien) y = (1440x-4w)/1440 y = 0,23
Coucher (en fraction de jour julien) z = (1440x+4w)/1440 z = 0,76

La conversion d'une fraction de jour julien (noté fJJ) en heure lisible se fait comme suit :

Pour la journée du vendredi 29 mars 2024, les heures sont les suivantes :

Lever du Soleil
05:36:23
Azimut du Soleil
11:55:50
Coucher du Soleil
18:15:17

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

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 :

Câblage des composants

Voici le câblage final des différents composants du prototype :

Câblage des composants

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 :

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.

Capture d'écran du client Web Capture d'écran du client Web Capture d'écran du client Web
Captures d'écran du client Web

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 :

Icônes au format SVG

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 :

Câblage du module AM2320

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 :

  1. Connexion au réseau Wi-Fi émis par la centrale domotique
  2. Appel de l'URL sur l'API
  3. Mise en veille prolongée (deep sleep mode) afin d'économiser un maximum d'énergie

Voici le câblage du bouton connecté :

Capture d'écran du client Web Capture d'écran du client Web
Schéma et photo 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 :

Principe de fonctionnement du PWM

Le montage final est le suivant :

Schéma du variateur PWM

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() {}