washing-machine-notify/esp32_MachineNotify.ino
2026-02-12 23:33:58 +01:00

476 lines
14 KiB
C++

#include <Wire.h>
#include "secrets.h"
// Feature Configuration - Comment out to disable
#define ENABLE_SENSORS
#define ENABLE_DISPLAY
#define ENABLE_WIFI
#define ENABLE_NOTIFICATIONS
#ifdef ENABLE_WIFI
#include <WiFi.h>
#include <HTTPClient.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <freertos/queue.h>
#endif
#ifdef ENABLE_SENSORS
#include <BH1750.h>
#endif
#ifdef ENABLE_DISPLAY
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#endif
// WiFi credentials
#ifdef ENABLE_WIFI
const char* WIFI_SSID = SECRET_WIFI_SSID;
const char* WIFI_PASSWORD = SECRET_WIFI_PASSWORD;
#endif
// ntfy.sh topic
const char* NTFY_TOPIC = SECRET_NTFY_TOPIC;
// Device Names
const char* DEVICE1_NAME = "Dryer"; // Monitored by Vibration
const char* DEVICE2_NAME = "Washer"; // Monitored by Light
// Pin definitions
const int VIBRATION_PIN = 4;
// OLED display settings
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 32
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C
#ifdef ENABLE_DISPLAY
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
#endif
// Light sensor
#ifdef ENABLE_SENSORS
BH1750 lightMeter;
#endif
// Thresholds
const float LIGHT_ACTIVATION_THRESHOLD = 100.0;
const float LIGHT_DEACTIVATION_THRESHOLD = 20.0;
const unsigned long VIBRATION_STATE_CHANGE_INTERVAL = 5000; // 5 seconds
const unsigned long VIBRATION_ACTIVE_THRESHOLD = 10000;
const int VIBRATION_WINDOW_SIZE = 4; // X * 5s seconds window
const unsigned long WARMUP_DURATION = 20000; // 20 seconds warmup
// Device state
bool isVibrationActive = false;
bool isLightActive = false;
// Time tracking for sensor states
unsigned long lastVibrationCheckTime = 0;
unsigned long lightHighStartTime = 0;
unsigned long lightLowStartTime = 0;
// OLED display data
#ifdef ENABLE_DISPLAY
String displayData[5];
int currentDisplayLine = 0;
unsigned long lastDisplayScrollTime = 0;
const unsigned long DISPLAY_SCROLL_INTERVAL = 2000; // 1000 = 1 second
const int displayTextLines = 4;
int displayDataLines = displayTextLines;
#endif
// Network Task & Queue
struct NotificationMessage {
char message[64];
char title[32];
char priority[10];
};
QueueHandle_t notificationQueue;
volatile bool isNetworkActive = false;
#ifdef ENABLE_SENSORS
unsigned long vibrationHistory[VIBRATION_WINDOW_SIZE];
int vibrationHistoryIdx = 0;
unsigned long vibrationWindowTotal = 0;
unsigned long vibrationTotalLow = 0;
volatile unsigned long lastChangeTime = 0;
volatile unsigned long lowTimeAccumulator = 0;
volatile unsigned long highTimeAccumulator = 0;
void IRAM_ATTR onChange() {
unsigned long now = micros();
unsigned long duration = now - lastChangeTime;
lastChangeTime = now;
if (digitalRead(VIBRATION_PIN) == HIGH) {
// Changed to HIGH, so previous state was LOW
lowTimeAccumulator += duration;
} else {
// Changed to LOW, so previous state was HIGH
highTimeAccumulator += duration;
}
}
#endif
#ifdef ENABLE_WIFI
void networkTask(void *parameter) {
NotificationMessage msg;
unsigned long lastWifiCheckTime = 0;
const unsigned long WIFI_CHECK_INTERVAL = 30000;
while (true) {
unsigned long currentTime = millis();
if (currentTime - lastWifiCheckTime >= WIFI_CHECK_INTERVAL) {
lastWifiCheckTime = currentTime;
if (WiFi.status() != WL_CONNECTED) {
Serial.println("WiFi disconnected. Attempting to reconnect...");
WiFi.reconnect();
}
}
if (xQueueReceive(notificationQueue, &msg, 1000 / portTICK_PERIOD_MS) == pdTRUE) {
isNetworkActive = true;
bool sent = false;
while (!sent) {
if (WiFi.status() == WL_CONNECTED) {
HTTPClient http;
String url = "http://ntfy.sh/" + String(NTFY_TOPIC);
http.begin(url);
http.addHeader("Content-Type", "text/plain");
http.addHeader("Title", msg.title);
http.addHeader("Priority", msg.priority);
int httpResponseCode = http.POST(msg.message);
http.end();
if (httpResponseCode > 0) {
Serial.print("HTTP Response code: ");
Serial.println(httpResponseCode);
sent = true;
} else {
Serial.print("Error on sending POST: ");
Serial.println(httpResponseCode);
vTaskDelay(2000 / portTICK_PERIOD_MS);
}
} else {
unsigned long now = millis();
if (now - lastWifiCheckTime >= WIFI_CHECK_INTERVAL) {
lastWifiCheckTime = now;
Serial.println("WiFi disconnected. Attempting to reconnect...");
WiFi.reconnect();
}
vTaskDelay(1000 / portTICK_PERIOD_MS);
}
}
isNetworkActive = false;
}
}
}
#endif
void setup() {
delay(500);
Serial.begin(9600);
delay(100);
Serial.println("Setup started.");
#if defined(ENABLE_SENSORS) || defined(ENABLE_DISPLAY)
Wire.begin();
#endif
#ifdef ENABLE_SENSORS
// Initialize vibration sensor pin
pinMode(VIBRATION_PIN, INPUT_PULLUP);
lastChangeTime = micros();
attachInterrupt(digitalPinToInterrupt(VIBRATION_PIN), onChange, CHANGE);
// Initialize history buffer
for(int i=0; i<VIBRATION_WINDOW_SIZE; i++) vibrationHistory[i] = 0;
// Initialize light sensor
if (!lightMeter.begin(BH1750::CONTINUOUS_HIGH_RES_MODE)) {
Serial.println(F("Could not find a valid BH1750 sensor, check wiring!"));
while (1) { }
}
#endif
#ifdef ENABLE_DISPLAY
// Initialize OLED display
if (!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for (;;);
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
display.setCursor(0, 0);
display.println(F("Initializing..."));
display.display();
#endif
#ifdef ENABLE_WIFI
// Connect to WiFi
Serial.print("Connecting to ");
Serial.println(WIFI_SSID);
WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("");
Serial.println("WiFi connected");
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
#ifdef ENABLE_NOTIFICATIONS
notificationQueue = xQueueCreate(10, sizeof(NotificationMessage));
xTaskCreatePinnedToCore(networkTask, "NetworkTask", 8192, NULL, 1, NULL, 0);
#endif
#endif
#if defined(ENABLE_DISPLAY) && defined(ENABLE_WIFI)
display.clearDisplay();
display.setCursor(0,0);
display.println("WiFi Connected");
display.display();
delay(1000);
#endif
Serial.println("Setup finished.");
}
void updateDisplay();
void loop() {
unsigned long currentTime = millis();
// Sensor readings
#ifdef ENABLE_SENSORS
float currentLightLevel = lightMeter.readLightLevel();
#else
float currentLightLevel = 0.0;
#endif
bool previousVibrationState = isVibrationActive;
bool previousLightState = isLightActive;
// Vibration state change logic
#ifdef ENABLE_SENSORS
if (currentTime - lastVibrationCheckTime >= VIBRATION_STATE_CHANGE_INTERVAL) {
lastVibrationCheckTime = currentTime;
noInterrupts();
unsigned long now = micros();
unsigned long duration = now - lastChangeTime;
lastChangeTime = now;
if (digitalRead(VIBRATION_PIN) == LOW) {
lowTimeAccumulator += duration;
} else {
highTimeAccumulator += duration;
}
vibrationTotalLow = lowTimeAccumulator;
lowTimeAccumulator = 0;
highTimeAccumulator = 0;
interrupts();
vibrationWindowTotal -= vibrationHistory[vibrationHistoryIdx];
vibrationHistory[vibrationHistoryIdx] = vibrationTotalLow;
vibrationWindowTotal += vibrationTotalLow;
vibrationHistoryIdx = (vibrationHistoryIdx + 1) % VIBRATION_WINDOW_SIZE;
if (vibrationWindowTotal > VIBRATION_ACTIVE_THRESHOLD && currentTime > WARMUP_DURATION) {
isVibrationActive = true;
} else {
isVibrationActive = false;
}
}
#endif
// Light state change logic
if (currentLightLevel >= LIGHT_ACTIVATION_THRESHOLD && !isLightActive) {
if (lightHighStartTime == 0) {
lightHighStartTime = currentTime;
}
if (currentTime - lightHighStartTime >= VIBRATION_STATE_CHANGE_INTERVAL && currentTime > WARMUP_DURATION) {
isLightActive = true;
}
lightLowStartTime = 0;
} else if (currentLightLevel < LIGHT_DEACTIVATION_THRESHOLD && isLightActive) {
if (lightLowStartTime == 0) {
lightLowStartTime = currentTime;
}
if (currentTime - lightLowStartTime >= VIBRATION_STATE_CHANGE_INTERVAL) {
isLightActive = false;
}
lightHighStartTime = 0;
} else if(currentLightLevel >= LIGHT_ACTIVATION_THRESHOLD) {
lightLowStartTime = 0;
} else {
lightHighStartTime = 0;
}
// Device 1 (Vibration) Notifications
if (isVibrationActive != previousVibrationState) {
String message;
String title;
String priority;
if (isVibrationActive) {
message = String(DEVICE1_NAME) + " started.";
title = String(DEVICE1_NAME) + " START";
priority = "default";
} else {
message = String(DEVICE1_NAME) + " finished.";
title = String(DEVICE1_NAME) + " END";
priority = "high";
}
#ifdef ENABLE_NOTIFICATIONS
#ifdef ENABLE_WIFI
NotificationMessage msg;
strncpy(msg.message, message.c_str(), sizeof(msg.message) - 1);
msg.message[sizeof(msg.message) - 1] = 0;
strncpy(msg.title, title.c_str(), sizeof(msg.title) - 1);
msg.title[sizeof(msg.title) - 1] = 0;
strncpy(msg.priority, priority.c_str(), sizeof(msg.priority) - 1);
msg.priority[sizeof(msg.priority) - 1] = 0;
xQueueSend(notificationQueue, &msg, 0);
#else
Serial.println("WiFi Disabled. Notification skipped: " + message);
#endif
#else
Serial.println("Notification (Disabled): " + title + " - " + message);
#endif
}
// Device 2 (Light) Notifications
if (isLightActive != previousLightState) {
String message;
String title;
String priority;
if (isLightActive) {
message = String(DEVICE2_NAME) + " active.";
title = String(DEVICE2_NAME) + " ACTIVE";
priority = "default";
} else {
message = String(DEVICE2_NAME) + " inactive.";
title = String(DEVICE2_NAME) + " INACTIVE";
priority = "high";
}
#ifdef ENABLE_NOTIFICATIONS
#ifdef ENABLE_WIFI
NotificationMessage msg;
strncpy(msg.message, message.c_str(), sizeof(msg.message) - 1);
msg.message[sizeof(msg.message) - 1] = 0;
strncpy(msg.title, title.c_str(), sizeof(msg.title) - 1);
msg.title[sizeof(msg.title) - 1] = 0;
strncpy(msg.priority, priority.c_str(), sizeof(msg.priority) - 1);
msg.priority[sizeof(msg.priority) - 1] = 0;
xQueueSend(notificationQueue, &msg, 0);
#else
Serial.println("WiFi Disabled. Notification skipped: " + message);
#endif
#else
Serial.println("Notification (Disabled): " + title + " - " + message);
#endif
}
#ifdef ENABLE_DISPLAY
// Update display data
displayData[0] = String(DEVICE1_NAME) + ": " + String(vibrationWindowTotal);
displayData[1] = "Vibra: " + String(isVibrationActive ? "ON" : "OFF");
displayData[2] = String(DEVICE2_NAME) + ": " + String(currentLightLevel, 0);
displayData[3] = "Light: " + String(isLightActive ? "ON" : "OFF");
displayDataLines = 4;
// Update display
updateDisplay();
#endif
}
void updateDisplay() {
#ifdef ENABLE_DISPLAY
unsigned long currentTime = millis();
// Scroll logic
if ((displayDataLines > displayTextLines) && (currentTime - lastDisplayScrollTime > DISPLAY_SCROLL_INTERVAL)) {
lastDisplayScrollTime = currentTime;
currentDisplayLine = (currentDisplayLine + 1) % displayDataLines;
}
display.clearDisplay();
display.setTextSize(1);
display.setTextColor(WHITE);
// Draw active status indicator
int rectWidth = 10;
int rectHeight = (SCREEN_HEIGHT / 2) - 2;
int rectX = 2;
int rectY1 = 1;
int rectY2 = (SCREEN_HEIGHT / 2) + 1;
int textX = rectX + rectWidth + 4;
for (int i = 0; i < displayDataLines; i++) {
int lineIndex = (currentDisplayLine + i) % displayDataLines;
display.setCursor(textX, i * 8);
display.println(displayData[lineIndex]);
}
// Draw Device 1 Indicator (Top)
display.drawRect(rectX, rectY1, rectWidth, rectHeight, WHITE);
if (isVibrationActive) {
int innerWidth = rectWidth - 4;
int step = (millis() / 150) % (innerWidth + 1);
display.fillRect(rectX + 2, rectY1 + 2, step, rectHeight - 4, WHITE);
}
// Draw Device 2 Indicator (Bottom)
display.drawRect(rectX, rectY2, rectWidth, rectHeight, WHITE);
if (isLightActive) {
int innerWidth = rectWidth - 4;
int step = (millis() / 150) % (innerWidth + 1);
display.fillRect(rectX + 2, rectY2 + 2, step, rectHeight - 4, WHITE);
}
// Network Status Indicator
#ifdef ENABLE_WIFI
int r = 8;
int cx = SCREEN_WIDTH - r - 2;
int cy = SCREEN_HEIGHT / 2;
if (WiFi.status() != WL_CONNECTED) {
display.drawLine(cx - r, cy - r, cx + r, cy + r, WHITE);
display.drawLine(cx - r, cy + r, cx + r, cy - r, WHITE);
} else if (isNetworkActive) {
display.fillCircle(cx, cy, r, WHITE);
} else {
display.drawCircle(cx, cy, r, WHITE);
}
#ifdef ENABLE_NOTIFICATIONS
if (notificationQueue != NULL) {
int waiting = (int)uxQueueMessagesWaiting(notificationQueue);
int startY = cy + r + 2;
int h = SCREEN_HEIGHT - startY;
for (int i = 0; i < waiting; i++) {
int x = (cx - r) + (i * 2);
if (x < SCREEN_WIDTH && h > 0) display.drawFastVLine(x, startY, h, WHITE);
}
}
#endif
#endif
// Warmup Indicator
if (currentTime < WARMUP_DURATION) {
display.drawRect(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, WHITE);
}
display.display();
#endif
}