Boîte à herbes intelligente : Kit de jardinage d'intérieur connecté à Alexa
Composants et fournitures
| Composant | Quantité |
|---|---|
| Amazon Alexa Echo Dot | 1 |
| Arduino UNO | 1 |
| Espressif ESP8266 ESP-01 | 1 |
| Arduino Proto Shield | 1 |
| Shield 4 relais Arduino | 1 |
| Pompe à eau (Aqua Pump) | 2 |
| Lampe de croissance pour plantes | 1 |
| Adaptateur secteur 220V CC | 1 |
| Adaptateur secteur 12V CC | 1 |
| Prise USB + câble | 1 |
| Tuyau d'air | 1 |
| Fils de connexion (jumper wires) | 1 jeu |
Applications et services en ligne
- Arduino IDE
- Amazon Alexa Skills Kit
- Amazon Web Services AWS Lambda
- Serveur Web PHP
À propos de ce projet
Mes plantes souffraient toujours d'un excès ou d'un manque d'eau, et j'aime utiliser beaucoup d'herbes dans mes plats. J'ai donc décidé de créer un système d'irrigation personnalisé. La boîte à herbes doit être configurable et fonctionner automatiquement ou manuellement. Une interface web permet la configuration et affiche l'humidité sous forme de graphique. La dernière étape a été l'intégration du contrôle vocal pour demander à Amazon Alexa l'humidité, allumer/éteindre la lampe de croissance et déclencher l'irrigation si l'automatisation est désactivée.
J'ai commencé par la partie technique en achetant un Arduino. Après quelques tutoriels, j'ai maîtrisé le logiciel et le contrôle de l'Arduino. J'ai commandé un contrôleur WiFi, des capteurs d'humidité, des pompes, une lampe de croissance et le matériel nécessaire (shield relais pour séparer les circuits de la lampe et des pompes de l'Arduino, fils et bois de hêtre pour le cadre). Le code Arduino est fourni dans ce tutoriel, avec des informations sur l'utilisation des composants. Le code du site web/API n'est pas fourni (sauf si la demande est très forte).
Étape 1 : Capteur d'humidité
Première étape : lire l'humidité avec l'Arduino. Le capteur YL-69 se connecte facilement : VCC sur une broche GPIO (ex: broche 6), GND au ground, A0 sur une broche analogique (ex: A1).
byte vccPin = 6;
byte dataPin = A1;
void setup() {
pinMode(vccPin, OUTPUT);
digitalWrite(vccPin, LOW);
Serial.begin(9600);
while (!Serial);
}
int readHumidity() {
digitalWrite(vccPin, HIGH);
delay(500); // Temps de pré-alimentation avant mesure
int value = analogRead(dataPin);
digitalWrite(vccPin, LOW);
return 1023 - value;
}
void loop() {
Serial.print("NiveauHumidite (0-1023): ");
Serial.println(readHumidity());
delay(10000);
}
Étape 2 : Relais pour pompes et lampe
Objectif : installer un shield relais (4 relais) pour séparer les circuits de la lampe (230V), des pompes (12V) et de l'Arduino (5V). Le shield se connecte aux broches 5V et GND de l'Arduino. Chaque relais nécessite une broche GPIO pour l'activation.
Important : Mon shield s'active avec un état BAS (LOW) sur la broche. Dès que la broche est définie en OUTPUT, elle s'active automatiquement. Pour désactiver le relais, il faut mettre la broche en INPUT et LOW. Par défaut, les broches Arduino sont en INPUT et LOW.
byte pump1 = 11;
byte pump2 = 10;
void setup() {
Serial.begin(9600);
while (!Serial);
pinMode(pump1, OUTPUT);
digitalWrite(pump2, LOW);
}
void loop() {
digitalWrite(pump1, HIGH); // Pompe 1 désactivée
pinMode(pump2, INPUT); // Pompe 2 désactivée
delay(1000);
digitalWrite(pump1, LOW); // Pompe 1 activée
pinMode(pump2, OUTPUT); // Pompe 2 activée
delay(1000);
}
Étape 3 : WiFi avec ESP-01
Connecter l'ESP8266 ESP-01 à l'Arduino pour le WiFi a été la partie la plus difficile. Connexions : VCC = 3,3V, GND = GND, CH_PD = 3,3V, TX = Broche 2, RX = Broche 3. Pour la production, utiliser un convertisseur de niveau 5V vers 3,3V sur les broches 2 et 3.
L'ESP-01 est un autre microcontrôleur. Pour communiquer, il faut utiliser la communication série. L'Arduino UNO utilise par défaut les broches 0 et 1 pour RX/TX, mais elles servent aussi au débogage USB. Il est donc recommandé d'inclure SoftwareSerial.h et de définir des broches personnalisées.
#include <SoftwareSerial.h>
SoftwareSerial espSerial(3, 2); // RX, TX
void setup() {
Serial.begin(9600);
espSerial.begin(115200); // Passer à 9600 après AT+UART_DEF=9600,8,1,0,0
while (!Serial);
}
void loop() {
if (espSerial.available()) Serial.write(espSerial.read());
if (Serial.available()) espSerial.write(Serial.read());
}
En exécutant ce script, vous pouvez entrer des commandes AT dans le moniteur série. La communication série étant sujette aux erreurs, j'ai réduit le baud rate de l'ESP de 115200 à 9600.
Étape 4 : La boîte en bois
Le cadre était prévu pour stocker toute l'électronique et trois pots d'herbes du supermarché. J'ai mesuré tous les composants et structuré les positions. Quatre capteurs d'humidité, deux pompes, l'Arduino + shield, un shield 4 relais, une prise USB et des fils doivent tenir dans la boîte. Réalisée en bois de hêtre pour la robustesse et la résistance à l'eau sans vernis supplémentaire.
Les cercles ont été découpés à la scie sauteuse sur une table de découpe maison. Les supports de plantes sont collés à l'intérieur des cercles avec de la colle chaude. Les parois sont collées à la colle à bois D3 (résistance à l'eau). Pas de vis ni clous sauf pour le panneau inférieur.
Tous les circuits, fils et tubes d'eau sont placés à l'intérieur, les capteurs et tubes pour le réservoir d'eau extérieur sortent. Avant de fermer, j'ai ajouté des soucoupes pour éviter que l'eau ne noie l'électronique.
Étape 5 : Le site web / API
L'API et le site web sont basés sur jQuery, Bootstrap, X-editable (formulaires AJAX inline) et Chart.js (graphique d'humidité), codés en PHP. Sur le site, on configure les paramètres de l'Arduino (broches capteurs, intervalle de vérification, pompes par plante, broches VCC pompes, broche VCC lampe) et on voit l'humidité actuelle + graphique.
La configuration est fournie en JSON pour l'Arduino. Au démarrage et à intervalles réguliers, la boîte vérifie les nouveaux paramètres. Pour parser le JSON, j'ai utilisé la bibliothèque ArduinoJson. Pour l'intervalle de sondage, j'ai utilisé StensTimer.
Étape 6 : Intégration Alexa
Le site web fournit une API pour la communication Alexa. Il sert de hub pour recevoir le JSON de requête d'Alexa et le traduire en JSON personnalisé pour l'Arduino (ex: lampe allumée, irriguer plante 1...). L'Arduino sonde les nouvelles actions et les exécute.
Comme les requêtes vocales sont plus complexes que de simples on/off, j'ai implémenté une Skill Alexa (pas Alexa Smart Home). AWS Lambda transmet le JSON de requête à mon API qui parse les intentions.
var https = require('https');
exports.handler = (event, context, callback) => {
var postData = JSON.stringify(event);
var options = {
host: '<MON-SERVEUR>',
path: '<MON-API-SITE>',
port: 443,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': postData.length,
}
};
var postRequest = https.request(options, function(res) {
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('Response: ' + chunk);
callback(null, JSON.parse(chunk));
});
});
postRequest.write(postData);
postRequest.end();
};
Extrait des intentions utilisées :
- ReadHumidityIntent : « Comment vont mes plantes » / « Comment va mon {nomPlante} »
- IrrigatePlantIntent : « Irrigue mes plantes » / « Irrigue mon {nomPlante} pendant {duréeSecondes} secondes »
- SwitchIntent : « Allume/éteins la lampe {état} »
- ReadIrrigateIntent : « Quelles plantes ont besoin d'eau »
- ReadLastIrrigationIntent : « Quand a eu lieu la dernière irrigation de mon {nomPlante} »
Support multilingue (allemand/anglais) ajouté.
Le résultat
Une boîte en bois pour y mettre des pots d'herbes du supermarché, avec tubes d'eau et capteurs d'humidité dans la terre, tubes dans un réservoir externe. Avec l'intégration Alexa, on peut dire :
- « Alexa, demande à la boîte à herbes comment vont mes plantes » → « Plante 1 va bien, Plante 2 est sèche... »
- « Alexa, dis à la boîte à herbes d'irriguer mon basilic pendant 5 secondes » → « Irrigation du basilic pendant 5 secondes »
- « Alexa, demande à la boîte à herbes quelles plantes ont besoin d'irrigation » → « Plante 1 est sèche, Plante 3 est sèche... »
- « Alexa, demande à la boîte à herbes quand a eu lieu la dernière irrigation de mon basilic » → « Dernière irrigation du basilic il y a 36h »
- « Alexa, dis à la boîte à herbes d'allumer la lampe » → « Lampe de croissance allumée »
Fonctionnalités prévues
- Mode économie d'énergie pour le code Arduino
- Ajouter des Arduino Nano externes avec communication sans fil (2,4 GHz) pour mesurer l'humidité d'autres plantes (la boîte est le hub WiFi) – alimentation par batteries uniquement
- Étendre l'API pour plusieurs instances de la boîte à herbes
- Ajouter un bouton pour irriguer et allumer la lampe sans site web ni Alexa
- Images Alexa (carte dans la réponse de la skill)
Mise à jour 23.03.2018
Ajout de deux nouvelles intentions importantes pour la fonctionnalité prévue d'Arduino Nano externes qui ne font que logger l'humidité :
- Quelles plantes sont sèches
- Quand a eu lieu la dernière irrigation
Code
Fichiers fournis :
- EcoActionBuffer.h / .cpp
- Plant.h / .cpp
- WhiteWalnut.ino
- WhiteWalnutApi.h / .cpp
EcoActionBuffer.h
#ifndef ECOACTIONBUFFER_H
#define ECOACTIONBUFFER_H
#include "Arduino.h"
#include "StensTimer.h"
struct EcoActionBuffer : public IStensTimerListener {
long entryNo;
int action;
int pin;
long duration;
void timerCallback(Timer* timer);
void switchPin(int pin, bool value);
void readStack();
void process();
void toSerial();
void reset();
};
#endif
EcoActionBuffer.cpp
#include "EcoActionBuffer.h"
#include "StensTimer.h"
#include "WhiteWalnutApi.h"
#define ACTION_ECOACTION_READ 1
#define ACTION_ECOACTION_HIGH 2
#define ACTION_ECOACTION_LOW 3
void EcoActionBuffer::readStack() {
reset();
WhiteWalnutApi::receiveActionFromStack(*this);
if (entryNo != 0) {
process();
}
}
void EcoActionBuffer::process() {
toSerial();
pinMode(pin, OUTPUT);
digitalWrite(pin, HIGH);
switch (action) {
case ACTION_ECOACTION_HIGH: switchPin(pin, true); break;
case ACTION_ECOACTION_LOW: switchPin(pin, false); break;
}
if (duration != 0) {
StensTimer::getInstance()->setTimer(this, -pin, duration);
}
}
void EcoActionBuffer::timerCallback(Timer* timer) {
switch (timer->getAction()) {
case ACTION_ECOACTION_READ: readStack(); break;
}
if (timer->getAction() < 0) {
switchPin(abs(timer->getAction()), false);
}
}
void EcoActionBuffer::switchPin(int pin, bool value) {
switch (value) {
case true: digitalWrite(pin, LOW); break;
case false: digitalWrite(pin, HIGH); break;
}
WhiteWalnutApi::switchPin(pin, value);
}
void EcoActionBuffer::reset() {
entryNo = 0; action = 0; pin = 0; duration = 0;
}
void EcoActionBuffer::toSerial() {
Serial.print(entryNo);
Serial.print(F(" - Action: "));
Serial.print(action);
Serial.print(F(", Pin: "));
Serial.print(pin);
Serial.print(F(", Durée: "));
Serial.print(duration);
Serial.println();
}
Plant.h
#ifndef PLANT_H
#define PLANT_H
#include "Arduino.h"
#include "StensTimer.h"
struct Plant : public IStensTimerListener {
const char* code;
int humidityDataPin;
long humidityCheckInterval;
void checkHumidity();
void timerCallback(Timer* timer);
void toSerial();
void updateApi();
};
#endif
Plant.cpp
#include "Plant.h"
#include "StensTimer.h"
#include "WhiteWalnutApi.h"
#define ACTION_PLANT_CHECKHUMIDITY 2
#define PIN_HUMIDITY_VCC 12
void Plant::checkHumidity() {
if (humidityDataPin != 0) {
Serial.print(code);
Serial.print(F(" - Vérification humidité..."));
digitalWrite(PIN_HUMIDITY_VCC, HIGH);
delay(200);
int humidity = 1023 - analogRead(humidityDataPin);
digitalWrite(PIN_HUMIDITY_VCC, LOW);
Serial.println(humidity);
WhiteWalnutApi::sendHumidity(*this, humidity);
if (humidityCheckInterval == 0) humidityCheckInterval = 60000;
StensTimer::getInstance()->setTimer(this, ACTION_PLANT_CHECKHUMIDITY, humidityCheckInterval);
} else {
StensTimer::getInstance()->setTimer(this, ACTION_PLANT_CHECKHUMIDITY, 60000);
}
}
void Plant::updateApi() {
WhiteWalnutApi::updatePlant(*this);
pinMode(PIN_HUMIDITY_VCC, OUTPUT);
toSerial();
}
void Plant::timerCallback(Timer* timer) {
switch (timer->getAction()) {
case ACTION_PLANT_CHECKHUMIDITY: checkHumidity(); break;
}
}
void Plant::toSerial() {
Serial.print(code);
Serial.print(F(" - DataPin: "));
Serial.print(humidityDataPin);
Serial.print(F(", Intervalle: "));
Serial.print(humidityCheckInterval);
Serial.println();
}
WhiteWalnut.ino
#include "EcoActionBuffer.h"
#include "Plant.h"
#include "StensTimer.h"
#include "WhiteWalnutApi.h"
struct TimerHelper : public IStensTimerListener {
public: void updateApi(); void timerCallback(Timer* timer);
};
StensTimer* stensTimer;
TimerHelper apiTimer;
Plant leftPlant, centerPlant, rightPlant, externalPlant;
EcoActionBuffer actionBuffer;
#define ACTION_PLANT_UPDATE 1
#define ACTION_ECOACTION_READ 1
void setup() {
Serial.begin(9600);
while (!Serial);
stensTimer = StensTimer::getInstance();
leftPlant.code = "LEFT";
centerPlant.code = "CENTER";
rightPlant.code = "RIGHT";
externalPlant.code = "EXTERNAL";
while(!WhiteWalnutApi::connectToWiFi()) delay(2000);
WhiteWalnutApi::switchPin(0, false);
apiTimer.updateApi();
leftPlant.checkHumidity();
centerPlant.checkHumidity();
rightPlant.checkHumidity();
externalPlant.checkHumidity();
actionBuffer.readStack();
StensTimer::getInstance()->setInterval(&apiTimer, ACTION_PLANT_UPDATE, 60000);
StensTimer::getInstance()->setInterval(&actionBuffer, ACTION_ECOACTION_READ, 1000);
}
void loop() {
stensTimer->run();
}
void TimerHelper::updateApi() {
leftPlant.updateApi();
centerPlant.updateApi();
rightPlant.updateApi();
externalPlant.updateApi();
}
void TimerHelper::timerCallback(Timer* timer){
switch (timer->getAction()) {
case ACTION_PLANT_UPDATE: updateApi(); break;
}
}
WhiteWalnutApi.cpp
#include "Arduino.h"
#include "ArduinoJson.h"
#include "EcoActionBuffer.h"
#include "MemoryFree.h"
#include "Plant.h"
#include "SoftwareSerial.h"
#include "WhiteWalnutApi.h"
SoftwareSerial espSerial(3, 2);
const char* ssid = "";
const char* pass = "";
const char* API_SERVER = "";
const char* API_PLANT = "";
const char* API_ACTION = "";
char* findOK = "OK";
char* findRY = "ready";
char* findGT = ">";
char* findDP = ":";
char* findHD = "\r\n\r\n";
char* findBT = "\r\n";
bool WhiteWalnutApi::connectToWiFi() {
espSerial.begin(9600);
espSerial.setTimeout(3000);
while (espSerial.available()) Serial.write(espSerial.read());
Serial.println(F("[ESP] Connexion WiFi"));
espSerial.println(F("AT+CIPSTATUS=2"));
if (!espSerial.find(findOK)) {
espSerial.setTimeout(10000);
Serial.println(F("[ESP] Reset Module"));
espSerial.println(F("AT+RST"));
if (!espSerial.find(findRY)) { Serial.println(F("[ESP] Échec reset")); return false; }
Serial.println(F("[ESP] Set CWMode"));
espSerial.println(F("AT+CWMODE=1"));
if (!espSerial.find(findOK)) { Serial.println(F("[ESP] Échec mode")); return false; }
Serial.println(F("[ESP] Connexion au routeur"));
espSerial.print(F("AT+CWJAP=\"")); espSerial.print(ssid); espSerial.print(F("\",\"")); espSerial.print(pass); espSerial.println("\"");
if (!espSerial.find(findOK)) { Serial.println(F("[ESP] Échec connexion WiFi")); return false; }
}
espSerial.setTimeout(3000);
Serial.println(F("[ESP] WiFi connecté"));
return true;
}
void WhiteWalnutApi::updatePlant(Plant& plant) {
String site = String(API_PLANT) + "?action=get&code=" + String(plant.code);
while (!httpRequest(site)) connectToWiFi();
JsonObject& root = parseJson();
if (root.success()) {
plant.humidityDataPin = root["dataPin"].as();
plant.humidityCheckInterval = atol(root["interval"].as());
}
}
void WhiteWalnutApi::sendHumidity(Plant& plant, int humidity) {
String site = String(API_PLANT) + "?action=humidity&code=" + String(plant.code) + "&humidity=" + String(humidity);
while (!httpRequest(site)) connectToWiFi();
}
void WhiteWalnutApi::sendHeartbeat(Plant& plant) {
String site = String(API_PLANT) + "?action=heartbeat&code=" + String(plant.code);
while (!httpRequest(site)) connectToWiFi();
}
void WhiteWalnutApi::receiveActionFromStack(EcoActionBuffer& actionBuffer) {
while (!httpRequest(String(API_ACTION))) connectToWiFi();
JsonObject& root = parseJson();
if (root.success()) {
actionBuffer.entryNo = atol(root["entryNo"].as());
actionBuffer.action = root["actionEnum"].as();
actionBuffer.pin = root["pin"].as();
actionBuffer.duration = atol(root["value"].as());
}
}
void WhiteWalnutApi::updateActionOnStack(EcoActionBuffer& actionBuffer) {
String site = String(API_ACTION) + "?action=processed&entryNo=" + String(actionBuffer.entryNo);
while (!httpRequest(site)) connectToWiFi();
}
void WhiteWalnutApi::switchPin(int pin, bool value) {
String site = String(API_ACTION) + "?action=switch&pin=" + String(pin) + "&value=" + String(value);
while (!httpRequest(site)) connectToWiFi();
}
bool WhiteWalnutApi::httpRequest(String site) {
int cmdLength = 44 + site.length() + strlen(API_SERVER);
espSerial.print(F("AT+CIPSTART=\"TCP\",\"")); espSerial.print(API_SERVER); espSerial.println(F("\",80"));
if (!espSerial.find(findOK)) { Serial.println(F("[ESP] Erreur connexion TCP")); return false; }
espSerial.print(F("AT+CIPSEND=")); espSerial.println(cmdLength);
if (!espSerial.find(findGT)) { Serial.println(F("[ESP] Erreur état envoi")); return false; }
espSerial.print(F("GET ")); espSerial.print(site); espSerial.print(F(" HTTP/1.0\r\n"));
espSerial.print(F("Host: ")); espSerial.print(API_SERVER); espSerial.print(F("\r\n"));
espSerial.print(F("Connection: close\r\n")); espSerial.println();
if (!espSerial.find(findDP)) { Serial.println(F("Octets non envoyés")); espSerial.print(F("AT+CIPCLOSE")); return false; }
char status[32] = {0};
espSerial.readBytesUntil('\r', status, sizeof(status));
if (strcmp(status, "HTTP/1.1 200 OK") != 0) { Serial.print(F("[ESP] Réponse inattendue: ")); Serial.println(status); return false; }
if (!espSerial.find(findHD)) { Serial.println(F("[ESP] Réponse invalide")); return false; }
return true;
}
JsonObject& WhiteWalnutApi::parseJson() {
const size_t capacity = JSON_OBJECT_SIZE(3) + JSON_ARRAY_SIZE(2) + 60;
DynamicJsonBuffer jsonBuffer(capacity);
JsonObject& root = jsonBuffer.parseObject(espSerial);
if (!root.success()) Serial.println(F("Échec parsing!"));
return root;
}
WhiteWalnutApi.h
#ifndef WHITEWALNUTAPI_H
#define WHITEWALNUTAPI_H
#include "Arduino.h"
#include "ArduinoJson.h"
#include "EcoActionBuffer.h"
#include "Plant.h"
class WhiteWalnutApi {
public:
static bool connectToWiFi();
static void updatePlant(Plant& plant);
static void sendHumidity(Plant& plant, int humidity);
static void sendHeartbeat(Plant& plant);
static void receiveActionFromStack(EcoActionBuffer& actionBuffer);
static void updateActionOnStack(EcoActionBuffer& actionBuffer);
static void switchPin(int pin, bool value);
private:
static bool httpRequest(String site);
static JsonObject& parseJson();
};
#endif
Schémas
Graphique sur la communication et les interfaces. Toutes les intentions implémentées pour Alexa (multilingue). Étapes d'entrée pour le parsing JSON Alexa.
Processus de fabrication
- Système d'éclairage DMX piloté par web – Contrôleur maître Arduino
- Construire un Spybot Arduino piloté par Bluetooth
- SERENA : Système d'Alarme Arduino Mega 2560 Personnalisé avec Écran Tactile TFT LCD
- Station Météo Compacte : Arduino Nano & ESP8266 avec Capteur BMP280 et Affichage OLED/Nokia LCD
- Système de Ventilation Intelligent pour Sous-Sol : Contrôle d'Humidité par Arduino
- Système de pointage RFID temps réel avec Arduino et Google Sheets
- Shield de programmation Arduino sans fil avec Bluetooth HC-05 – Plus besoin d'USB
- Système avancé d'alerte anti-collision Arduino – Alertes visuelles et sonores pour sécurité véhicule
- Shield TFT 2,8" haute performance pour Arduino Nano – Écran SPI 320×240