From 8db002785bc4d6516150dcfd741bd23d6eae5271 Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sat, 7 Sep 2024 13:54:25 -0700 Subject: [PATCH 1/9] Add WifiManager and MQTT config to controller --- Taskfile.yml | 5 + docs/controller_advanced.md | 2 - docs/hydroponics_example.md | 2 - docs/indoor_example.md | 2 - docs/sensors_only_example.md | 2 - garden-app/controller/generate_config.go | 1 - garden-app/controller/generate_config_test.go | 4 - garden-controller/include/config.h | 5 +- garden-controller/include/mqtt.h | 23 ++-- garden-controller/include/wifi_manager.h | 29 +++++ garden-controller/platformio.ini | 2 + garden-controller/src/dht22.cpp | 8 +- garden-controller/src/main.cpp | 2 + garden-controller/src/mqtt.cpp | 61 ++++----- garden-controller/src/wifi_manager.cpp | 121 ++++++++++++++++++ 15 files changed, 205 insertions(+), 64 deletions(-) create mode 100644 garden-controller/include/wifi_manager.h create mode 100644 garden-controller/src/wifi_manager.cpp diff --git a/Taskfile.yml b/Taskfile.yml index d06c7262..fcbb42f8 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -135,3 +135,8 @@ tasks: dir: ./garden-app cmds: - flyctl deploy + + pio: + dir: ./garden-controller + cmds: + - pio {{ .CLI_ARGS | default "run" }} diff --git a/docs/controller_advanced.md b/docs/controller_advanced.md index 4011b96b..3dcd29f0 100644 --- a/docs/controller_advanced.md +++ b/docs/controller_advanced.md @@ -25,8 +25,6 @@ These are the basic options that are required and do not fit in specific categor `QUEUE_SIZE`: maximum number of messages that can be queued in FreeRTOS queues. 10 is a sensible default that should never overflow unless you have a large number of Zones -`JSON_CAPACITY`: Size of JSON object calculated using Arduino JSON Assistant. This should not be changed - ### MQTT/WiFi Options These are all the configurations for setting up MQTT publish/subscribe. diff --git a/docs/hydroponics_example.md b/docs/hydroponics_example.md index 0b45eebe..c313cd49 100644 --- a/docs/hydroponics_example.md +++ b/docs/hydroponics_example.md @@ -54,8 +54,6 @@ This has a very basic setup since it just consists of the ESP32 and a single rel #ifdef ENABLE_MQTT_LOGGING #define MQTT_LOGGING_TOPIC TOPIC_PREFIX"/data/logs" #endif - -#define JSON_CAPACITY 48 #endif #define NUM_ZONES 1 diff --git a/docs/indoor_example.md b/docs/indoor_example.md index 99f28a17..b3ab4974 100644 --- a/docs/indoor_example.md +++ b/docs/indoor_example.md @@ -63,8 +63,6 @@ Then there are two 6-pin JST connectors that provide power (ground/5V) and 4 dat #ifdef ENABLE_MQTT_LOGGING #define MQTT_LOGGING_TOPIC TOPIC_PREFIX"/data/logs" #endif - -#define JSON_CAPACITY 48 #endif #define NUM_ZONES 3 diff --git a/docs/sensors_only_example.md b/docs/sensors_only_example.md index b5057cb9..c1407cfd 100644 --- a/docs/sensors_only_example.md +++ b/docs/sensors_only_example.md @@ -35,8 +35,6 @@ Notice that in this example, `GPIO_NUM_MAX` is used as the moisture sensor pin o #define MQTT_PORT 30002 #define MQTT_CLIENT_NAME TOPIC_PREFIX"-sensors" -#define JSON_CAPACITY 48 - #define DISABLE_WATERING #define NUM_ZONES 3 #define PUMP_PIN GPIO_NUM_18 diff --git a/garden-app/controller/generate_config.go b/garden-app/controller/generate_config.go index e82a52ad..e4e478ea 100644 --- a/garden-app/controller/generate_config.go +++ b/garden-app/controller/generate_config.go @@ -31,7 +31,6 @@ const ( #define ENABLE_MQTT_LOGGING -#define JSON_CAPACITY 48 #endif {{ if .DisableWatering }} diff --git a/garden-app/controller/generate_config_test.go b/garden-app/controller/generate_config_test.go index dcc5ea9e..e36cb077 100644 --- a/garden-app/controller/generate_config_test.go +++ b/garden-app/controller/generate_config_test.go @@ -53,7 +53,6 @@ func TestGenerateMainConfig(t *testing.T) { #define ENABLE_MQTT_LOGGING -#define JSON_CAPACITY 48 #endif #define NUM_ZONES 1 @@ -109,7 +108,6 @@ func TestGenerateMainConfig(t *testing.T) { #define ENABLE_MQTT_LOGGING -#define JSON_CAPACITY 48 #endif #define NUM_ZONES 1 @@ -170,7 +168,6 @@ func TestGenerateMainConfig(t *testing.T) { #define ENABLE_MQTT_LOGGING -#define JSON_CAPACITY 48 #endif #define DISABLE_WATERING @@ -225,7 +222,6 @@ func TestGenerateMainConfig(t *testing.T) { #define ENABLE_MQTT_LOGGING -#define JSON_CAPACITY 48 #endif #define NUM_ZONES 4 diff --git a/garden-controller/include/config.h b/garden-controller/include/config.h index 16f11e8c..1a0e5f6b 100644 --- a/garden-controller/include/config.h +++ b/garden-controller/include/config.h @@ -2,7 +2,7 @@ #define config_h // Unique prefix for this controller. It is used for the root of MQTT topics and as the MQTT ClientID -#define TOPIC_PREFIX "garden" +#define TOPIC_PREFIX "test-garden" // Size of FreeRTOS queues #define QUEUE_SIZE 10 @@ -25,9 +25,6 @@ // Enable logging messages to MQTT #define ENABLE_MQTT_LOGGING - // Size of JSON object calculated using Arduino JSON Assistant -#define JSON_CAPACITY 48 - /** * Garden Configurations * diff --git a/garden-controller/include/mqtt.h b/garden-controller/include/mqtt.h index 4ce5128e..e434c9f4 100644 --- a/garden-controller/include/mqtt.h +++ b/garden-controller/include/mqtt.h @@ -25,30 +25,29 @@ * MQTT_WATER_DATA_TOPIC * Topic to publish watering metrics on */ -#define MQTT_CLIENT_NAME TOPIC_PREFIX -#define MQTT_WATER_TOPIC TOPIC_PREFIX"/command/water" -#define MQTT_STOP_TOPIC TOPIC_PREFIX"/command/stop" -#define MQTT_STOP_ALL_TOPIC TOPIC_PREFIX"/command/stop_all" -#define MQTT_LIGHT_TOPIC TOPIC_PREFIX"/command/light" -#define MQTT_LIGHT_DATA_TOPIC TOPIC_PREFIX"/data/light" -#define MQTT_WATER_DATA_TOPIC TOPIC_PREFIX"/data/water" +#define MQTT_WATER_TOPIC "/command/water" +#define MQTT_STOP_TOPIC "/command/stop" +#define MQTT_STOP_ALL_TOPIC "/command/stop_all" +#define MQTT_LIGHT_TOPIC "/command/light" +#define MQTT_LIGHT_DATA_TOPIC "/data/light" +#define MQTT_WATER_DATA_TOPIC "/data/water" #ifdef ENABLE_MQTT_LOGGING -#define MQTT_LOGGING_TOPIC TOPIC_PREFIX"/data/logs" +#define MQTT_LOGGING_TOPIC "/data/logs" #endif #ifdef ENABLE_MQTT_HEALTH -#define MQTT_HEALTH_DATA_TOPIC TOPIC_PREFIX"/data/health" +#define MQTT_HEALTH_DATA_TOPIC "/data/health" #define HEALTH_PUBLISH_INTERVAL 60000 #endif #ifdef ENABLE_DHT22 -#define MQTT_TEMPERATURE_DATA_TOPIC TOPIC_PREFIX"/data/temperature" -#define MQTT_HUMIDITY_DATA_TOPIC TOPIC_PREFIX"/data/humidity" +#define MQTT_TEMPERATURE_DATA_TOPIC "/data/temperature" +#define MQTT_HUMIDITY_DATA_TOPIC "/data/humidity" #endif #ifdef ENABLE_MOISTURE_SENSORS -#define MQTT_MOISTURE_DATA_TOPIC TOPIC_PREFIX"/data/moisture" +#define MQTT_MOISTURE_DATA_TOPIC "/data/moisture" #endif extern PubSubClient client; diff --git a/garden-controller/include/wifi_manager.h b/garden-controller/include/wifi_manager.h new file mode 100644 index 00000000..96fd76c9 --- /dev/null +++ b/garden-controller/include/wifi_manager.h @@ -0,0 +1,29 @@ +#ifndef wifi_manager_h +#define wifi_manager_h + +#include +#include +#include + +#define FORMAT_LITTLEFS_IF_FAILED true + +extern WiFiManagerParameter custom_mqtt_server; +extern WiFiManagerParameter custom_mqtt_topic_prefix; +extern WiFiManagerParameter custom_mqtt_port; + +extern WiFiManager wifiManager; + +// TODO: use these variables for MQTT setup +// It looks like it will be difficult to refactor everything to use the new MQTT configuration. +// My options are to ditch that feature for now since I am just using WifiManager for Wifi + OTA +// and don't need to immediately connect to MQTT yet since I am not doing OTA or configs over MQTT +extern char* mqtt_server; +extern char* mqtt_topic_prefix; +extern int mqtt_port; + +void setupWifiManager(); +void mqttLoopTask(void* parameters); + +extern TaskHandle_t wifiManagerLoopTaskHandle; + +#endif diff --git a/garden-controller/platformio.ini b/garden-controller/platformio.ini index e2d106f5..8c10b300 100644 --- a/garden-controller/platformio.ini +++ b/garden-controller/platformio.ini @@ -13,8 +13,10 @@ platform = espressif32 board = esp32dev framework = arduino monitor_speed = 115200 +board_build.filesystem = littlefs lib_deps = bblanchon/ArduinoJson@^6.21.3 knolleary/PubSubClient@^2.8 adafruit/DHT sensor library@^1.4.4 adafruit/Adafruit Unified Sensor@^1.1.11 + tzapu/WiFiManager@^2.0.17 diff --git a/garden-controller/src/dht22.cpp b/garden-controller/src/dht22.cpp index 706df129..1f1bd217 100644 --- a/garden-controller/src/dht22.cpp +++ b/garden-controller/src/dht22.cpp @@ -6,15 +6,19 @@ #include "main.h" #include "mqtt.h" #include "DHT.h" +#include "wifi_manager.h" TaskHandle_t dht22TaskHandle; -const char* temperatureDataTopic = MQTT_TEMPERATURE_DATA_TOPIC; -const char* humidityDataTopic = MQTT_HUMIDITY_DATA_TOPIC; +char temperatureDataTopic[50]; +char humidityDataTopic[50]; DHT dht(DHT22_PIN, DHT22); void setupDHT22() { + snprintf(temperatureDataTopic, sizeof(temperatureDataTopic), "%s" MQTT_TEMPERATURE_DATA_TOPIC, mqtt_topic_prefix); + snprintf(humidityDataTopic, sizeof(humidityDataTopic), "%s" MQTT_TEMPERATURE_DATA_TOPIC, mqtt_topic_prefix); + dht.begin(); xTaskCreate(dht22PublishTask, "DHT22Task", 2048, NULL, 1, &dht22TaskHandle); } diff --git a/garden-controller/src/main.cpp b/garden-controller/src/main.cpp index f28db532..896f9cff 100644 --- a/garden-controller/src/main.cpp +++ b/garden-controller/src/main.cpp @@ -6,6 +6,7 @@ #include "config.h" #include "mqtt.h" #include "main.h" +#include "wifi_manager.h" #ifdef ENABLE_BUTTONS #include "buttons.h" #endif @@ -47,6 +48,7 @@ void setup() { light_state = 0; #endif + setupWifiManager(); setupWifi(); setupMQTT(); #ifdef ENABLE_MOISTURE_SENSORS diff --git a/garden-controller/src/mqtt.cpp b/garden-controller/src/mqtt.cpp index ec5e8294..e9902ffc 100644 --- a/garden-controller/src/mqtt.cpp +++ b/garden-controller/src/mqtt.cpp @@ -1,5 +1,6 @@ #include "mqtt.h" #include "main.h" +#include "wifi_manager.h" WiFiClient wifiClient; PubSubClient client(wifiClient); @@ -14,40 +15,31 @@ QueueHandle_t lightPublisherQueue; TaskHandle_t lightPublisherTaskHandle; #endif -#ifdef DISABLE_WATERING -const char* waterCommandTopic = ""; -const char* stopCommandTopic = ""; -const char* stopAllCommandTopic = ""; -const char* waterDataTopic = ""; -#else -const char* waterCommandTopic = MQTT_WATER_TOPIC; -const char* stopCommandTopic = MQTT_STOP_TOPIC; -const char* stopAllCommandTopic = MQTT_STOP_ALL_TOPIC; -const char* waterDataTopic = MQTT_WATER_DATA_TOPIC; -#endif - -#ifdef LIGHT_PIN -const char* lightCommandTopic = MQTT_LIGHT_TOPIC; -const char* lightDataTopic = MQTT_LIGHT_DATA_TOPIC; -#else -const char* lightCommandTopic = ""; -const char* lightDataTopic = ""; -#endif - -#ifdef ENABLE_MQTT_HEALTH -const char* healthDataTopic = MQTT_HEALTH_DATA_TOPIC; -#else -const char* healthDataTopic = ""; -#endif +char waterCommandTopic[50]; +char stopCommandTopic[50]; +char stopAllCommandTopic[50]; +char waterDataTopic[50]; +char lightCommandTopic[50]; +char lightDataTopic[50]; +char healthDataTopic[50]; #define ZERO (unsigned long int) 0 void setupMQTT() { // Connect to MQTT - client.setServer(MQTT_ADDRESS, MQTT_PORT); + printf("connecting to mqtt server: %s:%d\n", mqtt_server, mqtt_port); + client.setServer(mqtt_server, mqtt_port); client.setCallback(processIncomingMessage); client.setKeepAlive(MQTT_KEEPALIVE); + snprintf(waterCommandTopic, sizeof(waterCommandTopic), "%s" MQTT_WATER_TOPIC, mqtt_topic_prefix); + snprintf(stopCommandTopic, sizeof(stopCommandTopic), "%s" MQTT_STOP_TOPIC, mqtt_topic_prefix); + snprintf(stopAllCommandTopic, sizeof(stopAllCommandTopic), "%s" MQTT_STOP_ALL_TOPIC, mqtt_topic_prefix); + snprintf(waterDataTopic, sizeof(waterDataTopic), "%s" MQTT_WATER_DATA_TOPIC, mqtt_topic_prefix); + snprintf(lightCommandTopic, sizeof(lightCommandTopic), "%s" MQTT_LIGHT_TOPIC, mqtt_topic_prefix); + snprintf(lightDataTopic, sizeof(lightDataTopic), "%s" MQTT_LIGHT_DATA_TOPIC, mqtt_topic_prefix); + snprintf(healthDataTopic, sizeof(healthDataTopic), "%s" MQTT_HEALTH_DATA_TOPIC, mqtt_topic_prefix); + // Initialize publisher Queue waterPublisherQueue = xQueueCreate(QUEUE_SIZE, sizeof(WaterEvent)); if (waterPublisherQueue == NULL) { @@ -71,10 +63,12 @@ void setupMQTT() { } void setupWifi() { - delay(10); - printf("Connecting to " SSID " as " TOPIC_PREFIX "-controller\n"); + char hostname[50]; + snprintf(hostname, sizeof(hostname), "%s-controller", mqtt_topic_prefix); + WiFi.setHostname(hostname); - WiFi.setHostname(TOPIC_PREFIX"-controller"); + #if defined(SSID) && defined(PASSWORD) + printf(strcat("Connecting to " SSID " as ", mqtt_topic_prefix, "-controller\n")); WiFi.begin(SSID, PASSWORD); while (WiFi.status() != WL_CONNECTED) { @@ -83,6 +77,7 @@ void setupWifi() { } printf("Wifi connected...\n"); + #endif // Create event handler tp recpnnect to WiFi WiFi.onEvent(wifiDisconnectHandler, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); @@ -120,7 +115,7 @@ void lightPublisherTask(void* parameters) { while (true) { if (xQueueReceive(lightPublisherQueue, &state, portMAX_DELAY)) { char message[50]; - sprintf(message, "light,garden=\"%s\" state=%d", TOPIC_PREFIX, state); + sprintf(message, "light,garden=\"%s\" state=%d", mqtt_topic_prefix, state); if (client.connected()) { printf("publishing to MQTT:\n\ttopic=%s\n\tmessage=%s\n", lightDataTopic, message); client.publish(lightDataTopic, message); @@ -142,7 +137,7 @@ void healthPublisherTask(void* parameters) { WaterEvent we; while (true) { char message[50]; - sprintf(message, "health garden=\"%s\"", TOPIC_PREFIX); + sprintf(message, "health garden=\"%s\"", mqtt_topic_prefix); if (client.connected()) { printf("publishing to MQTT:\n\ttopic=%s\n\tmessage=%s\n", healthDataTopic, message); client.publish(healthDataTopic, message); @@ -164,7 +159,7 @@ void mqttConnectTask(void* parameters) { if (!client.connected()) { printf("attempting MQTT connection..."); // Connect with defaul arguments + cleanSession = false for persistent sessions - if (client.connect(MQTT_CLIENT_NAME, NULL, NULL, 0, 0, 0, 0, false)) { + if (client.connect(mqtt_topic_prefix, NULL, NULL, 0, 0, 0, 0, false)) { printf("connected\n"); #ifndef DISABLE_WATERING client.subscribe(waterCommandTopic, 1); @@ -210,7 +205,7 @@ void mqttLoopTask(void* parameters) { void processIncomingMessage(char* topic, byte* message, unsigned int length) { printf("message received:\n\ttopic=%s\n\tmessage=%s\n", topic, (char*)message); - StaticJsonDocument doc; + DynamicJsonDocument doc(1024); DeserializationError err = deserializeJson(doc, message); if (err) { printf("deserialize failed: %s\n", err.c_str()); diff --git a/garden-controller/src/wifi_manager.cpp b/garden-controller/src/wifi_manager.cpp new file mode 100644 index 00000000..98236c57 --- /dev/null +++ b/garden-controller/src/wifi_manager.cpp @@ -0,0 +1,121 @@ +#include "wifi_manager.h" +#include "config.h" + +char* mqtt_server = new char(); +char* mqtt_topic_prefix = new char(); +int mqtt_port; + +WiFiManagerParameter custom_mqtt_server("server", "mqtt server", "", 40); +WiFiManagerParameter custom_mqtt_topic_prefix("topic_prefix", "mqtt topic prefix", "", 40); +WiFiManagerParameter custom_mqtt_port("port", "mqtt port", "8080", 6); + +WiFiManager wifiManager; + +TaskHandle_t wifiManagerLoopTaskHandle; + +void saveConfig() { + // read updated parameters + strcpy(mqtt_server, custom_mqtt_server.getValue()); + strcpy(mqtt_topic_prefix, custom_mqtt_topic_prefix.getValue()); + mqtt_port = atoi(custom_mqtt_port.getValue()); + + DynamicJsonDocument json(1024); + json["mqtt_server"] = mqtt_server; + json["mqtt_port"] = mqtt_port; + json["mqtt_topic_prefix"] = mqtt_topic_prefix; + + File configFile = LittleFS.open("/config.json", "w"); + if (!configFile) { + printf("failed to open config file for writing\n"); + } + + serializeJson(json, configFile); + configFile.close(); +} + +void setupFS() { + printf("setting up filesystem\n"); + + // start with defaults + strcpy(mqtt_server, MQTT_ADDRESS); + strcpy(mqtt_topic_prefix, TOPIC_PREFIX); + mqtt_port = MQTT_PORT; + + if (!LittleFS.begin(FORMAT_LITTLEFS_IF_FAILED)) { + printf("failed to mount FS\n"); + return; + } + printf("successfully mounted FS\n"); + + + if (!LittleFS.exists("/config.json")) { + printf("config doesn't exist\n"); + return; + } + printf("config file exists\n"); + + // file exists, reading and loading + File configFile = LittleFS.open("/config.json", "r"); + if (!configFile) { + return; + } + printf("opened config file\n"); + + size_t size = configFile.size(); + + // Allocate a buffer to store contents of the file. + std::unique_ptr buf(new char[size]); + + configFile.readBytes(buf.get(), size); + + DynamicJsonDocument json(1024); + auto deserializeError = deserializeJson(json, buf.get()); + if (!deserializeError) { + strcpy(mqtt_server, json["mqtt_server"]); + strcpy(mqtt_topic_prefix, json["mqtt_topic_prefix"]); + mqtt_port = json["mqtt_port"]; + + printf("loaded config JSON: %s %s %d\n", mqtt_server, mqtt_topic_prefix, mqtt_port); + } else { + printf("failed to load json config\n"); + } + configFile.close(); +} + +/* + wifiManagerLoopTask will run the WifiManager process loop +*/ +void wifiManagerLoopTask(void* parameters) { + while (true) { + wifiManager.process(); + vTaskDelay(5 / portTICK_PERIOD_MS); + } + vTaskDelete(NULL); +} + +void setupWifiManager() { + wifiManager.setSaveConfigCallback(saveConfig); + wifiManager.setSaveParamsCallback(saveConfig); + + wifiManager.addParameter(&custom_mqtt_server); + wifiManager.addParameter(&custom_mqtt_topic_prefix); + wifiManager.addParameter(&custom_mqtt_port); + + // wifiManager.resetSettings(); + + setupFS(); + + if (!wifiManager.autoConnect("GardenControllerSetup", "password")) { + printf("failed to connect and hit timeout\n"); + delay(3000); + // reset and try again, or maybe put it to deep sleep + ESP.restart(); + delay(5000); + } + + wifiManager.setParamsPage(true); + wifiManager.setConfigPortalBlocking(false); + wifiManager.startWebPortal(); + + xTaskCreate(wifiManagerLoopTask, "WifiManagerLoopTask", 4096, NULL, 1, &wifiManagerLoopTaskHandle); +} From 8128a7ebe8afd2343673506c5392c15c87b8cd46 Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sat, 7 Sep 2024 14:54:36 -0700 Subject: [PATCH 2/9] Simplify garden-controller firmware and config - Remove unused moisture sensing: this is impractical and inaccurate - Remove unused buttons: this is a nice feature but I don't use it or imagine it being used since I have good connectivity and UI now - Remove options for logging, watering, and health so they are always enabled --- deploy/base/configs/telegraf.conf | 1 - deploy/configs/garden-app/config.yaml | 3 - deploy/configs/telegraf/telegraf.conf | 1 - deploy/overlays/dev/configs/config.yaml | 3 - docs/README.md | 1 - docs/_sidebar.md | 1 - docs/controller_advanced.md | 40 +----- docs/controller_quickstart.md | 13 +- docs/hydroponics_example.md | 37 +---- docs/indoor_example.md | 49 +------ docs/rest_api.md | 1 - docs/sensors_only_example.md | 57 -------- docs/weather_control.md | 14 -- garden-app/api/openapi.yaml | 27 +--- garden-app/cmd/controller.go | 21 +-- garden-app/controller/controller.go | 81 +---------- garden-app/controller/generate_config.go | 46 ++---- garden-app/controller/generate_config_test.go | 82 +++++------ garden-app/controller/interactive.go | 105 +------------- .../integration_tests/testdata/config.yml | 3 - garden-app/pkg/action/zone_action.go | 5 +- garden-app/pkg/influxdb/client.go | 45 +----- garden-app/pkg/influxdb/mock_Client.go | 24 ---- garden-app/pkg/water_schedule.go | 22 +-- garden-app/pkg/water_schedule_test.go | 11 -- garden-app/pkg/weather/control.go | 20 +-- garden-app/pkg/weather/control_test.go | 17 +-- garden-app/server/garden_responses.go | 2 +- garden-app/server/water_schedule_responses.go | 2 +- garden-app/server/water_schedule_test.go | 12 -- garden-app/server/weather_data_response.go | 5 +- garden-app/server/zone.go | 10 -- garden-app/server/zone_responses.go | 13 +- garden-app/server/zone_test.go | 54 +------ garden-app/worker/water_schedule_actions.go | 28 ---- .../worker/water_schedule_actions_test.go | 66 --------- garden-controller/include/buttons.h | 16 --- garden-controller/include/config.h | 48 ++----- garden-controller/include/main.h | 4 +- garden-controller/include/moisture.h | 10 -- garden-controller/include/mqtt.h | 12 -- garden-controller/platformio.ini | 2 +- garden-controller/src/buttons.cpp | 105 -------------- garden-controller/src/dht22.cpp | 3 - garden-controller/src/main.cpp | 135 ++++++++---------- garden-controller/src/moisture.cpp | 59 -------- garden-controller/src/mqtt.cpp | 34 ++--- garden-controller/src/wifi_manager.cpp | 16 +-- 48 files changed, 172 insertions(+), 1194 deletions(-) delete mode 100644 docs/sensors_only_example.md delete mode 100644 garden-controller/include/buttons.h delete mode 100644 garden-controller/include/moisture.h delete mode 100644 garden-controller/src/buttons.cpp delete mode 100644 garden-controller/src/moisture.cpp diff --git a/deploy/base/configs/telegraf.conf b/deploy/base/configs/telegraf.conf index b9ccc1c1..9c3b29f1 100644 --- a/deploy/base/configs/telegraf.conf +++ b/deploy/base/configs/telegraf.conf @@ -20,7 +20,6 @@ topics = [ "+/data/water", "+/data/light", - "+/data/moisture", "+/data/temperature", "+/data/humidity", "+/data/logs", diff --git a/deploy/configs/garden-app/config.yaml b/deploy/configs/garden-app/config.yaml index 050788d3..9efecb4e 100644 --- a/deploy/configs/garden-app/config.yaml +++ b/deploy/configs/garden-app/config.yaml @@ -19,9 +19,6 @@ storage: controller: topic_prefix: "garden" num_zones: 3 - moisture_strategy: increasing - moisture_value: 0 - moisture_interval: 30s publish_water_event: true publish_health: true health_interval: 1m diff --git a/deploy/configs/telegraf/telegraf.conf b/deploy/configs/telegraf/telegraf.conf index a0780d3f..22ae41e1 100644 --- a/deploy/configs/telegraf/telegraf.conf +++ b/deploy/configs/telegraf/telegraf.conf @@ -20,7 +20,6 @@ topics = [ "+/data/water", "+/data/light", - "+/data/moisture", "+/data/temperature", "+/data/humidity", "+/data/logs", diff --git a/deploy/overlays/dev/configs/config.yaml b/deploy/overlays/dev/configs/config.yaml index decdfd8e..1e9ab714 100644 --- a/deploy/overlays/dev/configs/config.yaml +++ b/deploy/overlays/dev/configs/config.yaml @@ -5,9 +5,6 @@ mqtt: controller: topic_prefix: "garden" num_zones: 3 - moisture_strategy: increasing - moisture_value: 0 - moisture_interval: 30s publish_water_event: true publish_health: true health_interval: 1m diff --git a/docs/README.md b/docs/README.md index b64791e9..51cbc769 100644 --- a/docs/README.md +++ b/docs/README.md @@ -38,7 +38,6 @@ Key features include: - Control valves or devices (only limited by number of output pins) - Queue up water events to water multiple zones one after the other - Publish data and logs to InfluxDB via Telegraf + MQTT - - Respond to buttons to water individual zones and cancel watering ## Core Technologies - Arduino/FreeRTOS diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 05e6da77..5165e951 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -14,4 +14,3 @@ * Examples * [Indoor Herb Garden](indoor_example.md) * [Indoor Hydroponics](hydroponics_example.md) - * [Sensors-Only Garden Add-on](sensors_only_example.md) diff --git a/docs/controller_advanced.md b/docs/controller_advanced.md index 3dcd29f0..4eeb94ee 100644 --- a/docs/controller_advanced.md +++ b/docs/controller_advanced.md @@ -4,9 +4,7 @@ This section provides more details on the features, code organization, and confi ## Features - Highly configurable and flexible - Number of connected pumps/valves is only limited by the number of pins on your controller (and memory) -- Optionally control watering with connected buttons -- Collect moisture data from connected sensors -- Connect to MQTT to publish periodic health check-ins, moisture sensor data, logs, and event data for watering and lighting +- Connect to MQTT to publish periodic health check-ins, logs, and event data for watering and lighting ## Code Organization This code is split up into different `.ino` and header files to improve organization and separate logic. @@ -58,8 +56,6 @@ The following options should be left as defaults, unless you have a good reason #### Health Publishing Options These options are used for enabled/configuring publishing of health check-ins to MQTT. -`ENABLE_MQTT_HEALTH`: Enables periodic publishing of health check-ins when defined - `MQTT_HEALTH_DATA_TOPIC`: Topic to publish health check-ins on `HEALTH_PUBLISH_INTERVAL`: Time, in milliseconds, to wait between publishing of health check-ins @@ -67,43 +63,13 @@ These options are used for enabled/configuring publishing of health check-ins to ### Zone Options These options are related to the actual pins and other necessary information for watering zones. -`DISABLE_WATERING`: Allows disabling Pump/Valve pins and doesn't listen on relevant MQTT topics. This allows a sensor-only Garden. If you are running this alongside a separate `garden-controller` that handles watering, please remember to change the `MQTT_CLIENT_NAME` to be different - `NUM_ZONES`: Number of zones connected to this Garden `PUMP_PIN`: Optional configuration that makes organization better if you use the same pump for all zones -`ZONE_1`, `ZONE_2`, ..., `ZONE_N`: These are optional configurations that will be included in `ZONES` below, but make it a bit easier to organize the configuration. Use the following format: -``` -{PUMP_PIN, VALVE_PIN, BUTTON_PIN, MOISTURE_SENSOR_PIN} -``` - -`ZONES`: This is a list of all zones managed by this controller. It contains the pin details for pump, valve, button, and moisture sensor. The button and sensor pins are ignored if not enabled (see sections below). If you are not using a pump, or not using a valve, just use the same pin for both. Use the following format: +`ZONES`: This is a list of all zones managed by this controller. It contains the pin details for following format: ``` -{ {PUMP_PIN, VALVE_PIN, BUTTON_PIN, MOISTURE_SENSOR_PIN} } +{ ZONE1_PIN, ZONE2_PIN, ... } ``` -**note**: Use `GPIO_NUM_MAX` to disable moisture sensing for only certain Zones. - -`DEFAULT_WATER_TIME`: The default amount of time to water for, in milliseconds, if one is not defined in the command. This is also used to determine how long button-presses will water for `LIGHT_PIN`: Pin used to control grow light relay - -#### Button Options -These options allow optionally enabling button control. The buttons pins are defined as a part of the zones configuration. - -`ENABLE_BUTTONS`: Enables reading input from buttons when defined - -`STOP_BUTTON_PIN`: Button pins are usually defined for each individual zone, but this is a separate button that will cancel in-progress watering - -#### Moisture Sensor Options -These options allow optionally enabling moisture data publishing. WiFi + MQTT are also required for this since the data must be published for storage. The value configurations below are used for calibrating the sensor. The moisture sensor pins are configured as part fo the zones configuration. - -`ENABLE_MOISTURE_SENSORS`: Enables moisture sensors when defined - -`MQTT_MOISTURE_DATA_TOPIC`: MQTT topic to publish moisture data to - -`MOISTURE_SENSOR_AIR_VALUE`: Value to use for a dry sensor - -`MOISTURE_SENSOR_WATER_VALUE`: Value to use for a fully-submerged sensor - -`MOISTURE_SENSOR_INTERVAL`: Time, in milliseconds, to wait between sensor readings diff --git a/docs/controller_quickstart.md b/docs/controller_quickstart.md index 38d34967..785328d4 100644 --- a/docs/controller_quickstart.md +++ b/docs/controller_quickstart.md @@ -36,7 +36,7 @@ In this interactive mode, the CLI will walk you through each required configurat garden-app controller generate-config --config config.yaml ``` -The following `config.yaml` file creates the necessary configuration for a 3-zone garden with moisture sensing, buttons, and light control: +The following `config.yaml` file creates the necessary configuration for a 3-zone garden with light control: ```YAML mqtt: @@ -49,25 +49,14 @@ controller: zones: - pump_pin: GPIO_NUM_18 valve_pin: GPIO_NUM_16 - button_pin: GPIO_NUM_19 - moisture_sensor_pin: GPIO_NUM_36 - pump_pin: GPIO_NUM_18 valve_pin: GPIO_NUM_17 - button_pin: GPIO_NUM_21 - moisture_sensor_pin: GPIO_NUM_39 - pump_pin: GPIO_NUM_18 valve_pin: GPIO_NUM_5 - button_pin: GPIO_NUM_22 - moisture_sensor_pin: GPIO_NUM_34 - enable_moisture_sensor: true - enable_buttons: true - stop_water_button: GPIO_NUM_23 light_pin: GPIO_NUM_32 topic_prefix: "garden" - default_water_time: 5s publish_health: true health_interval: 1m - moisture_interval: 5s ``` ## Advanced diff --git a/docs/hydroponics_example.md b/docs/hydroponics_example.md index c313cd49..162eefac 100644 --- a/docs/hydroponics_example.md +++ b/docs/hydroponics_example.md @@ -25,42 +25,7 @@ This has a very basic setup since it just consists of the ESP32 and a single rel #### **`garden-controller/config.h`** ```c -#ifndef config_h -#define config_h - -#define TOPIC_PREFIX "aerogarden" - -#define QUEUE_SIZE 10 - -#define ENABLE_WIFI -#ifdef ENABLE_WIFI -#define MQTT_ADDRESS "192.168.0.107" -#define MQTT_PORT 30002 -#define MQTT_CLIENT_NAME TOPIC_PREFIX -#define MQTT_WATER_TOPIC TOPIC_PREFIX"/command/water" -#define MQTT_STOP_TOPIC TOPIC_PREFIX"/command/stop" -#define MQTT_STOP_ALL_TOPIC TOPIC_PREFIX"/command/stop_all" -#define MQTT_LIGHT_TOPIC TOPIC_PREFIX"/command/light" -#define MQTT_LIGHT_DATA_TOPIC TOPIC_PREFIX"/data/light" -#define MQTT_WATER_DATA_TOPIC TOPIC_PREFIX"/data/water" - -#define ENABLE_MQTT_HEALTH -#ifdef ENABLE_MQTT_HEALTH -#define MQTT_HEALTH_DATA_TOPIC TOPIC_PREFIX"/data/health" -#define HEALTH_PUBLISH_INTERVAL 60000 -#endif - -#define ENABLE_MQTT_LOGGING -#ifdef ENABLE_MQTT_LOGGING -#define MQTT_LOGGING_TOPIC TOPIC_PREFIX"/data/logs" -#endif -#endif - -#define NUM_ZONES 1 -#define ZONES { { GPIO_NUM_18, GPIO_NUM_16, GPIO_NUM_19, GPIO_NUM_36 } } -#define DEFAULT_WATER_TIME 5000 - -#endif +WIP ``` #### **`garden-app/config.yaml`** diff --git a/docs/indoor_example.md b/docs/indoor_example.md index b3ab4974..52b4e28d 100644 --- a/docs/indoor_example.md +++ b/docs/indoor_example.md @@ -20,7 +20,6 @@ This small garden has worked really well for growing herbs or even cherry tomato ### 3D Printed - Electronics case -- Buttons case - Hose splitter - Hose routing clips - Grow light mounts @@ -34,53 +33,7 @@ Then there are two 6-pin JST connectors that provide power (ground/5V) and 4 dat #### **`garden-controller/config.h`** ```c -#ifndef config_h -#define config_h - -#define TOPIC_PREFIX "garden" - -#define QUEUE_SIZE 10 - -#define ENABLE_WIFI -#ifdef ENABLE_WIFI -#define MQTT_ADDRESS "192.168.0.107" -#define MQTT_PORT 30002 -#define MQTT_CLIENT_NAME TOPIC_PREFIX -#define MQTT_WATER_TOPIC TOPIC_PREFIX"/command/water" -#define MQTT_STOP_TOPIC TOPIC_PREFIX"/command/stop" -#define MQTT_STOP_ALL_TOPIC TOPIC_PREFIX"/command/stop_all" -#define MQTT_LIGHT_TOPIC TOPIC_PREFIX"/command/light" -#define MQTT_LIGHT_DATA_TOPIC TOPIC_PREFIX"/data/light" -#define MQTT_WATER_DATA_TOPIC TOPIC_PREFIX"/data/water" - -#define ENABLE_MQTT_HEALTH -#ifdef ENABLE_MQTT_HEALTH -#define MQTT_HEALTH_DATA_TOPIC TOPIC_PREFIX"/data/health" -#define HEALTH_PUBLISH_INTERVAL 60000 -#endif - -#define ENABLE_MQTT_LOGGING -#ifdef ENABLE_MQTT_LOGGING -#define MQTT_LOGGING_TOPIC TOPIC_PREFIX"/data/logs" -#endif -#endif - -#define NUM_ZONES 3 -#define PUMP_PIN GPIO_NUM_18 -#define ZONE_1 { PUMP_PIN, GPIO_NUM_16, GPIO_NUM_19, GPIO_NUM_36 } -#define ZONE_2 { PUMP_PIN, GPIO_NUM_17, GPIO_NUM_21, GPIO_NUM_39 } -#define ZONE_3 { PUMP_PIN, GPIO_NUM_5, GPIO_NUM_22, GPIO_NUM_34 } -#define ZONES { ZONE_1, ZONE_2, ZONE_3 } -#define DEFAULT_WATER_TIME 5000 - -#define LIGHT_PIN GPIO_NUM_32 - -#define ENABLE_BUTTONS -#ifdef ENABLE_BUTTONS -#define STOP_BUTTON_PIN GPIO_NUM_23 -#endif - -#endif +WIP ``` #### **`garden-app/config.yaml`** diff --git a/docs/rest_api.md b/docs/rest_api.md index 51d20d92..481949bf 100644 --- a/docs/rest_api.md +++ b/docs/rest_api.md @@ -90,7 +90,6 @@ A `Zone` represents a resource that can be watered. It may contain zero or more "start_time": "2021-07-24T19:00:00-07:00" } ``` - - Control of watering based on moisture using `minimum_moisture` in the `water_schedule`. This sets the moisture percentage the zone's soil must drop below to enable watering - On-demand control of watering using a `WaterAction` to the `/action` endpoint - Access to a Zone's watering history from InfluxDB using `/history` endpoint diff --git a/docs/sensors_only_example.md b/docs/sensors_only_example.md deleted file mode 100644 index c1407cfd..00000000 --- a/docs/sensors_only_example.md +++ /dev/null @@ -1,57 +0,0 @@ -# Sensors Only Example - -## Details -This setup doesn't handle any watering and instead just attaches sensors. Although this can be useful on its own, it is really intended to be a second controller that is part of the same real-world Garden. For example, if you have a large outdoor Garden, you cannot realistically wire a moisture sensor to each Zone since the main controller will be located potentially far away. This allows you to connect separate controllers to collect data. - -## Components - -### Purchased -- Power adapter -- Circuit board -- ESP32 dev board -- Capactivie moisture sensors - -### 3D Printed -- Electronics cases - -### Circuit -This has a very basic setup since it just consists of the ESP32 and moisture sensors. - -## Configurations -Only the `garden-controller` config is shown here because this is intended to be in addition to an existing Garden setup and won't require any additional changes to the `garden-app` setup. - -Notice that in this example, `GPIO_NUM_MAX` is used as the moisture sensor pin on a few of the Zones. This instructs the controller to skip moisture measuring for these Zones so you can have one controller per sensor without losing the correct `position` in the data. - -#### **`garden-controller/config.h`** -```c -#ifndef config_h -#define config_h - -#define TOPIC_PREFIX "garden" - -#define QUEUE_SIZE 10 - -#define MQTT_ADDRESS "192.168.0.107" -#define MQTT_PORT 30002 -#define MQTT_CLIENT_NAME TOPIC_PREFIX"-sensors" - -#define DISABLE_WATERING -#define NUM_ZONES 3 -#define PUMP_PIN GPIO_NUM_18 -#define ZONE_1 { PUMP_PIN, GPIO_NUM_16, GPIO_NUM_19, GPIO_NUM_MAX } -#define ZONE_2 { PUMP_PIN, GPIO_NUM_17, GPIO_NUM_21, GPIO_NUM_MAX } -#define ZONE_3 { PUMP_PIN, GPIO_NUM_5, GPIO_NUM_22, GPIO_NUM_34 } -#define ZONES { ZONE_1, ZONE_2, ZONE_3 } -#define DEFAULT_WATER_TIME 5000 - -#define ENABLE_MOISTURE_SENSORS -#ifdef ENABLE_MOISTURE_SENSORS -#define MQTT_MOISTURE_DATA_TOPIC TOPIC_PREFIX"/data/moisture" -#define MOISTURE_SENSOR_AIR_VALUE 3415 -#define MOISTURE_SENSOR_WATER_VALUE 1362 -#define MOISTURE_SENSOR_INTERVAL 5000 -#endif - -#endif -``` - diff --git a/docs/weather_control.md b/docs/weather_control.md index 36fb5184..8a8c6c92 100644 --- a/docs/weather_control.md +++ b/docs/weather_control.md @@ -37,20 +37,6 @@ Temperature control usese the average daily high temperatures for scaling contro In the above example, there is a baseline value of 30C (86F) and range of 10 degrees. If the average daily high temperatures in the last 3 days (72h) are >= 40C (104F), watering will be scaled to 1.5 (1h30m). If the average daily high is <= 20C (68F), watering is scaled to 0.5 (30m). The scaling is proportional between these values. -## Moisture Control - -If a Zone is configured with a moisture sensor, it can be configured to use moisture-based watering. Unlike the other controls, this will skip watering completely rather than proportionally scaling it. The following example shows that watering should be skipped when soil moisture is > 50%. - -```json -{ - "weather_control": { - "moisture_control": { - "minimum_moisture": 50 - } - } -} -``` - ## Viewing Weather and Scaling Data Sometimes it might be hard to know what the total rainfall was or the recent average highs and it would also be useful to see how exactly that data is going to impact the next watering. Luckily, this information is included in the Zone API. The following example shows these relevant parts of a Zone response: diff --git a/garden-app/api/openapi.yaml b/garden-app/api/openapi.yaml index aeb4cd8a..fea61f16 100644 --- a/garden-app/api/openapi.yaml +++ b/garden-app/api/openapi.yaml @@ -630,10 +630,6 @@ components: type: string description: time of the next scheduled watering format: date-time - moisture: - type: number - format: float - description: moisture percentage of a Plant with a soil moisture sensor links: type: array items: @@ -900,8 +896,6 @@ components: type: object description: | Allows control over how/when the Zone is watered. The `interval` is used to control the amount of time the pump runs to water the Garden. - Specifying a `minimum_moisture` will instruct the server to consider moisture sensor data when executing the WaterAction. - To avoid falsely triggering watering and avoid the complexity if constantly checking moisture data, the moisture is only checked when executing a WaterAction. properties: duration: type: string @@ -1006,18 +1000,6 @@ components: baseline_value: 27 factor: 0.5 range: 10 - moisture_control: - type: object - description: skip watering based on temperature measurements - properties: - minimum_moisture: - type: integer - minimum: 0 - maximum: 100 - description: | - this is a percentage representing the threshold that the Plant's moisture must be - below to enable a WaterAction - example: 50 ScaleControl: type: object @@ -1041,7 +1023,7 @@ components: type: number format: float description: | - the most extreme value (when added to baseline_value) that scaling will be + the most extreme value (when added to baseline_value) that scaling will be affected by (used as max/min) Zone: @@ -1209,10 +1191,6 @@ components: type: number format: float description: scale factor calculated by WeatherControl setup and recent temperature data - soil_moisture_percent: - type: number - format: float - description: moisture percentage of a Zone with a soil moisture sensor WaterHistoryResponse: type: object @@ -1265,8 +1243,5 @@ components: type: string description: amount of time, as duration string, that Zone should be watered example: 15m - ignore_moisture: - type: boolean - description: if Zone is configured with a `minimum_moisture` for watering, ignore it and force watering required: - duration diff --git a/garden-app/cmd/controller.go b/garden-app/cmd/controller.go index b819a4e9..b21e8676 100644 --- a/garden-app/cmd/controller.go +++ b/garden-app/cmd/controller.go @@ -11,9 +11,6 @@ import ( var ( topicPrefix string numZones int - moistureStrategy string - moistureValue int - moistureInterval time.Duration publishWaterEvent bool publishHealth bool healthInterval time.Duration @@ -36,25 +33,9 @@ func init() { controllerCommand.PersistentFlags().StringVarP(&topicPrefix, "topic", "t", "test-garden", "MQTT topic prefix of the garden-controller") viper.BindPFlag("controller.topic_prefix", controllerCommand.PersistentFlags().Lookup("topic")) - controllerCommand.PersistentFlags().IntVarP(&numZones, "zones", "z", 0, "Number of Zones for which moisture data should be emulated") + controllerCommand.PersistentFlags().IntVarP(&numZones, "zones", "z", 0, "Number of Zones") viper.BindPFlag("controller.num_zones", controllerCommand.PersistentFlags().Lookup("zones")) - controllerCommand.PersistentFlags().StringVar(&moistureStrategy, "moisture-strategy", "random", "Strategy for creating moisture data") - err := controllerCommand.RegisterFlagCompletionFunc("moisture-strategy", func(_ *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) { - return []string{"random", "constant", "increasing", "decreasing"}, cobra.ShellCompDirectiveDefault - }) - if err != nil { - panic(err) - } - - viper.BindPFlag("controller.moisture_strategy", controllerCommand.PersistentFlags().Lookup("moisture-strategy")) - - controllerCommand.PersistentFlags().IntVar(&moistureValue, "moisture-value", 100, "The value, or starting value, to use for moisture data publishing") - viper.BindPFlag("controller.moisture_value", controllerCommand.PersistentFlags().Lookup("moisture-value")) - - controllerCommand.PersistentFlags().DurationVar(&moistureInterval, "moisture-interval", 10*time.Second, "Interval between moisture data publishing") - viper.BindPFlag("controller.moisture_interval", controllerCommand.PersistentFlags().Lookup("moisture-interval")) - controllerCommand.PersistentFlags().BoolVar(&publishWaterEvent, "publish-water-event", true, "Whether or not watering events should be published for logging") viper.BindPFlag("controller.publish_water_event", controllerCommand.PersistentFlags().Lookup("publish-water-event")) diff --git a/garden-app/controller/controller.go b/garden-app/controller/controller.go index 03a25558..2305fd08 100644 --- a/garden-app/controller/controller.go +++ b/garden-app/controller/controller.go @@ -32,8 +32,6 @@ type Config struct { type NestedConfig struct { // Configs used only for running mock controller EnableUI bool `mapstructure:"enable_ui" survey:"enable_ui"` - MoistureStrategy string `mapstructure:"moisture_strategy" survey:"moisture_strategy"` - MoistureValue int `mapstructure:"moisture_value" survey:"moisture_value"` PublishWaterEvent bool `mapstructure:"publish_water_event" survey:"publish_water_event"` TemperatureValue float64 `mapstructure:"temperature_value"` HumidityValue float64 `mapstructure:"humidity_value"` @@ -42,7 +40,6 @@ type NestedConfig struct { // Configs used for both TopicPrefix string `mapstructure:"topic_prefix" survey:"topic_prefix"` NumZones int `mapstructure:"num_zones" survey:"num_zones"` - MoistureInterval time.Duration `mapstructure:"moisture_interval" survey:"moisture_interval"` PublishHealth bool `mapstructure:"publish_health" survey:"publish_health"` HealthInterval time.Duration `mapstructure:"health_interval" survey:"health_interval"` PublishTemperatureHumidity bool `mapstructure:"publish_temperature_humidity" survey:"publish_temperature_humidity"` @@ -50,14 +47,9 @@ type NestedConfig struct { // Configs only used for generate-config WifiConfig `mapstructure:"wifi" survey:"wifi"` - Zones []ZoneConfig `mapstructure:"zones" survey:"zones"` - DefaultWaterTime time.Duration `mapstructure:"default_water_time" survey:"default_water_time"` - EnableButtons bool `mapstructure:"enable_buttons" survey:"enable_buttons"` - EnableMoistureSensor bool `mapstructure:"enable_moisture_sensor" survey:"enable_moisture_sensor"` - LightPin string `mapstructure:"light_pin" survey:"light_pin"` - StopButtonPin string `mapstructure:"stop_water_button" survey:"stop_water_button"` - DisableWatering bool `mapstructure:"disable_watering" survey:"disable_watering"` - TemperatureHumidityPin string `mapstructure:"temperature_humidity_pin" survey:"temperature_humidity_pin"` + Zones []ZoneConfig `mapstructure:"zones" survey:"zones"` + LightPin string `mapstructure:"light_pin" survey:"light_pin"` + TemperatureHumidityPin string `mapstructure:"temperature_humidity_pin" survey:"temperature_humidity_pin"` MQTTAddress string `survey:"mqtt_address"` MQTTPort int `survey:"mqtt_port"` @@ -95,10 +87,6 @@ func NewController(cfg Config) (*Controller, error) { controller.logger.Info("starting controller", "topic_prefix", controller.TopicPrefix) - if cfg.NumZones > 0 { - controller.pubLogger.Info("publishing moisture data for Zones", "num_zones", cfg.NumZones) - } - topics, err := controller.topics() if err != nil { return nil, fmt.Errorf("unable to determine topics: %w", err) @@ -134,19 +122,6 @@ func (c *Controller) Start() { c.logger.Debug("initializing scheduler") scheduler := gocron.NewScheduler(time.Local) scheduler.CustomTime(clock.DefaultClock) - if c.MoistureInterval != 0 { - for p := 0; p < c.NumZones; p++ { - c.logger.With( - "interval", c.MoistureInterval.String(), - "strategy", c.MoistureStrategy, - ).Debug("create scheduled job to publish moisture data") - _, err := scheduler.Every(c.MoistureInterval).Do(c.publishMoistureData, p) - if err != nil { - c.logger.Error("error scheduling moisture publishing", "error", err) - return - } - } - } if c.PublishHealth { c.logger.Debug("create scheduled job to publish health data", "interval", c.HealthInterval.String()) _, err := scheduler.Every(c.HealthInterval).Do(c.publishHealthData) @@ -219,10 +194,6 @@ func (c *Controller) setupUI() *tview.Application { header := tview.NewTextView(). SetTextAlign(tview.AlignCenter). SetText(c.TopicPrefix) - tview.ANSIWriter(header).Write([]byte(fmt.Sprintf( - "\n%d Zones\nPublishWaterEvent: %t, PublishHealth: %t, MoistureStrategy: %s", - c.NumZones, c.PublishWaterEvent, c.PublishHealth, c.MoistureStrategy), - )) grid := tview.NewGrid(). SetRows(3, 0). @@ -239,25 +210,6 @@ func (c *Controller) setupUI() *tview.Application { return app.SetRoot(grid, true) } -// publishMoistureData publishes an InfluxDB line containing moisture data for a Zone -func (c *Controller) publishMoistureData(zone int) { - moisture := c.createMoistureData() - topic := fmt.Sprintf("%s/data/moisture", c.TopicPrefix) - moistureLogger := c.pubLogger.With( - "topic", topic, - "moisture", moisture, - "zone", zone, - ) - moistureLogger.Info("publishing moisture data for Zone") - err := c.mqttClient.Publish( - topic, - []byte(fmt.Sprintf("moisture,zone=%d value=%d", zone, moisture)), - ) - if err != nil { - moistureLogger.Error("unable to publish moisture data", "error", err) - } -} - // publishHealthData publishes an InfluxDB line to record that the controller is alive and active func (c *Controller) publishHealthData() { topic := fmt.Sprintf("%s/data/health", c.TopicPrefix) @@ -318,32 +270,7 @@ func addNoise(baseValue float64, percentRange float64) float64 { return baseValue + diff } -// createMoistureData uses the MoistureStrategy config to create a moisture data point -func (c *Controller) createMoistureData() int { - switch c.MoistureStrategy { - case "random": - // nolint:gosec - return rand.Intn(c.MoistureValue) - case "constant": - return c.MoistureValue - case "increasing": - c.MoistureValue++ - if c.MoistureValue > 100 { - c.MoistureValue = 0 - } - return c.MoistureValue - case "decreasing": - c.MoistureValue-- - if c.MoistureValue < 0 { - c.MoistureValue = 100 - } - return c.MoistureValue - default: - return 0 - } -} - -// publishWaterEvent logs moisture data to InfluxDB via Telegraf and MQTT +// publishWaterEvent publishes completed water events func (c *Controller) publishWaterEvent(waterMsg action.WaterMessage, cmdTopic string) { if !c.PublishWaterEvent { c.pubLogger.Debug("publishing water events is disabled") diff --git a/garden-app/controller/generate_config.go b/garden-app/controller/generate_config.go index e4e478ea..785c17ae 100644 --- a/garden-app/controller/generate_config.go +++ b/garden-app/controller/generate_config.go @@ -25,46 +25,28 @@ const ( #define MQTT_ADDRESS "{{ .MQTTConfig.Broker }}" #define MQTT_PORT {{ .MQTTConfig.Port }} -{{ if .PublishHealth }} -#define ENABLE_MQTT_HEALTH -{{ end }} - -#define ENABLE_MQTT_LOGGING - #endif -{{ if .DisableWatering }} -#define DISABLE_WATERING -{{ end -}} #define NUM_ZONES {{ len .Zones }} -#define ZONES { {{ range $index, $z := .Zones }}{{if $index}}, {{end}}{ {{ $z.PumpPin }}, {{ $z.ValvePin }}, {{ or $z.ButtonPin "GPIO_NUM_MAX" }}, {{ or $z.MoistureSensorPin "GPIO_NUM_MAX" }} }{{ end }} } -#define DEFAULT_WATER_TIME {{ milliseconds .DefaultWaterTime }} +#define VALVES { {{ range $index, $z := .Zones }}{{if $index}}, {{end}}{{ $z.ValvePin }}{{ end }} } +#define PUMPS { {{ range $index, $z := .Zones }}{{if $index}}, {{end}}{{ $z.PumpPin }}{{ end }} } {{ if .LightPin }} +#define LIGHT_ENABLED true #define LIGHT_PIN {{ .LightPin }} +{{ else }} +#define LIGHT_ENABLED false +#define LIGHT_PIN GPIO_NUM_MAX {{ end }} -{{ if .EnableButtons }} -#define ENABLE_BUTTONS -#ifdef ENABLE_BUTTONS -#define STOP_BUTTON_PIN {{ .StopButtonPin }} -#endif -{{ end }} - -{{ if .EnableMoistureSensor }} -#ifdef ENABLE_MOISTURE_SENSORS AND ENABLE_WIFI -#define MOISTURE_SENSOR_AIR_VALUE 3415 -#define MOISTURE_SENSOR_WATER_VALUE 1362 -#define MOISTURE_SENSOR_INTERVAL {{ milliseconds .MoistureInterval }} -#endif -{{ end -}} - {{ if .PublishTemperatureHumidity }} -#define ENABLE_DHT22 -#ifdef ENABLE_DHT22 +#define ENABLE_DHT22 true #define DHT22_PIN {{ .TemperatureHumidityPin }} #define DHT22_INTERVAL {{ milliseconds .TemperatureHumidityInterval }} -#endif +{{ else }} +#define ENABLE_DHT22 false +#define DHT22_PIN GPIO_NUM_MAX +#define DHT22_INTERVAL 0 {{ end -}} #endif ` @@ -86,10 +68,8 @@ type WifiConfig struct { // ZoneConfig has the configuration details for controlling hardware pins type ZoneConfig struct { - PumpPin string `mapstructure:"pump_pin" survey:"pump_pin"` - ValvePin string `mapstructure:"valve_pin" survey:"valve_pin"` - ButtonPin string `mapstructure:"button_pin" survey:"button_pin"` - MoistureSensorPin string `mapstructure:"moisture_sensor_pin" survey:"moisture_sensor_pin"` + PumpPin string `mapstructure:"pump_pin" survey:"pump_pin"` + ValvePin string `mapstructure:"valve_pin" survey:"valve_pin"` } // GenerateConfig will create config.h and wifi_config.h based on the provided configurations. It can optionally write to files diff --git a/garden-app/controller/generate_config_test.go b/garden-app/controller/generate_config_test.go index e36cb077..958c02d3 100644 --- a/garden-app/controller/generate_config_test.go +++ b/garden-app/controller/generate_config_test.go @@ -31,8 +31,7 @@ func TestGenerateMainConfig(t *testing.T) { ValvePin: "GPIO_NUM_16", }, }, - TopicPrefix: "garden", - DefaultWaterTime: 5 * time.Second, + TopicPrefix: "garden", }, MQTTConfig: mqtt.Config{ Broker: "localhost", @@ -51,14 +50,18 @@ func TestGenerateMainConfig(t *testing.T) { #define MQTT_ADDRESS "localhost" #define MQTT_PORT 1883 -#define ENABLE_MQTT_LOGGING - #endif #define NUM_ZONES 1 -#define ZONES { { GPIO_NUM_18, GPIO_NUM_16, GPIO_NUM_MAX, GPIO_NUM_MAX } } -#define DEFAULT_WATER_TIME 5000 +#define VALVES { GPIO_NUM_16 } +#define PUMPS { GPIO_NUM_18 } + +#define LIGHT_ENABLED false +#define LIGHT_PIN GPIO_NUM_MAX +#define ENABLE_DHT22 false +#define DHT22_PIN GPIO_NUM_MAX +#define DHT22_INTERVAL 0 #endif `, }, @@ -68,19 +71,12 @@ func TestGenerateMainConfig(t *testing.T) { NestedConfig: NestedConfig{ Zones: []ZoneConfig{ { - PumpPin: "GPIO_NUM_18", - ValvePin: "GPIO_NUM_16", - ButtonPin: "GPIO_NUM_19", - MoistureSensorPin: "GPIO_NUM_36", + PumpPin: "GPIO_NUM_18", + ValvePin: "GPIO_NUM_16", }, }, TopicPrefix: "garden", - DefaultWaterTime: 5 * time.Second, LightPin: "GPIO_NUM_32", - EnableButtons: true, - StopButtonPin: "GPIO_NUM_23", - EnableMoistureSensor: true, - MoistureInterval: 5 * time.Second, PublishHealth: true, HealthInterval: 1 * time.Minute, PublishTemperatureHumidity: true, @@ -104,35 +100,19 @@ func TestGenerateMainConfig(t *testing.T) { #define MQTT_ADDRESS "localhost" #define MQTT_PORT 1883 -#define ENABLE_MQTT_HEALTH - -#define ENABLE_MQTT_LOGGING - #endif #define NUM_ZONES 1 -#define ZONES { { GPIO_NUM_18, GPIO_NUM_16, GPIO_NUM_19, GPIO_NUM_36 } } -#define DEFAULT_WATER_TIME 5000 +#define VALVES { GPIO_NUM_16 } +#define PUMPS { GPIO_NUM_18 } +#define LIGHT_ENABLED true #define LIGHT_PIN GPIO_NUM_32 -#define ENABLE_BUTTONS -#ifdef ENABLE_BUTTONS -#define STOP_BUTTON_PIN GPIO_NUM_23 -#endif - -#ifdef ENABLE_MOISTURE_SENSORS AND ENABLE_WIFI -#define MOISTURE_SENSOR_AIR_VALUE 3415 -#define MOISTURE_SENSOR_WATER_VALUE 1362 -#define MOISTURE_SENSOR_INTERVAL 5000 -#endif - -#define ENABLE_DHT22 -#ifdef ENABLE_DHT22 +#define ENABLE_DHT22 true #define DHT22_PIN GPIO_NUM_27 #define DHT22_INTERVAL 300000 #endif -#endif `, }, { @@ -145,9 +125,7 @@ func TestGenerateMainConfig(t *testing.T) { ValvePin: "GPIO_NUM_16", }, }, - TopicPrefix: "garden", - DefaultWaterTime: 5 * time.Second, - DisableWatering: true, + TopicPrefix: "garden", }, MQTTConfig: mqtt.Config{ Broker: "localhost", @@ -166,15 +144,18 @@ func TestGenerateMainConfig(t *testing.T) { #define MQTT_ADDRESS "localhost" #define MQTT_PORT 1883 -#define ENABLE_MQTT_LOGGING - #endif -#define DISABLE_WATERING #define NUM_ZONES 1 -#define ZONES { { GPIO_NUM_18, GPIO_NUM_16, GPIO_NUM_MAX, GPIO_NUM_MAX } } -#define DEFAULT_WATER_TIME 5000 +#define VALVES { GPIO_NUM_16 } +#define PUMPS { GPIO_NUM_18 } +#define LIGHT_ENABLED false +#define LIGHT_PIN GPIO_NUM_MAX + +#define ENABLE_DHT22 false +#define DHT22_PIN GPIO_NUM_MAX +#define DHT22_INTERVAL 0 #endif `, }, @@ -200,8 +181,7 @@ func TestGenerateMainConfig(t *testing.T) { ValvePin: "GPIO_NUM_16", }, }, - TopicPrefix: "garden", - DefaultWaterTime: 5 * time.Second, + TopicPrefix: "garden", }, MQTTConfig: mqtt.Config{ Broker: "localhost", @@ -220,14 +200,18 @@ func TestGenerateMainConfig(t *testing.T) { #define MQTT_ADDRESS "localhost" #define MQTT_PORT 1883 -#define ENABLE_MQTT_LOGGING - #endif #define NUM_ZONES 4 -#define ZONES { { GPIO_NUM_18, GPIO_NUM_16, GPIO_NUM_MAX, GPIO_NUM_MAX }, { GPIO_NUM_18, GPIO_NUM_16, GPIO_NUM_MAX, GPIO_NUM_MAX }, { GPIO_NUM_18, GPIO_NUM_16, GPIO_NUM_MAX, GPIO_NUM_MAX }, { GPIO_NUM_18, GPIO_NUM_16, GPIO_NUM_MAX, GPIO_NUM_MAX } } -#define DEFAULT_WATER_TIME 5000 +#define VALVES { GPIO_NUM_16, GPIO_NUM_16, GPIO_NUM_16, GPIO_NUM_16 } +#define PUMPS { GPIO_NUM_18, GPIO_NUM_18, GPIO_NUM_18, GPIO_NUM_18 } + +#define LIGHT_ENABLED false +#define LIGHT_PIN GPIO_NUM_MAX +#define ENABLE_DHT22 false +#define DHT22_PIN GPIO_NUM_MAX +#define DHT22_INTERVAL 0 #endif `, }, diff --git a/garden-app/controller/interactive.go b/garden-app/controller/interactive.go index c18feb67..1dd0a8fc 100644 --- a/garden-app/controller/interactive.go +++ b/garden-app/controller/interactive.go @@ -12,11 +12,6 @@ func configPrompts(config *Config) error { return fmt.Errorf("error completing MQTT prompts: %w", err) } - err = wateringPrompts(config) - if err != nil { - return fmt.Errorf("error completing watering prompts: %w", err) - } - err = zonePrompts(config) if err != nil { return fmt.Errorf("error completing zone prompts: %w", err) @@ -31,16 +26,6 @@ func configPrompts(config *Config) error { return fmt.Errorf("error completing light pin prompt: %w", err) } - err = buttonPrompts(config) - if err != nil { - return fmt.Errorf("error completing button prompts: %w", err) - } - - err = moisturePrompts(config) - if err != nil { - return fmt.Errorf("error completing moisture prompts: %w", err) - } - err = temperatureHumidityPrompts(config) if err != nil { return fmt.Errorf("error completing temperature and humidity prompts: %w", err) @@ -111,54 +96,6 @@ func mqttPrompts(config *Config) error { return nil } -func buttonPrompts(config *Config) error { - err := survey.AskOne(&survey.Input{ - Message: "Enable buttons", - Default: fmt.Sprintf("%t", config.EnableButtons), - Help: "allow the use of buttons for controlling watering using the default water time", - }, &config.EnableButtons) - if err != nil { - return err - } - - if !config.EnableButtons { - return nil - } - - return survey.AskOne(&survey.Input{ - Message: "Stop watering button pin", - Default: config.StopButtonPin, - Help: "pin identifier of the button to use for stopping current watering", - }, &config.StopButtonPin) -} - -func moisturePrompts(config *Config) error { - err := survey.AskOne(&survey.Input{ - Message: "Enable moisture sensor", - Default: fmt.Sprintf("%t", config.EnableMoistureSensor), - Help: "enable moisture data publishing", - }, &config.EnableMoistureSensor) - if err != nil { - return err - } - - if !config.EnableMoistureSensor { - return nil - } - - qs := []*survey.Question{ - { - Name: "moisture_interval", - Prompt: &survey.Input{ - Message: "Moisture reading interval", - Default: config.MoistureInterval.String(), - Help: "how often to read and publish moisture data for each configured sensor", - }, - }, - } - return survey.Ask(qs, config) -} - func temperatureHumidityPrompts(config *Config) error { err := survey.AskOne(&survey.Input{ Message: "Enable temperature and humidity (DHT22) sensor", @@ -194,30 +131,6 @@ func temperatureHumidityPrompts(config *Config) error { return survey.Ask(qs, config) } -func wateringPrompts(config *Config) error { - qs := []*survey.Question{ - { - Name: "disable_watering", - Prompt: &survey.Input{ - Message: "Disable watering", - Default: fmt.Sprintf("%t", config.DisableWatering), - Help: "do not allow watering. Only used by sensor-only gardens", - }, - Validate: survey.Required, - }, - { - Name: "default_water_time", - Prompt: &survey.Input{ - Message: "Default water time", - Default: config.DefaultWaterTime.String(), - Help: "default time (in milliseconds) to use for watering if button is used or command is missing value", - }, - Validate: survey.Required, - }, - } - return survey.Ask(qs, config) -} - func zonePrompts(config *Config) error { addAnotherZone := true for addAnotherZone { @@ -237,7 +150,7 @@ func zonePrompts(config *Config) error { Name: "pump_pin", Prompt: &survey.Input{ Message: "\tPump pin", - Help: "pin identifier for the relay controlling a pump or main valve", + Help: "pin identifier for the relay controlling a pump or main valve. Use the same value as the valve if no pump is used", }, Validate: survey.Required, }, @@ -249,22 +162,6 @@ func zonePrompts(config *Config) error { }, Validate: survey.Required, }, - { - Name: "button_pin", - Prompt: &survey.Input{ - Message: "\tButton pin", - Default: "GPIO_NUM_MAX", - Help: "pin identifier for a button that controls this zone (GPIO_NUM_MAX to disable)", - }, - }, - { - Name: "moisture_sensor_pin", - Prompt: &survey.Input{ - Message: "\tMoisture sensor pin", - Default: "GPIO_NUM_MAX", - Help: "pin identifier for a moisture sensor that corresponds to this zone (GPIO_NUM_MAX to disable)", - }, - }, } var zc ZoneConfig diff --git a/garden-app/integration_tests/testdata/config.yml b/garden-app/integration_tests/testdata/config.yml index 34fdc899..096e57ea 100644 --- a/garden-app/integration_tests/testdata/config.yml +++ b/garden-app/integration_tests/testdata/config.yml @@ -17,9 +17,6 @@ storage: controller: topic_prefix: "test" num_zones: 3 - moisture_strategy: random - moisture_value: 100 - moisture_interval: 10s publish_water_event: true publish_health: true health_interval: 500ms diff --git a/garden-app/pkg/action/zone_action.go b/garden-app/pkg/action/zone_action.go index 0ddc3f22..170f2b3b 100644 --- a/garden-app/pkg/action/zone_action.go +++ b/garden-app/pkg/action/zone_action.go @@ -31,9 +31,8 @@ func (action *ZoneAction) Bind(*http.Request) error { // WaterAction is an action for watering a Zone for the specified amount of time type WaterAction struct { - Duration *pkg.Duration `json:"duration" form:"duration"` - IgnoreMoisture bool `json:"ignore_moisture"` - IgnoreWeather bool `json:"ignore_weather"` + Duration *pkg.Duration `json:"duration" form:"duration"` + IgnoreWeather bool `json:"ignore_weather"` } // WaterMessage is the message being sent over MQTT to the embedded garden controller diff --git a/garden-app/pkg/influxdb/client.go b/garden-app/pkg/influxdb/client.go index 525a0e52..83a32951 100644 --- a/garden-app/pkg/influxdb/client.go +++ b/garden-app/pkg/influxdb/client.go @@ -13,15 +13,7 @@ import ( const ( // QueryTimeout is the default time to use for a query's context timeout - QueryTimeout = time.Millisecond * 1000 - moistureQueryTemplate = `from(bucket: "{{.Bucket}}") -|> range(start: -{{.Start}}) -|> filter(fn: (r) => r["_measurement"] == "moisture") -|> filter(fn: (r) => r["_field"] == "value") -|> filter(fn: (r) => r["zone"] == "{{.ZonePosition}}") -|> filter(fn: (r) => r["topic"] == "{{.TopicPrefix}}/data/moisture") -|> drop(columns: ["host"]) -|> mean()` + QueryTimeout = time.Millisecond * 1000 healthQueryTemplate = `from(bucket: "{{.Bucket}}") |> range(start: -{{.Start}}) |> filter(fn: (r) => r["_measurement"] == "health") @@ -63,7 +55,6 @@ var influxDBClientSummary = prometheus.NewSummaryVec(prometheus.SummaryOpts{ // Client is an interface that allows querying InfluxDB for data type Client interface { - GetMoisture(context.Context, uint, string) (float64, error) GetLastContact(context.Context, string) (time.Time, error) GetWaterHistory(context.Context, uint, string, time.Duration, uint64) ([]map[string]interface{}, error) GetTemperatureAndHumidity(context.Context, string) (float64, float64, error) @@ -112,38 +103,6 @@ func NewClient(config Config) Client { } } -// GetMoisture returns the Zone's average soil moisture in the last 15 minutes -func (client *client) GetMoisture(ctx context.Context, zonePosition uint, topicPrefix string) (float64, error) { - timer := prometheus.NewTimer(influxDBClientSummary.WithLabelValues("GetMoisture")) - defer timer.ObserveDuration() - - // Prepare query - queryString, err := queryData{ - Bucket: client.config.Bucket, - Start: time.Minute * 15, - ZonePosition: zonePosition, - TopicPrefix: topicPrefix, - }.Render(moistureQueryTemplate) - if err != nil { - return 0, err - } - - // Query InfluxDB - queryAPI := client.QueryAPI(client.config.Org) - queryResult, err := queryAPI.Query(ctx, queryString) - if err != nil { - return 0, err - } - - // Read and return the result - var result float64 - if queryResult.Next() { - result = queryResult.Record().Value().(float64) - } - - return result, queryResult.Err() -} - func (client *client) GetLastContact(ctx context.Context, topicPrefix string) (time.Time, error) { timer := prometheus.NewTimer(influxDBClientSummary.WithLabelValues("GetLastContact")) defer timer.ObserveDuration() @@ -212,7 +171,7 @@ func (client *client) GetWaterHistory(ctx context.Context, zonePosition uint, to // GetTemperatureAndHumidity gets the recent temperature and humidity data for a Garden func (client *client) GetTemperatureAndHumidity(ctx context.Context, topicPrefix string) (float64, float64, error) { - timer := prometheus.NewTimer(influxDBClientSummary.WithLabelValues("GetMoisture")) + timer := prometheus.NewTimer(influxDBClientSummary.WithLabelValues("GetTemperatureAndHumidity")) defer timer.ObserveDuration() queryString, err := queryData{ diff --git a/garden-app/pkg/influxdb/mock_Client.go b/garden-app/pkg/influxdb/mock_Client.go index 7142a35a..aa77bbee 100644 --- a/garden-app/pkg/influxdb/mock_Client.go +++ b/garden-app/pkg/influxdb/mock_Client.go @@ -116,30 +116,6 @@ func (_m *MockClient) GetLastContact(_a0 context.Context, _a1 string) (time.Time return r0, r1 } -// GetMoisture provides a mock function with given fields: _a0, _a1, _a2 -func (_m *MockClient) GetMoisture(_a0 context.Context, _a1 uint, _a2 string) (float64, error) { - ret := _m.Called(_a0, _a1, _a2) - - var r0 float64 - var r1 error - if rf, ok := ret.Get(0).(func(context.Context, uint, string) (float64, error)); ok { - return rf(_a0, _a1, _a2) - } - if rf, ok := ret.Get(0).(func(context.Context, uint, string) float64); ok { - r0 = rf(_a0, _a1, _a2) - } else { - r0 = ret.Get(0).(float64) - } - - if rf, ok := ret.Get(1).(func(context.Context, uint, string) error); ok { - r1 = rf(_a0, _a1, _a2) - } else { - r1 = ret.Error(1) - } - - return r0, r1 -} - // GetTemperatureAndHumidity provides a mock function with given fields: _a0, _a1 func (_m *MockClient) GetTemperatureAndHumidity(_a0 context.Context, _a1 string) (float64, float64, error) { ret := _m.Called(_a0, _a1) diff --git a/garden-app/pkg/water_schedule.go b/garden-app/pkg/water_schedule.go index ba4ff918..c1d0165f 100644 --- a/garden-app/pkg/water_schedule.go +++ b/garden-app/pkg/water_schedule.go @@ -14,8 +14,7 @@ import ( const currentWaterScheduleVersion = uint(1) -// WaterSchedule allows the user to have more control over how the Zone is watered using an Interval -// and optional MinimumMoisture which acts as the threshold the Zone's soil should be above. +// WaterSchedule allows the user to have more control over how the Zone is watered using an Interval. // StartTime specifies when the watering interval should originate from. It can be used to increase/decrease delays in watering. type WaterSchedule struct { ID babyapi.ID `json:"id" yaml:"id"` @@ -70,7 +69,7 @@ func (ws *WaterSchedule) SetEndDate(now time.Time) { // This checks that WeatherControl is defined and has at least one type of control configured func (ws *WaterSchedule) HasWeatherControl() bool { return ws != nil && - (ws.HasRainControl() || ws.HasSoilMoistureControl() || ws.HasTemperatureControl()) + (ws.HasRainControl() || ws.HasTemperatureControl()) } // Patch allows modifying the struct in-place with values from a different instance @@ -121,13 +120,6 @@ func (ws *WaterSchedule) HasRainControl() bool { ws.WeatherControl.Rain != nil } -// HasSoilMoistureControl is used to determine if soil moisture conditions should be checked before watering the Zone -func (ws *WaterSchedule) HasSoilMoistureControl() bool { - return ws.WeatherControl != nil && - ws.WeatherControl.SoilMoisture != nil && - ws.WeatherControl.SoilMoisture.MinimumMoisture != nil -} - // HasTemperatureControl is used to determine if configuration is available for environmental scaling func (ws *WaterSchedule) HasTemperatureControl() bool { return ws.WeatherControl != nil && @@ -215,9 +207,8 @@ type NextWaterDetails struct { // WeatherData is used to represent the data used for WeatherControl to a user type WeatherData struct { - Rain *RainData `json:"rain,omitempty"` - Temperature *TemperatureData `json:"average_temperature,omitempty"` - SoilMoisturePercent *float64 `json:"soil_moisture_percent,omitempty"` + Rain *RainData `json:"rain,omitempty"` + Temperature *TemperatureData `json:"average_temperature,omitempty"` } // RainData shows the total rain in the last watering interval and the scaling factor it would result in @@ -315,11 +306,6 @@ func ValidateWeatherControl(wc *weather.Control) error { return fmt.Errorf("error validating rain_control: %w", err) } } - if wc.SoilMoisture != nil { - if wc.SoilMoisture.MinimumMoisture == nil { - return errors.New("error validating moisture_control: missing required field: minimum_moisture") - } - } return nil } diff --git a/garden-app/pkg/water_schedule_test.go b/garden-app/pkg/water_schedule_test.go index 41a72938..c2fa7c7c 100644 --- a/garden-app/pkg/water_schedule_test.go +++ b/garden-app/pkg/water_schedule_test.go @@ -35,7 +35,6 @@ func TestWaterScheduleEndDated(t *testing.T) { } func TestWaterSchedulePatch(t *testing.T) { - one := 1 float := float32(1) now := clock.Now() tests := []struct { @@ -66,16 +65,6 @@ func TestWaterSchedulePatch(t *testing.T) { Description: "description", }, }, - { - "PatchWeatherControl.SoilMoisture.MinimumMoisture", - &WaterSchedule{ - WeatherControl: &weather.Control{ - SoilMoisture: &weather.SoilMoistureControl{ - MinimumMoisture: &one, - }, - }, - }, - }, { "PatchStartTime", &WaterSchedule{ diff --git a/garden-app/pkg/weather/control.go b/garden-app/pkg/weather/control.go index a32e20db..9c8b6753 100644 --- a/garden-app/pkg/weather/control.go +++ b/garden-app/pkg/weather/control.go @@ -4,9 +4,8 @@ import "github.com/rs/xid" // Control defines certain parameters and behaviors to influence watering patterns based off weather data type Control struct { - Rain *ScaleControl `json:"rain_control,omitempty"` - SoilMoisture *SoilMoistureControl `json:"moisture_control,omitempty"` - Temperature *ScaleControl `json:"temperature_control,omitempty"` + Rain *ScaleControl `json:"rain_control,omitempty"` + Temperature *ScaleControl `json:"temperature_control,omitempty"` } // Patch allows modifying the struct in-place with values from a different instance @@ -17,14 +16,6 @@ func (wc *Control) Patch(new *Control) { } wc.Rain.Patch(new.Rain) } - if new.SoilMoisture != nil { - if wc.SoilMoisture == nil { - wc.SoilMoisture = &SoilMoistureControl{} - } - if new.SoilMoisture.MinimumMoisture != nil { - wc.SoilMoisture.MinimumMoisture = new.SoilMoisture.MinimumMoisture - } - } if new.Temperature != nil { if wc.Temperature == nil { wc.Temperature = &ScaleControl{} @@ -33,13 +24,6 @@ func (wc *Control) Patch(new *Control) { } } -// SoilMoistureControl defines parameters for delaying watering based on soil moisture data. This will skip watering if the -// soil moisture is below the minimum -// soil moisture value is currently hard-coded as the average value over the last 15 minutes -type SoilMoistureControl struct { - MinimumMoisture *int `json:"minimum_moisture,omitempty"` -} - // ScaleControl is a generic struct that enables scaling // BaselineValue is the value that scaling starts at // Range is the most extreme value that scaling will go to (used as max/min) diff --git a/garden-app/pkg/weather/control_test.go b/garden-app/pkg/weather/control_test.go index 5f1fbc8d..40909e18 100644 --- a/garden-app/pkg/weather/control_test.go +++ b/garden-app/pkg/weather/control_test.go @@ -9,7 +9,6 @@ import ( ) func TestPatch(t *testing.T) { - fifty := 50 tests := []struct { name string newControl *Control @@ -54,14 +53,6 @@ func TestPatch(t *testing.T) { }, }, }, - { - "PatchSoilMoisture.MinimumMoisture", - &Control{ - SoilMoisture: &SoilMoistureControl{ - MinimumMoisture: &fifty, - }, - }, - }, } for _, tt := range tests { @@ -77,13 +68,9 @@ func TestPatch(t *testing.T) { if tt.newControl.Temperature == nil { tt.newControl.Temperature = &ScaleControl{} } - if tt.newControl.SoilMoisture == nil { - tt.newControl.SoilMoisture = &SoilMoistureControl{} - } c := &Control{ - Rain: &ScaleControl{}, - Temperature: &ScaleControl{}, - SoilMoisture: &SoilMoistureControl{}, + Rain: &ScaleControl{}, + Temperature: &ScaleControl{}, } c.Patch(tt.newControl) assert.Equal(t, tt.newControl, c) diff --git a/garden-app/server/garden_responses.go b/garden-app/server/garden_responses.go index 27caf1b0..d18c0284 100644 --- a/garden-app/server/garden_responses.go +++ b/garden-app/server/garden_responses.go @@ -14,7 +14,7 @@ import ( "github.com/go-chi/render" ) -// GardenResponse is used to represent a Garden in the response body with the additional Moisture data +// GardenResponse is used to represent a Garden in the response body with additional data // and hypermedia Links fields type GardenResponse struct { *pkg.Garden diff --git a/garden-app/server/water_schedule_responses.go b/garden-app/server/water_schedule_responses.go index 151088ad..2b1c023c 100644 --- a/garden-app/server/water_schedule_responses.go +++ b/garden-app/server/water_schedule_responses.go @@ -57,7 +57,7 @@ func GetNextWaterDetails(r *http.Request, ws *pkg.WaterSchedule, worker *worker. return result } -// WaterScheduleResponse is used to represent a WaterSchedule in the response body with the additional Moisture data +// WaterScheduleResponse is used to represent a WaterSchedule in the response body with the additional data // and hypermedia Links fields type WaterScheduleResponse struct { *pkg.WaterSchedule diff --git a/garden-app/server/water_schedule_test.go b/garden-app/server/water_schedule_test.go index b28b3c3f..3e778cf2 100644 --- a/garden-app/server/water_schedule_test.go +++ b/garden-app/server/water_schedule_test.go @@ -723,18 +723,6 @@ func TestWaterScheduleRequest(t *testing.T) { }, "error validating weather_control: error validating rain_control: range must be a positive number", }, - { - "WeatherControlMissingMinimumMoisture", - &pkg.WaterSchedule{ - Interval: &pkg.Duration{Duration: time.Hour * 24}, - Duration: &pkg.Duration{Duration: time.Second}, - StartTime: pkg.NewStartTime(now), - WeatherControl: &weather.Control{ - SoilMoisture: &weather.SoilMoistureControl{}, - }, - }, - "error validating weather_control: error validating moisture_control: missing required field: minimum_moisture", - }, { "ActivePeriodInvalid", &pkg.WaterSchedule{ diff --git a/garden-app/server/weather_data_response.go b/garden-app/server/weather_data_response.go index 0854e6ab..1831ef34 100644 --- a/garden-app/server/weather_data_response.go +++ b/garden-app/server/weather_data_response.go @@ -11,9 +11,8 @@ import ( // WeatherData is used to represent the data used for WeatherControl to a user type WeatherData struct { - Rain *RainData `json:"rain,omitempty"` - Temperature *TemperatureData `json:"average_temperature,omitempty"` - SoilMoisturePercent *float64 `json:"soil_moisture_percent,omitempty"` + Rain *RainData `json:"rain,omitempty"` + Temperature *TemperatureData `json:"average_temperature,omitempty"` } // RainData shows the total rain in the last watering interval and the scaling factor it would result in diff --git a/garden-app/server/zone.go b/garden-app/server/zone.go index f9b35545..52c0005d 100644 --- a/garden-app/server/zone.go +++ b/garden-app/server/zone.go @@ -325,16 +325,6 @@ func (api *ZonesAPI) getWaterHistoryFromRequest(r *http.Request, zone *pkg.Zone, return history, nil } -func (api *ZonesAPI) getMoisture(ctx context.Context, g *pkg.Garden, z *pkg.Zone) (float64, error) { - defer api.influxdbClient.Close() - - moisture, err := api.influxdbClient.GetMoisture(ctx, *z.Position, g.TopicPrefix) - if err != nil { - return 0, err - } - return moisture, err -} - // getWaterHistory gets previous WaterEvents for this Zone from InfluxDB func (api *ZonesAPI) getWaterHistory(ctx context.Context, zone *pkg.Zone, garden *pkg.Garden, timeRange time.Duration, limit uint64) (result []pkg.WaterHistory, err error) { defer api.influxdbClient.Close() diff --git a/garden-app/server/zone_responses.go b/garden-app/server/zone_responses.go index 2dc57c44..0d0a02cc 100644 --- a/garden-app/server/zone_responses.go +++ b/garden-app/server/zone_responses.go @@ -12,7 +12,7 @@ import ( "github.com/go-chi/render" ) -// ZoneResponse is used to represent a Zone in the response body with the additional Moisture data +// ZoneResponse is used to represent a Zone in the response body with the additional data // and hypermedia Links fields type ZoneResponse struct { *pkg.Zone @@ -137,17 +137,6 @@ func (zr *ZoneResponse) Render(w http.ResponseWriter, r *http.Request) error { if nextWaterSchedule.HasWeatherControl() && !excludeWeatherData { zr.WeatherData = getWeatherData(ctx, nextWaterSchedule, zr.api.storageClient) - - if nextWaterSchedule.HasSoilMoistureControl() && garden != nil { - logger.Debug("getting moisture data for Zone") - soilMoisture, err := zr.api.getMoisture(ctx, garden, zr.Zone) - if err != nil { - logger.Warn("unable to get moisture data for Zone", "error", err) - } else { - logger.Debug("successfully got moisture data for Zone", "moisture", soilMoisture) - zr.WeatherData.SoilMoisturePercent = &soilMoisture - } - } } return nil diff --git a/garden-app/server/zone_test.go b/garden-app/server/zone_test.go index 682a197b..20f1e86c 100644 --- a/garden-app/server/zone_test.go +++ b/garden-app/server/zone_test.go @@ -106,7 +106,6 @@ func float32Pointer(n float64) *float32 { } func TestGetZone(t *testing.T) { - one := 1 weatherClientID, _ := xid.FromString("c5cvhpcbcv45e8bp16dg") tests := []struct { @@ -124,27 +123,7 @@ func TestGetZone(t *testing.T) { `{"name":"test-zone","id":"c5cvhpcbcv45e8bp16dg","garden_id":"c5cvhpcbcv45e8bp16dg","position":0,"created_at":"2021-10-03T11:24:52.891386-07:00","water_schedule_ids":\["c5cvhpcbcv45e8bp16dg"\],"skip_count":null,"next_water":{"time":"\d\d\d\d-\d\d-\d\dT11:24:52-07:00","duration":"1s","water_schedule_id":"c5cvhpcbcv45e8bp16dg"},"links":\[{"rel":"self","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg"},{"rel":"garden","href":"/gardens/c5cvhpcbcv45e8bp16dg"},{"rel":"action","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/action"},{"rel":"history","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/history"}\]}`, }, { - "SuccessfulWithMoisture", - false, - []*pkg.WaterSchedule{{ - ID: babyapi.ID{ID: id}, - Duration: &pkg.Duration{Duration: time.Second}, - Interval: &pkg.Duration{Duration: 24 * time.Hour}, - StartTime: pkg.NewStartTime(createdAt), - WeatherControl: &weather.Control{ - SoilMoisture: &weather.SoilMoistureControl{ - MinimumMoisture: &one, - }, - }, - }}, - func(influxdbClient *influxdb.MockClient) { - influxdbClient.On("GetMoisture", mock.Anything, mock.Anything, mock.Anything).Return(float64(2), nil) - influxdbClient.On("Close") - }, - `{"name":"test-zone","id":"c5cvhpcbcv45e8bp16dg","garden_id":"c5cvhpcbcv45e8bp16dg","position":0,"created_at":"2021-10-03T11:24:52.891386-07:00","water_schedule_ids":\["c5cvhpcbcv45e8bp16dg"\],"skip_count":null,"weather_data":{"soil_moisture_percent":2},"next_water":{"time":"\d\d\d\d-\d\d-\d\dT11:24:52-07:00","duration":"1s","water_schedule_id":"c5cvhpcbcv45e8bp16dg"},"links":\[{"rel":"self","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg"},{"rel":"garden","href":"/gardens/c5cvhpcbcv45e8bp16dg"},{"rel":"action","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/action"},{"rel":"history","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/history"}\]}`, - }, - { - "SuccessfulWithMoistureRainAndTemperatureData", + "SuccessfulWithRainAndTemperatureData", false, []*pkg.WaterSchedule{{ ID: babyapi.ID{ID: id}, @@ -152,9 +131,6 @@ func TestGetZone(t *testing.T) { Duration: &pkg.Duration{Duration: time.Hour}, StartTime: pkg.NewStartTime(createdAt), WeatherControl: &weather.Control{ - SoilMoisture: &weather.SoilMoistureControl{ - MinimumMoisture: &one, - }, Rain: &weather.ScaleControl{ BaselineValue: float32Pointer(0), Factor: float32Pointer(0), @@ -170,13 +146,12 @@ func TestGetZone(t *testing.T) { }, }}, func(influxdbClient *influxdb.MockClient) { - influxdbClient.On("GetMoisture", mock.Anything, mock.Anything, mock.Anything).Return(float64(2), nil) influxdbClient.On("Close") }, - `{"name":"test-zone","id":"c5cvhpcbcv45e8bp16dg","garden_id":"c5cvhpcbcv45e8bp16dg","position":0,"created_at":"2021-10-03T11:24:52.891386-07:00","water_schedule_ids":\["c5cvhpcbcv45e8bp16dg"\],"skip_count":null,"weather_data":{"rain":{"mm":25.4,"scale_factor":0},"average_temperature":{"celsius":80,"scale_factor":1.5},"soil_moisture_percent":2},"next_water":{"time":"\d\d\d\d-\d\d-\d\dT11:24:52-07:00","duration":"0s","water_schedule_id":"c5cvhpcbcv45e8bp16dg"},"links":\[{"rel":"self","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg"},{"rel":"garden","href":"/gardens/c5cvhpcbcv45e8bp16dg"},{"rel":"action","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/action"},{"rel":"history","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/history"}\]}`, + `{"name":"test-zone","id":"c5cvhpcbcv45e8bp16dg","garden_id":"c5cvhpcbcv45e8bp16dg","position":0,"created_at":"2021-10-03T11:24:52.891386-07:00","water_schedule_ids":\["c5cvhpcbcv45e8bp16dg"\],"skip_count":null,"weather_data":{"rain":{"mm":25.4,"scale_factor":0},"average_temperature":{"celsius":80,"scale_factor":1.5}},"next_water":{"time":"\d\d\d\d-\d\d-\d\dT11:24:52-07:00","duration":"0s","water_schedule_id":"c5cvhpcbcv45e8bp16dg"},"links":\[{"rel":"self","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg"},{"rel":"garden","href":"/gardens/c5cvhpcbcv45e8bp16dg"},{"rel":"action","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/action"},{"rel":"history","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/history"}\]}`, }, { - "SuccessfulWithMoistureRainAndTemperatureDataButWeatherDataExcluded", + "SuccessfulWithRainAndTemperatureDataButWeatherDataExcluded", true, []*pkg.WaterSchedule{{ ID: babyapi.ID{ID: id}, @@ -184,9 +159,6 @@ func TestGetZone(t *testing.T) { Duration: &pkg.Duration{Duration: time.Hour}, StartTime: pkg.NewStartTime(createdAt), WeatherControl: &weather.Control{ - SoilMoisture: &weather.SoilMoistureControl{ - MinimumMoisture: &one, - }, Rain: &weather.ScaleControl{ BaselineValue: float32Pointer(0), Factor: float32Pointer(0), @@ -204,26 +176,6 @@ func TestGetZone(t *testing.T) { func(_ *influxdb.MockClient) {}, `{"name":"test-zone","id":"c5cvhpcbcv45e8bp16dg","garden_id":"c5cvhpcbcv45e8bp16dg","position":0,"created_at":"2021-10-03T11:24:52.891386-07:00","water_schedule_ids":\["c5cvhpcbcv45e8bp16dg"\],"skip_count":null,"next_water":{"time":"\d\d\d\d-\d\d-\d\dT11:24:52-07:00","duration":"1h0m0s","water_schedule_id":"c5cvhpcbcv45e8bp16dg"},"links":\[{"rel":"self","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg"},{"rel":"garden","href":"/gardens/c5cvhpcbcv45e8bp16dg"},{"rel":"action","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/action"},{"rel":"history","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/history"}\]}`, }, - { - "ErrorGettingMoisture", - false, - []*pkg.WaterSchedule{{ - ID: babyapi.ID{ID: id}, - Duration: &pkg.Duration{Duration: time.Second}, - Interval: &pkg.Duration{Duration: time.Hour * 24}, - StartTime: pkg.NewStartTime(createdAt), - WeatherControl: &weather.Control{ - SoilMoisture: &weather.SoilMoistureControl{ - MinimumMoisture: &one, - }, - }, - }}, - func(influxdbClient *influxdb.MockClient) { - influxdbClient.On("GetMoisture", mock.Anything, mock.Anything, mock.Anything).Return(float64(2), errors.New("influxdb error")) - influxdbClient.On("Close") - }, - `{"name":"test-zone","id":"c5cvhpcbcv45e8bp16dg","garden_id":"c5cvhpcbcv45e8bp16dg","position":0,"created_at":"2021-10-03T11:24:52.891386-07:00","water_schedule_ids":\["c5cvhpcbcv45e8bp16dg"\],"skip_count":null,"weather_data":{},"next_water":{"time":"\d\d\d\d-\d\d-\d\dT11:24:52-07:00","duration":"1s","water_schedule_id":"c5cvhpcbcv45e8bp16dg"},"links":\[{"rel":"self","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg"},{"rel":"garden","href":"/gardens/c5cvhpcbcv45e8bp16dg"},{"rel":"action","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/action"},{"rel":"history","href":"/gardens/c5cvhpcbcv45e8bp16dg/zones/c5cvhpcbcv45e8bp16dg/history"}\]}`, - }, } for _, tt := range tests { diff --git a/garden-app/worker/water_schedule_actions.go b/garden-app/worker/water_schedule_actions.go index f5ecca85..0b4bbf42 100644 --- a/garden-app/worker/water_schedule_actions.go +++ b/garden-app/worker/water_schedule_actions.go @@ -7,7 +7,6 @@ import ( "github.com/calvinmclean/automated-garden/garden-app/pkg" "github.com/calvinmclean/automated-garden/garden-app/pkg/action" - "github.com/calvinmclean/automated-garden/garden-app/pkg/influxdb" ) // ExecuteScheduledWaterAction will run ExecuteWaterAction after checking SkipCount and scaling based on weather data @@ -46,37 +45,10 @@ func (w *Worker) exerciseWeatherControl(g *pkg.Garden, z *pkg.Zone, ws *pkg.Wate return ws.Duration.Duration, nil } - skipMoisture, err := w.shouldMoistureSkip(g, z, ws) - if err != nil { - return 0, err - } - if skipMoisture { - return 0, nil - } - duration, _ := w.ScaleWateringDuration(ws) return duration, nil } -func (w *Worker) shouldMoistureSkip(g *pkg.Garden, z *pkg.Zone, ws *pkg.WaterSchedule) (bool, error) { - if !ws.HasSoilMoistureControl() { - return false, nil - } - - ctx, cancel := context.WithTimeout(context.Background(), influxdb.QueryTimeout) - defer cancel() - - defer w.influxdbClient.Close() - moisture, err := w.influxdbClient.GetMoisture(ctx, *z.Position, g.TopicPrefix) - if err != nil { - return false, fmt.Errorf("error getting Zone's moisture data: %w", err) - } - w.logger.Info("got soil moisture", "moisture_percent", moisture) - - // if moisture > minimum, skip watering - return moisture > float64(*ws.WeatherControl.SoilMoisture.MinimumMoisture), nil -} - // ScaleWateringDuration returns a new watering duration based on weather scaling. It will not return // any errors if they are encountered because there are multiple factors impacting watering func (w *Worker) ScaleWateringDuration(ws *pkg.WaterSchedule) (time.Duration, bool) { diff --git a/garden-app/worker/water_schedule_actions_test.go b/garden-app/worker/water_schedule_actions_test.go index 7c972409..33c5b6a4 100644 --- a/garden-app/worker/water_schedule_actions_test.go +++ b/garden-app/worker/water_schedule_actions_test.go @@ -2,7 +2,6 @@ package worker import ( "context" - "errors" "log/slog" "testing" "time" @@ -38,8 +37,6 @@ func TestExecuteScheduledWaterAction(t *testing.T) { ClientID: weatherClientID, } - fifty := 50 - tests := []struct { name string waterSchedule *pkg.WaterSchedule @@ -60,69 +57,6 @@ func TestExecuteScheduledWaterAction(t *testing.T) { }, "", }, - { - "SuccessWhenMoistureLessThanMinimum", - &pkg.WaterSchedule{ - Duration: &pkg.Duration{Duration: time.Second}, - Interval: &pkg.Duration{Duration: time.Hour * 24}, - WeatherControl: &weather.Control{ - SoilMoisture: &weather.SoilMoistureControl{ - MinimumMoisture: &fifty, - }, - }, - }, - &pkg.Zone{ - Position: uintPointer(0), - }, - func(mqttClient *mqtt.MockClient, influxdbClient *influxdb.MockClient, sc *storage.Client) { - mqttClient.On("Publish", "garden/command/water", mock.Anything).Return(nil) - influxdbClient.On("GetMoisture", mock.Anything, uint(0), garden.Name).Return(float64(0), nil) - influxdbClient.On("Close") - }, - "", - }, - { - "SuccessWhenMoistureGreaterThanMinimum", - &pkg.WaterSchedule{ - Duration: &pkg.Duration{Duration: time.Second}, - Interval: &pkg.Duration{Duration: time.Hour * 24}, - WeatherControl: &weather.Control{ - SoilMoisture: &weather.SoilMoistureControl{ - MinimumMoisture: &fifty, - }, - }, - }, - &pkg.Zone{ - Position: uintPointer(0), - }, - func(mqttClient *mqtt.MockClient, influxdbClient *influxdb.MockClient, sc *storage.Client) { - influxdbClient.On("GetMoisture", mock.Anything, uint(0), garden.Name).Return(float64(51), nil) - influxdbClient.On("Close") - // No MQTT calls made - }, - "", - }, - { - "InfluxDBClientErrorStillWaters", - &pkg.WaterSchedule{ - Duration: &pkg.Duration{Duration: time.Second}, - Interval: &pkg.Duration{Duration: time.Hour * 24}, - WeatherControl: &weather.Control{ - SoilMoisture: &weather.SoilMoistureControl{ - MinimumMoisture: &fifty, - }, - }, - }, - &pkg.Zone{ - Position: uintPointer(0), - }, - func(mqttClient *mqtt.MockClient, influxdbClient *influxdb.MockClient, sc *storage.Client) { - mqttClient.On("Publish", "garden/command/water", mock.Anything).Return(nil) - influxdbClient.On("GetMoisture", mock.Anything, uint(0), garden.Name).Return(float64(0), errors.New("influxdb error")) - influxdbClient.On("Close") - }, - "", - }, { "SuccessfulRainScaleToZero", &pkg.WaterSchedule{ diff --git a/garden-controller/include/buttons.h b/garden-controller/include/buttons.h deleted file mode 100644 index 89c0863c..00000000 --- a/garden-controller/include/buttons.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef buttons_h -#define buttons_h - -#include -#include "config.h" - -#define DEBOUNCE_DELAY 50 - -void setupButtons(); -void readButtonsTask(void* parameters); -void readButton(int valveID); -void readStopButton(); - -extern TaskHandle_t readButtonsTaskHandle; - -#endif diff --git a/garden-controller/include/config.h b/garden-controller/include/config.h index 1a0e5f6b..00576760 100644 --- a/garden-controller/include/config.h +++ b/garden-controller/include/config.h @@ -19,60 +19,28 @@ #define MQTT_ADDRESS "192.168.0.107" #define MQTT_PORT 30002 -// Enable publishing health status to MQTT -#define ENABLE_MQTT_HEALTH - -// Enable logging messages to MQTT -#define ENABLE_MQTT_LOGGING - /** * Garden Configurations * - * DISABLE_WATERING - * Allows disabling Pump/Valve pins and doesn't listen on relevant MQTT topics. This allows a sensor-only Garden * NUM_ZONES * Number of zones in the ZONES list - * ZONES - * List of zone pins in this format: { {PUMP_PIN, VALVE_PIN, BUTTON_PIN, MOISTURE_SENSOR_PIN} } - * You can create multiple zones before creating the ZONES list for improved readability (see example below) - * DEFAULT_WATER_TIME - * Default time to water for if none is specified. This is used by buttons - * ENABLE_BUTTONS - * Configure if there are any hardware buttons corresponding to zones - * STOP_BUTTON_PIN - * Pin used for the button that will stop all watering + * VALVES + * List of valve pins + * PUMPS + * List of pump pins. If a pump is not being used for this Zone, just use the same pin as the valve. * LIGHT_PIN * The pin used to control a grow light relay */ - // #define DISABLE_WATERING #define NUM_ZONES 3 -#define PUMP_PIN GPIO_NUM_18 -#define ZONE_1 { PUMP_PIN, GPIO_NUM_16, GPIO_NUM_19, GPIO_NUM_36 } -#define ZONE_2 { PUMP_PIN, GPIO_NUM_17, GPIO_NUM_21, GPIO_NUM_39 } -#define ZONE_3 { PUMP_PIN, GPIO_NUM_5, GPIO_NUM_22, GPIO_NUM_34 } -#define ZONES { ZONE_1, ZONE_2, ZONE_3 } -#define DEFAULT_WATER_TIME 5000 +#define VALVES { GPIO_NUM_16, GPIO_NUM_17, GPIO_NUM_5 } +#define PUMPS { GPIO_NUM_18, GPIO_NUM_18, GPIO_NUM_18 } +#define LIGHT_ENABLED true #define LIGHT_PIN GPIO_NUM_32 -// #define ENABLE_BUTTONS -#ifdef ENABLE_BUTTONS -#define STOP_BUTTON_PIN GPIO_NUM_23 -#endif - -// Currently, moisture sensing requires MQTT because the logic for handling this data lives in the garden-app -// #define ENABLE_MOISTURE_SENSORS -#ifdef ENABLE_MOISTURE_SENSORS -#define MOISTURE_SENSOR_AIR_VALUE 3415 -#define MOISTURE_SENSOR_WATER_VALUE 1362 -#define MOISTURE_SENSOR_INTERVAL 5000 -#endif - // DHT22 Temperature and Humidity sensor -#define ENABLE_DHT22 -#ifdef ENABLE_DHT22 +#define ENABLE_DHT22 true #define DHT22_PIN GPIO_NUM_27 #define DHT22_INTERVAL 5000 -#endif #endif diff --git a/garden-controller/include/main.h b/garden-controller/include/main.h index adabc661..54389739 100644 --- a/garden-controller/include/main.h +++ b/garden-controller/include/main.h @@ -19,6 +19,8 @@ void stopWatering(); void stopAllWatering(); void changeLight(LightEvent le); -extern gpio_num_t zones[NUM_ZONES][4]; +extern gpio_num_t zones[NUM_ZONES]; +extern gpio_num_t pumps[NUM_ZONES]; +extern bool lightEnabled; #endif diff --git a/garden-controller/include/moisture.h b/garden-controller/include/moisture.h deleted file mode 100644 index 8d1fc00f..00000000 --- a/garden-controller/include/moisture.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef moisture_h -#define moisture_h - -extern TaskHandle_t moistureSensorTaskHandle; - -void setupMoistureSensors(); -int readMoisturePercentage(int position); -void moistureSensorTask(void* parameters); - -#endif diff --git a/garden-controller/include/mqtt.h b/garden-controller/include/mqtt.h index e434c9f4..efad4e6b 100644 --- a/garden-controller/include/mqtt.h +++ b/garden-controller/include/mqtt.h @@ -32,23 +32,13 @@ #define MQTT_LIGHT_DATA_TOPIC "/data/light" #define MQTT_WATER_DATA_TOPIC "/data/water" -#ifdef ENABLE_MQTT_LOGGING #define MQTT_LOGGING_TOPIC "/data/logs" -#endif -#ifdef ENABLE_MQTT_HEALTH #define MQTT_HEALTH_DATA_TOPIC "/data/health" #define HEALTH_PUBLISH_INTERVAL 60000 -#endif -#ifdef ENABLE_DHT22 #define MQTT_TEMPERATURE_DATA_TOPIC "/data/temperature" #define MQTT_HUMIDITY_DATA_TOPIC "/data/humidity" -#endif - -#ifdef ENABLE_MOISTURE_SENSORS -#define MQTT_MOISTURE_DATA_TOPIC "/data/moisture" -#endif extern PubSubClient client; @@ -68,9 +58,7 @@ extern TaskHandle_t mqttLoopTaskHandle; extern TaskHandle_t healthPublisherTaskHandle; extern TaskHandle_t waterPublisherTaskHandle; extern QueueHandle_t waterPublisherQueue; -#ifdef LIGHT_PIN extern QueueHandle_t lightPublisherQueue; extern TaskHandle_t lightPublisherTaskHandle; -#endif #endif diff --git a/garden-controller/platformio.ini b/garden-controller/platformio.ini index 8c10b300..1d233d91 100644 --- a/garden-controller/platformio.ini +++ b/garden-controller/platformio.ini @@ -14,7 +14,7 @@ board = esp32dev framework = arduino monitor_speed = 115200 board_build.filesystem = littlefs -lib_deps = +lib_deps = bblanchon/ArduinoJson@^6.21.3 knolleary/PubSubClient@^2.8 adafruit/DHT sensor library@^1.4.4 diff --git a/garden-controller/src/buttons.cpp b/garden-controller/src/buttons.cpp deleted file mode 100644 index 4cc163a7..00000000 --- a/garden-controller/src/buttons.cpp +++ /dev/null @@ -1,105 +0,0 @@ -#include "config.h" -#ifdef ENABLE_BUTTONS -#include "buttons.h" -#include "main.h" - -TaskHandle_t readButtonsTaskHandle; - -/* button variables */ -unsigned long lastDebounceTime = 0; -int buttonStates[NUM_ZONES]; -int lastButtonStates[NUM_ZONES]; - -/* stop button variables */ -unsigned long lastStopDebounceTime = 0; -int stopButtonState = LOW; -int lastStopButtonState; - -void setupButtons() { - // Setup button pins and state - for (int i = 0; i < NUM_ZONES; i++) { - gpio_set_direction(zones[i][2], GPIO_MODE_INPUT); - buttonStates[i] = LOW; - lastButtonStates[i] = LOW; - } - - xTaskCreate(readButtonsTask, "ReadButtonsTask", 2048, NULL, 1, &readButtonsTaskHandle); -} - -/* - readButtonsTask will check if any buttons are being pressed -*/ -void readButtonsTask(void* parameters) { - while (true) { - // Check if any valves need to be stopped and check all buttons - for (int i = 0; i < NUM_ZONES; i++) { - readButton(i); - } - readStopButton(); - vTaskDelay(5 / portTICK_PERIOD_MS); - } - vTaskDelete(NULL); -} - -/* - readButton takes an ID that represents the array index for the valve and - button arrays and checks if the button is pressed. If the button is pressed, - a WaterEvent for that zone is added to the queue -*/ -void readButton(int valveID) { - // Exit if valveID is out of bounds - if (valveID >= NUM_ZONES || valveID < 0) { - return; - } - int reading = gpio_get_level(zones[valveID][2]); - // If the switch changed, due to noise or pressing, reset debounce timer - if (reading != lastButtonStates[valveID]) { - lastDebounceTime = millis(); - } - - // Current reading has been the same longer than our delay, so now we can do something - if ((millis() - lastDebounceTime) > DEBOUNCE_DELAY) { - // If the button state has changed - if (reading != buttonStates[valveID]) { - buttonStates[valveID] = reading; - - // If our button state is HIGH, water the zone - if (reading == HIGH && buttonStates[valveID] == HIGH) { - printf("button pressed: %d\n", valveID); - WaterEvent we = { valveID, DEFAULT_WATER_TIME, "N/A" }; - waterZone(we); - } - } - } - lastButtonStates[valveID] = reading; -} - -/* - readStopButton is similar to the readButton function, but had to be separated because this - button does not correspond to a Valve and could not be included in the array of buttons. -*/ -void readStopButton() { - int reading = gpio_get_level(STOP_BUTTON_PIN); - // If the switch changed, due to noise or pressing, reset debounce timer - if (reading != lastStopButtonState) { - lastStopDebounceTime = millis(); - } - - // Current reading has been the same longer than our delay, so now we can do something - if ((millis() - lastStopDebounceTime) > DEBOUNCE_DELAY) { - // If the button state has changed - if (reading != stopButtonState) { - stopButtonState = reading; - - // If our button state is HIGH, do some things - if (stopButtonState == HIGH) { - if (reading == HIGH) { - printf("stop button pressed\n"); - stopWatering(); - } - } - } - } - lastStopButtonState = reading; -} -#endif diff --git a/garden-controller/src/dht22.cpp b/garden-controller/src/dht22.cpp index 1f1bd217..e4acd364 100644 --- a/garden-controller/src/dht22.cpp +++ b/garden-controller/src/dht22.cpp @@ -1,5 +1,4 @@ #include "config.h" -#ifdef ENABLE_DHT22 #include #include "dht22.h" @@ -50,5 +49,3 @@ void dht22PublishTask(void* parameters) { } vTaskDelete(NULL); } - -#endif diff --git a/garden-controller/src/main.cpp b/garden-controller/src/main.cpp index 896f9cff..4507f20d 100644 --- a/garden-controller/src/main.cpp +++ b/garden-controller/src/main.cpp @@ -7,19 +7,18 @@ #include "mqtt.h" #include "main.h" #include "wifi_manager.h" -#ifdef ENABLE_BUTTONS -#include "buttons.h" -#endif -#ifdef ENABLE_MOISTURE_SENSORS -#include "moisture.h" -#endif -#ifdef ENABLE_DHT22 #include "dht22.h" -#endif -/* zone/valve variables */ -gpio_num_t zones[NUM_ZONES][4] = ZONES; +/* zone valve and pump variables */ +gpio_num_t valves[NUM_ZONES] = VALVES; +gpio_num_t pumps[NUM_ZONES] = PUMPS; + +/* light variables */ +bool lightEnabled = LIGHT_ENABLED; +gpio_num_t lightPin = LIGHT_PIN; + +bool dht22Enabled = ENABLE_DHT22; /* FreeRTOS Queue and Task handlers */ QueueHandle_t waterQueue; @@ -28,62 +27,22 @@ TaskHandle_t waterZoneTaskHandle; /* state variables */ int light_state; -void setup() { -#ifndef DISABLE_WATERING - // Prepare pins - for (int i = 0; i < NUM_ZONES; i++) { - // Setup valve pins - gpio_reset_pin(zones[i][1]); - gpio_set_direction(zones[i][1], GPIO_MODE_OUTPUT); - - // Setup pump pins - gpio_reset_pin(zones[i][0]); - gpio_set_direction(zones[i][0], GPIO_MODE_OUTPUT); - } -#endif - -#ifdef LIGHT_PIN - gpio_reset_pin(LIGHT_PIN); - gpio_set_direction(LIGHT_PIN, GPIO_MODE_OUTPUT); - light_state = 0; -#endif - - setupWifiManager(); - setupWifi(); - setupMQTT(); -#ifdef ENABLE_MOISTURE_SENSORS - setupMoistureSensors(); -#endif - -#ifdef ENABLE_DHT22 - setupDHT22(); -#endif +void setupZones() { + for (int i = 0; i < NUM_ZONES; i++) { + // Setup valve and pump pins + gpio_reset_pin(valves[i]); + gpio_set_direction(valves[i], GPIO_MODE_OUTPUT); -#ifdef ENABLE_BUTTONS - setupButtons(); -#endif - - // Initialize Queues - waterQueue = xQueueCreate(QUEUE_SIZE, sizeof(WaterEvent)); - if (waterQueue == NULL) { - printf("error creating the waterQueue\n"); - } - - // Start all tasks (currently using equal priorities) - xTaskCreate(waterZoneTask, "WaterZoneTask", 2048, NULL, 1, &waterZoneTaskHandle); - -#ifdef ENABLE_MQTT_LOGGING - // Delay 1 second to allow MQTT to connect - delay(1000); - if (client.connected()) { - client.publish(MQTT_LOGGING_TOPIC, "logs message=\"garden-controller setup complete\""); - } else { - printf("unable to publish: not connected to MQTT broker\n"); - } -#endif + gpio_reset_pin(pumps[i]); + gpio_set_direction(pumps[i], GPIO_MODE_OUTPUT); + } } -void loop() {} +void setupLight() { + gpio_reset_pin(lightPin); + gpio_set_direction(lightPin, GPIO_MODE_OUTPUT); + light_state = 0; +} /* waterZoneTask will wait for WaterEvents on a queue and will then open the @@ -100,10 +59,6 @@ void waterZoneTask(void* parameters) { // watering to be skipped if I run xTaskNotify when not waiting ulTaskNotifyTake(NULL, 0); - if (we.duration == 0) { - we.duration = DEFAULT_WATER_TIME; - } - unsigned long start = millis(); zoneOn(we.position); // Delay for specified watering time with option to interrupt @@ -123,8 +78,8 @@ void waterZoneTask(void* parameters) { */ void zoneOn(int id) { printf("turning on zone %d\n", id); - gpio_set_level(zones[id][0], 1); - gpio_set_level(zones[id][1], 1); + gpio_set_level(pumps[id], 1); + gpio_set_level(valves[id], 1); } /* @@ -132,8 +87,8 @@ void zoneOn(int id) { */ void zoneOff(int id) { printf("turning off zone %d\n", id); - gpio_set_level(zones[id][0], 0); - gpio_set_level(zones[id][1], 0); + gpio_set_level(pumps[id], 0); + gpio_set_level(valves[id], 0); } /* @@ -166,7 +121,6 @@ void waterZone(WaterEvent we) { xQueueSend(waterQueue, &we, portMAX_DELAY); } -#ifdef LIGHT_PIN /* changeLight will use the state on the LightEvent to change the state of the light. If the state is empty, this will toggle the current state. @@ -183,9 +137,42 @@ void changeLight(LightEvent le) { printf("Unrecognized LightEvent.state, so state will be unchanged\n"); } printf("Setting light state to %d\n", light_state); - gpio_set_level(LIGHT_PIN, light_state); + gpio_set_level(lightPin, light_state); // Log data to MQTT if enabled xQueueSend(lightPublisherQueue, &light_state, portMAX_DELAY); } -#endif + +void setup() { + setupZones(); + if (lightEnabled) { + setupLight(); + } + + setupWifiManager(); + setupWifi(); + setupMQTT(); + + if (dht22Enabled) { + setupDHT22(); + } + + // Initialize Queues + waterQueue = xQueueCreate(QUEUE_SIZE, sizeof(WaterEvent)); + if (waterQueue == NULL) { + printf("error creating the waterQueue\n"); + } + + // Start all tasks (currently using equal priorities) + xTaskCreate(waterZoneTask, "WaterZoneTask", 2048, NULL, 1, &waterZoneTaskHandle); + + // Delay 1 second to allow MQTT to connect + delay(1000); + if (client.connected()) { + client.publish(MQTT_LOGGING_TOPIC, "logs message=\"garden-controller setup complete\""); + } else { + printf("unable to publish: not connected to MQTT broker\n"); + } +} + +void loop() {} diff --git a/garden-controller/src/moisture.cpp b/garden-controller/src/moisture.cpp deleted file mode 100644 index 89cd0a5c..00000000 --- a/garden-controller/src/moisture.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "config.h" -#ifdef ENABLE_MOISTURE_SENSORS - -#include -#include "moisture.h" -#include "main.h" -#include "mqtt.h" - -TaskHandle_t moistureSensorTaskHandle; - -const char* moistureDataTopic = MQTT_MOISTURE_DATA_TOPIC; - -void setupMoistureSensors() { - for (int i = 0; i < NUM_ZONES; i++) { - if (zones[i][3] == GPIO_NUM_MAX) { - continue; - } - gpio_reset_pin(zones[i][3]); - gpio_set_direction(zones[i][3], GPIO_MODE_INPUT); - } - - xTaskCreate(moistureSensorTask, "MoistureSensorTask", 2048, NULL, 1, &moistureSensorTaskHandle); -} - -int readMoisturePercentage(int position) { - int value = analogRead(zones[position][3]); - printf("Moisture value: %d\n", value); - int percentage = map(value, MOISTURE_SENSOR_AIR_VALUE, MOISTURE_SENSOR_WATER_VALUE, 0, 100); - printf("Moisture percentage: %d\n", percentage); - if (percentage < 0) { - percentage = 0; - } else if (percentage > 100) { - percentage = 100; - } - return percentage; -} - -void moistureSensorTask(void* parameters) { - while (true) { - for (int zone = 0; zone < NUM_ZONES; zone++) { - if (zones[zone][3] == GPIO_NUM_MAX) { - continue; - } - int percentage = readMoisturePercentage(zone); - char message[50]; - sprintf(message, "moisture,zone=%d value=%d", zone, percentage); - if (client.connected()) { - printf("publishing to MQTT:\n\ttopic=%s\n\tmessage=%s\n", moistureDataTopic, message); - client.publish(moistureDataTopic, message); - } else { - printf("unable to publish: not connected to MQTT broker\n"); - } - } - vTaskDelay(MOISTURE_SENSOR_INTERVAL / portTICK_PERIOD_MS); - } - vTaskDelete(NULL); -} - -#endif diff --git a/garden-controller/src/mqtt.cpp b/garden-controller/src/mqtt.cpp index e9902ffc..b7cb947b 100644 --- a/garden-controller/src/mqtt.cpp +++ b/garden-controller/src/mqtt.cpp @@ -10,10 +10,8 @@ TaskHandle_t mqttLoopTaskHandle; TaskHandle_t healthPublisherTaskHandle; TaskHandle_t waterPublisherTaskHandle; QueueHandle_t waterPublisherQueue; -#ifdef LIGHT_PIN QueueHandle_t lightPublisherQueue; TaskHandle_t lightPublisherTaskHandle; -#endif char waterCommandTopic[50]; char stopCommandTopic[50]; @@ -50,16 +48,15 @@ void setupMQTT() { xTaskCreate(mqttConnectTask, "MQTTConnectTask", 2048, NULL, 1, &mqttConnectTaskHandle); xTaskCreate(mqttLoopTask, "MQTTLoopTask", 4096, NULL, 1, &mqttLoopTaskHandle); xTaskCreate(waterPublisherTask, "WaterPublisherTask", 2048, NULL, 1, &waterPublisherTaskHandle); -#ifdef LIGHT_PIN - lightPublisherQueue = xQueueCreate(QUEUE_SIZE, sizeof(LightEvent)); - if (lightPublisherQueue == NULL) { - printf("error creating the lightPublisherQueue\n"); + + if (lightEnabled) { + lightPublisherQueue = xQueueCreate(QUEUE_SIZE, sizeof(LightEvent)); + if (lightPublisherQueue == NULL) { + printf("error creating the lightPublisherQueue\n"); + } + xTaskCreate(lightPublisherTask, "LightPublisherTask", 2048, NULL, 1, &lightPublisherTaskHandle); + xTaskCreate(healthPublisherTask, "HealthPublisherTask", 2048, NULL, 1, &healthPublisherTaskHandle); } - xTaskCreate(lightPublisherTask, "LightPublisherTask", 2048, NULL, 1, &lightPublisherTaskHandle); -#endif -#ifdef ENABLE_MQTT_HEALTH - xTaskCreate(healthPublisherTask, "HealthPublisherTask", 2048, NULL, 1, &healthPublisherTaskHandle); -#endif } void setupWifi() { @@ -105,7 +102,6 @@ void waterPublisherTask(void* parameters) { vTaskDelete(NULL); } -#ifdef LIGHT_PIN /* lightPublisherTask reads from a queue to publish LightEvents as an InfluxDB line protocol message to MQTT @@ -127,9 +123,7 @@ void lightPublisherTask(void* parameters) { } vTaskDelete(NULL); } -#endif -#ifdef ENABLE_MQTT_HEALTH /* healthPublisherTask runs every minute and publishes a message to MQTT to record a health check-in */ @@ -148,7 +142,6 @@ void healthPublisherTask(void* parameters) { } vTaskDelete(NULL); } -#endif /* mqttConnectTask will periodically attempt to reconnect to MQTT if needed @@ -161,14 +154,13 @@ void mqttConnectTask(void* parameters) { // Connect with defaul arguments + cleanSession = false for persistent sessions if (client.connect(mqtt_topic_prefix, NULL, NULL, 0, 0, 0, 0, false)) { printf("connected\n"); -#ifndef DISABLE_WATERING client.subscribe(waterCommandTopic, 1); client.subscribe(stopCommandTopic, 1); client.subscribe(stopAllCommandTopic, 1); -#endif -#ifdef LIGHT_PIN - client.subscribe(lightCommandTopic, 1); -#endif + + if (lightEnabled) { + client.subscribe(lightCommandTopic, 1); + } } else { printf("failed, rc=%zu\n", client.state()); } @@ -229,10 +221,8 @@ void processIncomingMessage(char* topic, byte* message, unsigned int length) { LightEvent le = { doc["state"] | "" }; -#ifdef LIGHT_PIN printf("received command to change state of the light: '%s'\n", le.state); changeLight(le); -#endif } } diff --git a/garden-controller/src/wifi_manager.cpp b/garden-controller/src/wifi_manager.cpp index 98236c57..c6c33a52 100644 --- a/garden-controller/src/wifi_manager.cpp +++ b/garden-controller/src/wifi_manager.cpp @@ -67,19 +67,19 @@ void setupFS() { std::unique_ptr buf(new char[size]); configFile.readBytes(buf.get(), size); + configFile.close(); DynamicJsonDocument json(1024); auto deserializeError = deserializeJson(json, buf.get()); - if (!deserializeError) { - strcpy(mqtt_server, json["mqtt_server"]); - strcpy(mqtt_topic_prefix, json["mqtt_topic_prefix"]); - mqtt_port = json["mqtt_port"]; - - printf("loaded config JSON: %s %s %d\n", mqtt_server, mqtt_topic_prefix, mqtt_port); - } else { + if (deserializeError) { printf("failed to load json config\n"); + return; } - configFile.close(); + strcpy(mqtt_server, json["mqtt_server"]); + strcpy(mqtt_topic_prefix, json["mqtt_topic_prefix"]); + mqtt_port = json["mqtt_port"]; + + printf("loaded config JSON: %s %s %d\n", mqtt_server, mqtt_topic_prefix, mqtt_port); } /* From 86fcb9dc7dbd131c59bdc2655c3c34ce7d0bbda8 Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sat, 7 Sep 2024 16:42:05 -0700 Subject: [PATCH 3/9] Add temporary garden-controller-config-test - Currenlty using this project to explore serializing the controller configuration to JSON --- garden-controller-config-test/.gitignore | 1 + garden-controller-config-test/include/README | 39 +++++++++ .../include/garden_config.h | 27 +++++++ garden-controller-config-test/platformio.ini | 10 +++ .../src/garden_config.cpp | 56 +++++++++++++ garden-controller-config-test/test/README | 11 +++ .../test/test_garden_config.cpp | 79 +++++++++++++++++++ 7 files changed, 223 insertions(+) create mode 100644 garden-controller-config-test/.gitignore create mode 100644 garden-controller-config-test/include/README create mode 100644 garden-controller-config-test/include/garden_config.h create mode 100644 garden-controller-config-test/platformio.ini create mode 100644 garden-controller-config-test/src/garden_config.cpp create mode 100644 garden-controller-config-test/test/README create mode 100644 garden-controller-config-test/test/test_garden_config.cpp diff --git a/garden-controller-config-test/.gitignore b/garden-controller-config-test/.gitignore new file mode 100644 index 00000000..03f4a3c1 --- /dev/null +++ b/garden-controller-config-test/.gitignore @@ -0,0 +1 @@ +.pio diff --git a/garden-controller-config-test/include/README b/garden-controller-config-test/include/README new file mode 100644 index 00000000..194dcd43 --- /dev/null +++ b/garden-controller-config-test/include/README @@ -0,0 +1,39 @@ + +This directory is intended for project header files. + +A header file is a file containing C declarations and macro definitions +to be shared between several project source files. You request the use of a +header file in your project source file (C, C++, etc) located in `src` folder +by including it, with the C preprocessing directive `#include'. + +```src/main.c + +#include "header.h" + +int main (void) +{ + ... +} +``` + +Including a header file produces the same results as copying the header file +into each source file that needs it. Such copying would be time-consuming +and error-prone. With a header file, the related declarations appear +in only one place. If they need to be changed, they can be changed in one +place, and programs that include the header file will automatically use the +new version when next recompiled. The header file eliminates the labor of +finding and changing all the copies as well as the risk that a failure to +find one copy will result in inconsistencies within a program. + +In C, the usual convention is to give header files names that end with `.h'. +It is most portable to use only letters, digits, dashes, and underscores in +header file names, and at most one dot. + +Read more about using header files in official GCC documentation: + +* Include Syntax +* Include Operation +* Once-Only Headers +* Computed Includes + +https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html diff --git a/garden-controller-config-test/include/garden_config.h b/garden-controller-config-test/include/garden_config.h new file mode 100644 index 00000000..0f1b4757 --- /dev/null +++ b/garden-controller-config-test/include/garden_config.h @@ -0,0 +1,27 @@ +#ifndef GARDEN_CONFIG_H +#define GARDEN_CONFIG_H + +#include +#include + +struct Config { + int mqttPort; + const char* mqttServer; + const char* mqttTopicPrefix; + + int numZones; + gpio_num_t zonePins[12]; + gpio_num_t pumpPins[12]; + + bool light; + gpio_num_t lightPin; + + bool dht22; + gpio_num_t dht22Pin; + int dht22Interval; +}; + +void serializeConfig(const Config& config, String& jsonString); +bool deserializeConfig(const String& jsonString, Config& config); + +#endif diff --git a/garden-controller-config-test/platformio.ini b/garden-controller-config-test/platformio.ini new file mode 100644 index 00000000..699faaa2 --- /dev/null +++ b/garden-controller-config-test/platformio.ini @@ -0,0 +1,10 @@ +[env:esp32dev] +platform = espressif32 +board = esp32dev +framework = arduino +monitor_speed = 115200 +board_build.filesystem = littlefs +test_framework = unity +lib_deps = + bblanchon/ArduinoJson@^6.21.3 +test_build_src = yes diff --git a/garden-controller-config-test/src/garden_config.cpp b/garden-controller-config-test/src/garden_config.cpp new file mode 100644 index 00000000..cdde40ea --- /dev/null +++ b/garden-controller-config-test/src/garden_config.cpp @@ -0,0 +1,56 @@ +#include "garden_config.h" + +// Write Config to JSON +void serializeConfig(const Config& config, String& jsonString) { + DynamicJsonDocument doc(1024); + + doc["mqttPort"] = config.mqttPort; + doc["mqttServer"] = config.mqttServer; + doc["mqttTopicPrefix"] = config.mqttTopicPrefix; + + doc["numZones"] = config.numZones; + for (int i = 0; i < config.numZones; i++) { + doc["zonePins"][i] = config.zonePins[i]; + doc["pumpPins"][i] = config.pumpPins[i]; + } + + doc["light"] = config.light; + doc["lightPin"] = config.lightPin; + + doc["dht22"] = config.dht22; + doc["dht22Pin"] = config.dht22Pin; + doc["dht22Interval"] = config.dht22Interval; + + serializeJson(doc, jsonString); +} + +// Read Config from JSON +bool deserializeConfig(const String& jsonString, Config& config) { + DynamicJsonDocument doc(1024); + + DeserializationError error = deserializeJson(doc, jsonString); + + if (error) { + printf("deserialize config failed: %s\n", error.c_str()); + return false; + } + + config.mqttPort = doc["mqttPort"].as(); + config.mqttServer = doc["mqttServer"].as(); + config.mqttTopicPrefix = doc["mqttTopicPrefix"].as(); + + config.numZones = doc["numZones"].as(); + for (int i = 0; i < config.numZones; i++) { + config.zonePins[i] = static_cast(doc["zonePins"][i].as()); + config.pumpPins[i] = static_cast(doc["pumpPins"][i].as()); + } + + config.light = doc["light"].as(); + config.lightPin = static_cast(doc["lightPin"].as()); + + config.dht22 = doc["dht22"].as(); + config.dht22Pin = static_cast(doc["dht22Pin"].as()); + config.dht22Interval = doc["dht22Interval"].as(); + + return true; +} diff --git a/garden-controller-config-test/test/README b/garden-controller-config-test/test/README new file mode 100644 index 00000000..9b1e87bc --- /dev/null +++ b/garden-controller-config-test/test/README @@ -0,0 +1,11 @@ + +This directory is intended for PlatformIO Test Runner and project tests. + +Unit Testing is a software testing method by which individual units of +source code, sets of one or more MCU program modules together with associated +control data, usage procedures, and operating procedures, are tested to +determine whether they are fit for use. Unit testing finds problems early +in the development cycle. + +More information about PlatformIO Unit Testing: +- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html diff --git a/garden-controller-config-test/test/test_garden_config.cpp b/garden-controller-config-test/test/test_garden_config.cpp new file mode 100644 index 00000000..bf55eb98 --- /dev/null +++ b/garden-controller-config-test/test/test_garden_config.cpp @@ -0,0 +1,79 @@ +#include +#include +#include +#include "garden_config.h" + +void setUp(void) {} + +void tearDown(void) {} + +void test_serializeConfig(void) { + Config inputConfig = { + 1883, + "mqtt.example.com", + "topic_prefix", + 4, // numZones + { GPIO_NUM_4, GPIO_NUM_5, GPIO_NUM_6, GPIO_NUM_7 }, // zonePins + { GPIO_NUM_12, GPIO_NUM_13, GPIO_NUM_14, GPIO_NUM_15 }, // pumpPins + true, // light + GPIO_NUM_2, // lightPin + true, // dht22 + GPIO_NUM_21, // dht22Pin + 60 // dht22Interval + }; + + String outputJSON; + serializeConfig(inputConfig, outputJSON); + + TEST_ASSERT_EQUAL_STRING("{\"mqttPort\":1883,\"mqttServer\":\"mqtt.example.com\",\"mqttTopicPrefix\":\"topic_prefix\",\"numZones\":4,\"zonePins\":[4,5,6,7],\"pumpPins\":[12,13,14,15],\"light\":true,\"lightPin\":2,\"dht22\":true,\"dht22Pin\":21,\"dht22Interval\":60}", outputJSON.c_str()); +} + +void test_deserializeConfig(void) { + String inputJSON = "{\"mqttPort\":1883,\"mqttServer\":\"mqtt.example.com\",\"mqttTopicPrefix\":\"topic_prefix\",\"numZones\":4,\"zonePins\":[4,5,6,7],\"pumpPins\":[12,13,14,15],\"light\":true,\"lightPin\":2,\"dht22\":true,\"dht22Pin\":21,\"dht22Interval\":60}"; + Config outputConfig; + + bool result = deserializeConfig(inputJSON, outputConfig); + + TEST_ASSERT_TRUE(result); + + Config expectedConfig = { + 1883, + "mqtt.example.com", + "topic_prefix", + 4, // numZones + { GPIO_NUM_4, GPIO_NUM_5, GPIO_NUM_6, GPIO_NUM_7 }, // zonePins + { GPIO_NUM_12, GPIO_NUM_13, GPIO_NUM_14, GPIO_NUM_15 }, // pumpPins + true, // light + GPIO_NUM_2, // lightPin + true, // dht22 + GPIO_NUM_21, // dht22Pin + 60 // dht22Interval + }; + + TEST_ASSERT_EQUAL(expectedConfig.mqttPort, outputConfig.mqttPort); + TEST_ASSERT_EQUAL_STRING(expectedConfig.mqttServer, outputConfig.mqttServer); + TEST_ASSERT_EQUAL_STRING(expectedConfig.mqttTopicPrefix, outputConfig.mqttTopicPrefix); + TEST_ASSERT_EQUAL(expectedConfig.numZones, outputConfig.numZones); + + for (int i = 0; i < expectedConfig.numZones; i++) { + TEST_ASSERT_EQUAL(expectedConfig.zonePins[i], outputConfig.zonePins[i]); + TEST_ASSERT_EQUAL(expectedConfig.pumpPins[i], outputConfig.pumpPins[i]); + } + + TEST_ASSERT_EQUAL(expectedConfig.light, outputConfig.light); + TEST_ASSERT_EQUAL(expectedConfig.lightPin, outputConfig.lightPin); + TEST_ASSERT_EQUAL(expectedConfig.dht22, outputConfig.dht22); + TEST_ASSERT_EQUAL(expectedConfig.dht22Pin, outputConfig.dht22Pin); + TEST_ASSERT_EQUAL(expectedConfig.dht22Interval, outputConfig.dht22Interval); +} + +void setup() { + Serial.begin(115200); + + UNITY_BEGIN(); + RUN_TEST(test_serializeConfig); + RUN_TEST(test_deserializeConfig); + UNITY_END(); +} + +void loop() {} From f3b80acc5b87072607fe0c98bae9a767c77c393d Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sat, 7 Sep 2024 20:39:16 -0700 Subject: [PATCH 4/9] Add FS storage to test project --- .../include/garden_config.h | 6 +++ .../src/garden_config.cpp | 50 +++++++++++++++++++ .../test/test_garden_config.cpp | 41 +++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/garden-controller-config-test/include/garden_config.h b/garden-controller-config-test/include/garden_config.h index 0f1b4757..17e29a9a 100644 --- a/garden-controller-config-test/include/garden_config.h +++ b/garden-controller-config-test/include/garden_config.h @@ -4,6 +4,8 @@ #include #include +#define FORMAT_LITTLEFS_IF_FAILED true + struct Config { int mqttPort; const char* mqttServer; @@ -23,5 +25,9 @@ struct Config { void serializeConfig(const Config& config, String& jsonString); bool deserializeConfig(const String& jsonString, Config& config); +void initFS(); +bool configFileExists(); +void saveConfigToFile(const Config& config); +void loadConfigFromFile(Config& config); #endif diff --git a/garden-controller-config-test/src/garden_config.cpp b/garden-controller-config-test/src/garden_config.cpp index cdde40ea..2dd118f5 100644 --- a/garden-controller-config-test/src/garden_config.cpp +++ b/garden-controller-config-test/src/garden_config.cpp @@ -1,4 +1,5 @@ #include "garden_config.h" +#include // Write Config to JSON void serializeConfig(const Config& config, String& jsonString) { @@ -54,3 +55,52 @@ bool deserializeConfig(const String& jsonString, Config& config) { return true; } + +void initFS() { + if (!LittleFS.begin(false)) { + printf("failed to mount FS\n"); + } +} + +bool configFileExists() { + return LittleFS.exists("/config.json"); +} + +void loadConfigFromFile(Config& config) { + File configFile = LittleFS.open("/config.json", "r"); + if (!configFile) { + return; + } + printf("opened config file\n"); + + size_t size = configFile.size(); + + // Allocate a buffer to store contents of the file. + std::unique_ptr buf(new char[size]); + + configFile.readBytes(buf.get(), size); + configFile.close(); + + if (deserializeConfig(buf.get(), config)) { + printf("failed to load json config\n"); + return; + } +} + +void saveConfigToFile(const Config& config) { + String configJSON; + serializeConfig(config, configJSON); + + File configFile = LittleFS.open("/config.json", "w"); + if (!configFile) { + printf("failed to open config file for writing\n"); + } + + if (configFile.print(configJSON)) { + printf("File written successfully\n"); + } else { + printf("Write failed\n"); + } + + configFile.close(); +} diff --git a/garden-controller-config-test/test/test_garden_config.cpp b/garden-controller-config-test/test/test_garden_config.cpp index bf55eb98..89d979ff 100644 --- a/garden-controller-config-test/test/test_garden_config.cpp +++ b/garden-controller-config-test/test/test_garden_config.cpp @@ -7,6 +7,46 @@ void setUp(void) {} void tearDown(void) {} +void test_loadAndSaveConfig() { + Config inputConfig = { + 1883, + "mqtt.example.com", + "topic_prefix", + 4, // numZones + { GPIO_NUM_4, GPIO_NUM_5, GPIO_NUM_6, GPIO_NUM_7 }, // zonePins + { GPIO_NUM_12, GPIO_NUM_13, GPIO_NUM_14, GPIO_NUM_15 }, // pumpPins + true, // light + GPIO_NUM_2, // lightPin + true, // dht22 + GPIO_NUM_21, // dht22Pin + 60 // dht22Interval + }; + + initFS(); + saveConfigToFile(inputConfig); + + TEST_ASSERT_TRUE(configFileExists()); + + Config outputConfig; + loadConfigFromFile(outputConfig); + + TEST_ASSERT_EQUAL(inputConfig.mqttPort, outputConfig.mqttPort); + TEST_ASSERT_EQUAL_STRING(inputConfig.mqttServer, outputConfig.mqttServer); + TEST_ASSERT_EQUAL_STRING(inputConfig.mqttTopicPrefix, outputConfig.mqttTopicPrefix); + TEST_ASSERT_EQUAL(inputConfig.numZones, outputConfig.numZones); + + for (int i = 0; i < inputConfig.numZones; i++) { + TEST_ASSERT_EQUAL(inputConfig.zonePins[i], outputConfig.zonePins[i]); + TEST_ASSERT_EQUAL(inputConfig.pumpPins[i], outputConfig.pumpPins[i]); + } + + TEST_ASSERT_EQUAL(inputConfig.light, outputConfig.light); + TEST_ASSERT_EQUAL(inputConfig.lightPin, outputConfig.lightPin); + TEST_ASSERT_EQUAL(inputConfig.dht22, outputConfig.dht22); + TEST_ASSERT_EQUAL(inputConfig.dht22Pin, outputConfig.dht22Pin); + TEST_ASSERT_EQUAL(inputConfig.dht22Interval, outputConfig.dht22Interval); +} + void test_serializeConfig(void) { Config inputConfig = { 1883, @@ -71,6 +111,7 @@ void setup() { Serial.begin(115200); UNITY_BEGIN(); + RUN_TEST(test_loadAndSaveConfig); RUN_TEST(test_serializeConfig); RUN_TEST(test_deserializeConfig); UNITY_END(); From ffa91b589f429c9fa9face8a95e9678b56ed96e1 Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sat, 7 Sep 2024 22:28:03 -0700 Subject: [PATCH 5/9] Enable WifiManager when SSID is configured - This allows OTA update when a user opts to install firmware with configured SSID and password --- garden-controller/include/mqtt.h | 4 -- garden-controller/include/wifi_manager.h | 1 + garden-controller/src/main.cpp | 1 - garden-controller/src/mqtt.cpp | 25 -------- garden-controller/src/wifi_manager.cpp | 73 ++++++++++++++++++------ 5 files changed, 55 insertions(+), 49 deletions(-) diff --git a/garden-controller/include/mqtt.h b/garden-controller/include/mqtt.h index efad4e6b..b8607462 100644 --- a/garden-controller/include/mqtt.h +++ b/garden-controller/include/mqtt.h @@ -1,12 +1,9 @@ #ifndef mqtt_h #define mqtt_h -#include #include #include -// Configure network name and password in this file -#include "wifi_config.h" #include "config.h" /** @@ -50,7 +47,6 @@ void healthPublisherTask(void* parameters); void mqttConnectTask(void* parameters); void mqttLoopTask(void* parameters); void processIncomingMessage(char* topic, byte* message, unsigned int length); -void wifiDisconnectHandler(WiFiEvent_t event, WiFiEventInfo_t info); /* FreeRTOS Queue and Task handlers */ extern TaskHandle_t mqttConnectTaskHandle; diff --git a/garden-controller/include/wifi_manager.h b/garden-controller/include/wifi_manager.h index 96fd76c9..852625b9 100644 --- a/garden-controller/include/wifi_manager.h +++ b/garden-controller/include/wifi_manager.h @@ -1,6 +1,7 @@ #ifndef wifi_manager_h #define wifi_manager_h +#include #include #include #include diff --git a/garden-controller/src/main.cpp b/garden-controller/src/main.cpp index 4507f20d..03255135 100644 --- a/garden-controller/src/main.cpp +++ b/garden-controller/src/main.cpp @@ -150,7 +150,6 @@ void setup() { } setupWifiManager(); - setupWifi(); setupMQTT(); if (dht22Enabled) { diff --git a/garden-controller/src/mqtt.cpp b/garden-controller/src/mqtt.cpp index b7cb947b..a19be118 100644 --- a/garden-controller/src/mqtt.cpp +++ b/garden-controller/src/mqtt.cpp @@ -59,27 +59,6 @@ void setupMQTT() { } } -void setupWifi() { - char hostname[50]; - snprintf(hostname, sizeof(hostname), "%s-controller", mqtt_topic_prefix); - WiFi.setHostname(hostname); - - #if defined(SSID) && defined(PASSWORD) - printf(strcat("Connecting to " SSID " as ", mqtt_topic_prefix, "-controller\n")); - WiFi.begin(SSID, PASSWORD); - - while (WiFi.status() != WL_CONNECTED) { - delay(500); - printf("."); - } - - printf("Wifi connected...\n"); - #endif - - // Create event handler tp recpnnect to WiFi - WiFi.onEvent(wifiDisconnectHandler, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); -} - /* waterPublisherTask reads from a queue to publish WaterEvents as an InfluxDB line protocol message to MQTT @@ -225,7 +204,3 @@ void processIncomingMessage(char* topic, byte* message, unsigned int length) { changeLight(le); } } - -void wifiDisconnectHandler(WiFiEvent_t event, WiFiEventInfo_t info) { - ESP.restart(); -} diff --git a/garden-controller/src/wifi_manager.cpp b/garden-controller/src/wifi_manager.cpp index c6c33a52..e7949aa9 100644 --- a/garden-controller/src/wifi_manager.cpp +++ b/garden-controller/src/wifi_manager.cpp @@ -1,19 +1,20 @@ #include "wifi_manager.h" #include "config.h" +#include "wifi_config.h" char* mqtt_server = new char(); char* mqtt_topic_prefix = new char(); int mqtt_port; -WiFiManagerParameter custom_mqtt_server("server", "mqtt server", "", 40); -WiFiManagerParameter custom_mqtt_topic_prefix("topic_prefix", "mqtt topic prefix", "", 40); -WiFiManagerParameter custom_mqtt_port("port", "mqtt port", "8080", 6); +WiFiManagerParameter custom_mqtt_server("server", "mqtt server", "192.168.0.x", 40); +WiFiManagerParameter custom_mqtt_topic_prefix("topic_prefix", "mqtt topic prefix", "garden", 40); +WiFiManagerParameter custom_mqtt_port("port", "mqtt port", "1883", 6); WiFiManager wifiManager; TaskHandle_t wifiManagerLoopTaskHandle; -void saveConfig() { +void saveParamsToConfig() { // read updated parameters strcpy(mqtt_server, custom_mqtt_server.getValue()); strcpy(mqtt_topic_prefix, custom_mqtt_topic_prefix.getValue()); @@ -36,18 +37,12 @@ void saveConfig() { void setupFS() { printf("setting up filesystem\n"); - // start with defaults - strcpy(mqtt_server, MQTT_ADDRESS); - strcpy(mqtt_topic_prefix, TOPIC_PREFIX); - mqtt_port = MQTT_PORT; - if (!LittleFS.begin(FORMAT_LITTLEFS_IF_FAILED)) { printf("failed to mount FS\n"); return; } printf("successfully mounted FS\n"); - if (!LittleFS.exists("/config.json")) { printf("config doesn't exist\n"); return; @@ -93,29 +88,69 @@ void wifiManagerLoopTask(void* parameters) { vTaskDelete(NULL); } +void wifiDisconnectHandler(WiFiEvent_t event, WiFiEventInfo_t info) { + ESP.restart(); +} + +#if defined SSID && defined PASSWORD +/* connect directly to WiFi and run web portal in the background */ +void connectWifiDirect() { + printf("Connecting to %s as %s\n", SSID, mqtt_topic_prefix); + WiFi.begin(SSID, PASSWORD); + + while (WiFi.status() != WL_CONNECTED) { + delay(500); + printf("."); + } + + printf("Wifi connected...\n"); + + wifiManager.setEnableConfigPortal(false); + wifiManager.setConfigPortalBlocking(false); + wifiManager.autoConnect(); +} +#endif + +void runWifiManagerPortal() { + bool connected = wifiManager.autoConnect("GardenControllerSetup", "password"); + if (!connected) { + printf("failed to connect and hit timeout\n"); + delay(3000); + ESP.restart(); + delay(5000); + } +} + void setupWifiManager() { - wifiManager.setSaveConfigCallback(saveConfig); - wifiManager.setSaveParamsCallback(saveConfig); + wifiManager.setSaveConfigCallback(saveParamsToConfig); + wifiManager.setSaveParamsCallback(saveParamsToConfig); wifiManager.addParameter(&custom_mqtt_server); wifiManager.addParameter(&custom_mqtt_topic_prefix); wifiManager.addParameter(&custom_mqtt_port); + char hostname[50]; + snprintf(hostname, sizeof(hostname), "%s-controller", mqtt_topic_prefix); + wifiManager.setHostname(hostname); + // wifiManager.resetSettings(); setupFS(); - if (!wifiManager.autoConnect("GardenControllerSetup", "password")) { - printf("failed to connect and hit timeout\n"); - delay(3000); - // reset and try again, or maybe put it to deep sleep - ESP.restart(); - delay(5000); - } + // If SSID/PASSWORD are configured, connect regularly and use WifiManager for setup portal only + #if defined SSID && defined PASSWORD + connectWifiDirect(); + #else + // Otherwise, use WifiManager autoconnect portal + runWifiManagerPortal(); + #endif wifiManager.setParamsPage(true); wifiManager.setConfigPortalBlocking(false); wifiManager.startWebPortal(); xTaskCreate(wifiManagerLoopTask, "WifiManagerLoopTask", 4096, NULL, 1, &wifiManagerLoopTaskHandle); + + // Create event handler tp reconnect to WiFi + WiFi.onEvent(wifiDisconnectHandler, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); } From 348a66a6de115c311897cef50451d394bf727608 Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sun, 8 Sep 2024 14:42:06 -0700 Subject: [PATCH 6/9] Add mDNS to controller --- garden-app/controller/generate_config.go | 4 ---- garden-app/controller/generate_config_test.go | 16 ---------------- garden-app/controller/interactive.go | 9 --------- garden-controller/include/wifi_manager.h | 1 + garden-controller/src/wifi_manager.cpp | 13 ++++++++++--- 5 files changed, 11 insertions(+), 32 deletions(-) diff --git a/garden-app/controller/generate_config.go b/garden-app/controller/generate_config.go index 785c17ae..edd0fe49 100644 --- a/garden-app/controller/generate_config.go +++ b/garden-app/controller/generate_config.go @@ -20,13 +20,9 @@ const ( #define QUEUE_SIZE 10 -#define ENABLE_WIFI -#ifdef ENABLE_WIFI #define MQTT_ADDRESS "{{ .MQTTConfig.Broker }}" #define MQTT_PORT {{ .MQTTConfig.Port }} -#endif - #define NUM_ZONES {{ len .Zones }} #define VALVES { {{ range $index, $z := .Zones }}{{if $index}}, {{end}}{{ $z.ValvePin }}{{ end }} } #define PUMPS { {{ range $index, $z := .Zones }}{{if $index}}, {{end}}{{ $z.PumpPin }}{{ end }} } diff --git a/garden-app/controller/generate_config_test.go b/garden-app/controller/generate_config_test.go index 958c02d3..409e60b8 100644 --- a/garden-app/controller/generate_config_test.go +++ b/garden-app/controller/generate_config_test.go @@ -45,13 +45,9 @@ func TestGenerateMainConfig(t *testing.T) { #define QUEUE_SIZE 10 -#define ENABLE_WIFI -#ifdef ENABLE_WIFI #define MQTT_ADDRESS "localhost" #define MQTT_PORT 1883 -#endif - #define NUM_ZONES 1 #define VALVES { GPIO_NUM_16 } #define PUMPS { GPIO_NUM_18 } @@ -95,13 +91,9 @@ func TestGenerateMainConfig(t *testing.T) { #define QUEUE_SIZE 10 -#define ENABLE_WIFI -#ifdef ENABLE_WIFI #define MQTT_ADDRESS "localhost" #define MQTT_PORT 1883 -#endif - #define NUM_ZONES 1 #define VALVES { GPIO_NUM_16 } #define PUMPS { GPIO_NUM_18 } @@ -139,13 +131,9 @@ func TestGenerateMainConfig(t *testing.T) { #define QUEUE_SIZE 10 -#define ENABLE_WIFI -#ifdef ENABLE_WIFI #define MQTT_ADDRESS "localhost" #define MQTT_PORT 1883 -#endif - #define NUM_ZONES 1 #define VALVES { GPIO_NUM_16 } #define PUMPS { GPIO_NUM_18 } @@ -195,13 +183,9 @@ func TestGenerateMainConfig(t *testing.T) { #define QUEUE_SIZE 10 -#define ENABLE_WIFI -#ifdef ENABLE_WIFI #define MQTT_ADDRESS "localhost" #define MQTT_PORT 1883 -#endif - #define NUM_ZONES 4 #define VALVES { GPIO_NUM_16, GPIO_NUM_16, GPIO_NUM_16, GPIO_NUM_16 } #define PUMPS { GPIO_NUM_18, GPIO_NUM_18, GPIO_NUM_18, GPIO_NUM_18 } diff --git a/garden-app/controller/interactive.go b/garden-app/controller/interactive.go index 1dd0a8fc..92a5607a 100644 --- a/garden-app/controller/interactive.go +++ b/garden-app/controller/interactive.go @@ -64,15 +64,6 @@ func mqttPrompts(config *Config) error { }, Validate: survey.Required, }, - { - Name: "publish_health", - Prompt: &survey.Input{ - Message: "Enable health publishing?", - Default: fmt.Sprintf("%t", config.PublishHealth), - Help: "control whether or not healh publishing is enabled. Enable it unless you have a good reason not to", - }, - Validate: survey.Required, - }, } err := survey.Ask(qs, config) if err != nil { diff --git a/garden-controller/include/wifi_manager.h b/garden-controller/include/wifi_manager.h index 852625b9..b86fa315 100644 --- a/garden-controller/include/wifi_manager.h +++ b/garden-controller/include/wifi_manager.h @@ -5,6 +5,7 @@ #include #include #include +#include #define FORMAT_LITTLEFS_IF_FAILED true diff --git a/garden-controller/src/wifi_manager.cpp b/garden-controller/src/wifi_manager.cpp index e7949aa9..de228270 100644 --- a/garden-controller/src/wifi_manager.cpp +++ b/garden-controller/src/wifi_manager.cpp @@ -105,6 +105,10 @@ void connectWifiDirect() { printf("Wifi connected...\n"); + strcpy(mqtt_server, MQTT_ADDRESS); + strcpy(mqtt_topic_prefix, TOPIC_PREFIX); + mqtt_port = MQTT_PORT; + wifiManager.setEnableConfigPortal(false); wifiManager.setConfigPortalBlocking(false); wifiManager.autoConnect(); @@ -129,9 +133,7 @@ void setupWifiManager() { wifiManager.addParameter(&custom_mqtt_topic_prefix); wifiManager.addParameter(&custom_mqtt_port); - char hostname[50]; - snprintf(hostname, sizeof(hostname), "%s-controller", mqtt_topic_prefix); - wifiManager.setHostname(hostname); + wifiManager.setHostname(mqtt_topic_prefix); // wifiManager.resetSettings(); @@ -151,6 +153,11 @@ void setupWifiManager() { xTaskCreate(wifiManagerLoopTask, "WifiManagerLoopTask", 4096, NULL, 1, &wifiManagerLoopTaskHandle); + if (!MDNS.begin(mqtt_topic_prefix)) { + printf("error starting mDNS\n"); + return; + } + // Create event handler tp reconnect to WiFi WiFi.onEvent(wifiDisconnectHandler, WiFiEvent_t::ARDUINO_EVENT_WIFI_STA_DISCONNECTED); } From 73b239c48c4cbe85bccdbceecf8267626b4d1a6a Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sun, 8 Sep 2024 19:39:48 -0700 Subject: [PATCH 7/9] Wait for MQTT connect before publishing startup --- garden-controller/src/main.cpp | 10 ---------- garden-controller/src/mqtt.cpp | 12 ++++++++++-- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/garden-controller/src/main.cpp b/garden-controller/src/main.cpp index 03255135..2bb79458 100644 --- a/garden-controller/src/main.cpp +++ b/garden-controller/src/main.cpp @@ -156,22 +156,12 @@ void setup() { setupDHT22(); } - // Initialize Queues waterQueue = xQueueCreate(QUEUE_SIZE, sizeof(WaterEvent)); if (waterQueue == NULL) { printf("error creating the waterQueue\n"); } - // Start all tasks (currently using equal priorities) xTaskCreate(waterZoneTask, "WaterZoneTask", 2048, NULL, 1, &waterZoneTaskHandle); - - // Delay 1 second to allow MQTT to connect - delay(1000); - if (client.connected()) { - client.publish(MQTT_LOGGING_TOPIC, "logs message=\"garden-controller setup complete\""); - } else { - printf("unable to publish: not connected to MQTT broker\n"); - } } void loop() {} diff --git a/garden-controller/src/mqtt.cpp b/garden-controller/src/mqtt.cpp index a19be118..dbb3b44c 100644 --- a/garden-controller/src/mqtt.cpp +++ b/garden-controller/src/mqtt.cpp @@ -13,13 +13,17 @@ QueueHandle_t waterPublisherQueue; QueueHandle_t lightPublisherQueue; TaskHandle_t lightPublisherTaskHandle; +// command topics (subscribe) char waterCommandTopic[50]; char stopCommandTopic[50]; char stopAllCommandTopic[50]; -char waterDataTopic[50]; char lightCommandTopic[50]; + +// data topics (publish) +char waterDataTopic[50]; char lightDataTopic[50]; char healthDataTopic[50]; +char logDataTopic[50]; #define ZERO (unsigned long int) 0 @@ -33,10 +37,12 @@ void setupMQTT() { snprintf(waterCommandTopic, sizeof(waterCommandTopic), "%s" MQTT_WATER_TOPIC, mqtt_topic_prefix); snprintf(stopCommandTopic, sizeof(stopCommandTopic), "%s" MQTT_STOP_TOPIC, mqtt_topic_prefix); snprintf(stopAllCommandTopic, sizeof(stopAllCommandTopic), "%s" MQTT_STOP_ALL_TOPIC, mqtt_topic_prefix); - snprintf(waterDataTopic, sizeof(waterDataTopic), "%s" MQTT_WATER_DATA_TOPIC, mqtt_topic_prefix); snprintf(lightCommandTopic, sizeof(lightCommandTopic), "%s" MQTT_LIGHT_TOPIC, mqtt_topic_prefix); + + snprintf(waterDataTopic, sizeof(waterDataTopic), "%s" MQTT_WATER_DATA_TOPIC, mqtt_topic_prefix); snprintf(lightDataTopic, sizeof(lightDataTopic), "%s" MQTT_LIGHT_DATA_TOPIC, mqtt_topic_prefix); snprintf(healthDataTopic, sizeof(healthDataTopic), "%s" MQTT_HEALTH_DATA_TOPIC, mqtt_topic_prefix); + snprintf(logDataTopic, sizeof(logDataTopic), "%s" MQTT_LOGGING_TOPIC, mqtt_topic_prefix); // Initialize publisher Queue waterPublisherQueue = xQueueCreate(QUEUE_SIZE, sizeof(WaterEvent)); @@ -140,6 +146,8 @@ void mqttConnectTask(void* parameters) { if (lightEnabled) { client.subscribe(lightCommandTopic, 1); } + + client.publish(logDataTopic, "logs message=\"garden-controller setup complete\""); } else { printf("failed, rc=%zu\n", client.state()); } From b543e7deaedd4336ea8205f054200a20d9f32eb7 Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sun, 8 Sep 2024 19:46:14 -0700 Subject: [PATCH 8/9] Remove unnecessary externs --- garden-controller/include/dht22.h | 2 -- garden-controller/include/main.h | 2 -- garden-controller/include/mqtt.h | 29 +++--------------------- garden-controller/include/wifi_manager.h | 13 +---------- 4 files changed, 4 insertions(+), 42 deletions(-) diff --git a/garden-controller/include/dht22.h b/garden-controller/include/dht22.h index 5cec2a46..d8687b7e 100644 --- a/garden-controller/include/dht22.h +++ b/garden-controller/include/dht22.h @@ -1,8 +1,6 @@ #ifndef dht22_h #define dht22_h -extern TaskHandle_t dht22TaskHandle; - void setupDHT22(); void dht22PublishTask(void* parameters); diff --git a/garden-controller/include/main.h b/garden-controller/include/main.h index 54389739..9f3632b7 100644 --- a/garden-controller/include/main.h +++ b/garden-controller/include/main.h @@ -19,8 +19,6 @@ void stopWatering(); void stopAllWatering(); void changeLight(LightEvent le); -extern gpio_num_t zones[NUM_ZONES]; -extern gpio_num_t pumps[NUM_ZONES]; extern bool lightEnabled; #endif diff --git a/garden-controller/include/mqtt.h b/garden-controller/include/mqtt.h index b8607462..62c21142 100644 --- a/garden-controller/include/mqtt.h +++ b/garden-controller/include/mqtt.h @@ -6,37 +6,20 @@ #include "config.h" -/** - * MQTT_CLIENT_NAME - * Name to use when connecting to MQTT broker. By default this is TOPIC_PREFIX - * MQTT_WATER_TOPIC - * Topic to subscribe to for incoming commands to water a zone - * MQTT_STOP_TOPIC - * Topic to subscribe to for incoming command to stop watering a zone - * MQTT_STOP_ALL_TOPIC - * Topic to subscribe to for incoming command to stop watering a zone and clear the watering queue - * MQTT_LIGHT_TOPIC - * Topic to subscribe to for incoming command to change the state of an attached grow light - * MQTT_LIGHT_DATA_TOPIC - * Topic to publish LightEvents on - * MQTT_WATER_DATA_TOPIC - * Topic to publish watering metrics on - */ #define MQTT_WATER_TOPIC "/command/water" #define MQTT_STOP_TOPIC "/command/stop" #define MQTT_STOP_ALL_TOPIC "/command/stop_all" #define MQTT_LIGHT_TOPIC "/command/light" + #define MQTT_LIGHT_DATA_TOPIC "/data/light" #define MQTT_WATER_DATA_TOPIC "/data/water" - #define MQTT_LOGGING_TOPIC "/data/logs" - #define MQTT_HEALTH_DATA_TOPIC "/data/health" -#define HEALTH_PUBLISH_INTERVAL 60000 - #define MQTT_TEMPERATURE_DATA_TOPIC "/data/temperature" #define MQTT_HUMIDITY_DATA_TOPIC "/data/humidity" +#define HEALTH_PUBLISH_INTERVAL 60000 + extern PubSubClient client; void setupMQTT(); @@ -48,13 +31,7 @@ void mqttConnectTask(void* parameters); void mqttLoopTask(void* parameters); void processIncomingMessage(char* topic, byte* message, unsigned int length); -/* FreeRTOS Queue and Task handlers */ -extern TaskHandle_t mqttConnectTaskHandle; -extern TaskHandle_t mqttLoopTaskHandle; -extern TaskHandle_t healthPublisherTaskHandle; -extern TaskHandle_t waterPublisherTaskHandle; extern QueueHandle_t waterPublisherQueue; extern QueueHandle_t lightPublisherQueue; -extern TaskHandle_t lightPublisherTaskHandle; #endif diff --git a/garden-controller/include/wifi_manager.h b/garden-controller/include/wifi_manager.h index b86fa315..4ba68b4a 100644 --- a/garden-controller/include/wifi_manager.h +++ b/garden-controller/include/wifi_manager.h @@ -9,23 +9,12 @@ #define FORMAT_LITTLEFS_IF_FAILED true -extern WiFiManagerParameter custom_mqtt_server; -extern WiFiManagerParameter custom_mqtt_topic_prefix; -extern WiFiManagerParameter custom_mqtt_port; - extern WiFiManager wifiManager; -// TODO: use these variables for MQTT setup -// It looks like it will be difficult to refactor everything to use the new MQTT configuration. -// My options are to ditch that feature for now since I am just using WifiManager for Wifi + OTA -// and don't need to immediately connect to MQTT yet since I am not doing OTA or configs over MQTT extern char* mqtt_server; extern char* mqtt_topic_prefix; extern int mqtt_port; void setupWifiManager(); -void mqttLoopTask(void* parameters); - -extern TaskHandle_t wifiManagerLoopTaskHandle; -#endif + #endif From 02a8b1edd78492f0099693d9997e20880a0b643b Mon Sep 17 00:00:00 2001 From: Calvin McLean Date: Sun, 8 Sep 2024 19:59:16 -0700 Subject: [PATCH 9/9] Fix linting --- garden-app/worker/water_schedule_actions.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/garden-app/worker/water_schedule_actions.go b/garden-app/worker/water_schedule_actions.go index 0b4bbf42..84835228 100644 --- a/garden-app/worker/water_schedule_actions.go +++ b/garden-app/worker/water_schedule_actions.go @@ -21,7 +21,7 @@ func (w *Worker) ExecuteScheduledWaterAction(g *pkg.Garden, z *pkg.Zone, ws *pkg w.logger.Info("skipping watering Zone because of SkipCount", "zone_id", z.GetID()) return nil } - duration, err := w.exerciseWeatherControl(g, z, ws) + duration, err := w.exerciseWeatherControl(ws) if err != nil { w.logger.Error("error executing weather controls, continuing to water", "error", err) duration = ws.Duration.Duration @@ -40,7 +40,7 @@ func (w *Worker) ExecuteScheduledWaterAction(g *pkg.Garden, z *pkg.Zone, ws *pkg }) } -func (w *Worker) exerciseWeatherControl(g *pkg.Garden, z *pkg.Zone, ws *pkg.WaterSchedule) (time.Duration, error) { +func (w *Worker) exerciseWeatherControl(ws *pkg.WaterSchedule) (time.Duration, error) { if !ws.HasWeatherControl() { return ws.Duration.Duration, nil }