Welcome to Hackster!
Hackster is a community dedicated to learning hardware, from beginner to pro. Join us, it's free!
Brian Wente
Published © LGPL

8 Glasses a Day

A simple, DIY IoT water tracker that helps you stay on top of your hydration goals.

IntermediateFull instructions provided3 hours181
8 Glasses a Day

Things used in this project

Hardware components

Wemos D1 Mini
Espressif Wemos D1 Mini
×1
WS2812 Addressable LED Strip
Digilent WS2812 Addressable LED Strip
get the kind you can cut, a strip of eight
×1
Switch Actuator, APEM A01 series Illuminated Push-Button Switches
Switch Actuator, APEM A01 series Illuminated Push-Button Switches
close enough
×1
Connector Accessory, Hex Nut
Connector Accessory, Hex Nut
3mm hex screw and nut
×4

Software apps and online services

MQTT
MQTT
optional

Hand tools and fabrication machines

Laser cutter (generic)
Laser cutter (generic)
Soldering iron (generic)
Soldering iron (generic)

Story

Read more

Custom parts and enclosures

SVG Laser cut files

Code

Water Tracker

Arduino
#include <ESP8266WiFi.h>
#include <WiFiManager.h>
#include <PubSubClient.h>
#include <FastLED.h>
#include <EEPROM.h>
#include <WiFiUdp.h>
#include <NTPClient.h>
#include <Bounce2.h>

// LED and button definitions
#define LED_PIN     5
#define NUM_LEDS    8
#define BUTTON_PIN  D2

// Long press threshold (milliseconds)
#define LONG_PRESS_THRESHOLD 2000

// EEPROM settings
#define EEPROM_SIZE         512
#define ADDR_GLASS_COUNT    0       // 4 bytes (int)
#define ADDR_LAST_RESET     4       // 4 bytes (unsigned long)
#define ADDR_MQTT_SERVER    8       // 40 bytes reserved (addresses 8 to 47)
#define MQTT_SERVER_LENGTH  40
#define ADDR_MQTT_USER      48      // 20 bytes reserved (addresses 48 to 67)
#define MQTT_USER_LENGTH    20
#define ADDR_MQTT_PASS      68      // 20 bytes reserved (addresses 68 to 87)
#define MQTT_PASS_LENGTH    20
#define ADDR_TZ_OFFSET      88      // 4 bytes (long) for timezone offset

CRGB leds[NUM_LEDS];
int glassCount = 0;
unsigned long lastResetTime = 0;

// Default MQTT configuration (if EEPROM is uninitialized, these are defaults)
char mqtt_server[MQTT_SERVER_LENGTH] = "192.168.1.100";
char mqtt_user[MQTT_USER_LENGTH]     = "yourUser";
char mqtt_pass[MQTT_PASS_LENGTH]     = "yourPassword";

// Timezone offset in seconds (default to Eastern Standard Time: -18000)
long tzOffset = -18000;

// WiFi and MQTT objects
WiFiClient espClient;
PubSubClient client(espClient);
WiFiManager wifiManager;

// MQTT reconnect interval
unsigned long lastMqttAttempt = 0;
const unsigned long MQTT_RETRY_INTERVAL = 5000;

// Daily reset using NTPClient
unsigned long lastResetDay = 0;
WiFiUDP ntpUDP;
long utcOffsetInSeconds = 0; // This will be set from tzOffset
NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds, 60000);

// --- Button Debounce using Bounce2 ---
Bounce debouncer = Bounce();
unsigned long buttonPressTime = 0;

// Forward declarations
void callback(char* topic, byte* payload, unsigned int length);
void tryReconnect();
void publishDiscovery();
void publishStatus();
void publishHelp();
void incrementCount();
void resetCount();
void displayCount();
void displayClear();
void playReminderAnimation();
void loadSettings();
void saveSettings();

void setup() {
  Serial.begin(115200);
  EEPROM.begin(EEPROM_SIZE);
  loadSettings();  // Loads glassCount, lastResetTime, mqtt config, and tzOffset

  // --- WiFiManager Setup ---
  WiFiManagerParameter custom_mqtt("server", "MQTT Server", mqtt_server, MQTT_SERVER_LENGTH);
  WiFiManagerParameter custom_user("user", "MQTT Username", mqtt_user, MQTT_USER_LENGTH);
  WiFiManagerParameter custom_pass("pass", "MQTT Password", mqtt_pass, MQTT_PASS_LENGTH);
  
  // Prepare a string for the timezone offset parameter from tzOffset
  char tzOffsetString[8];
  snprintf(tzOffsetString, sizeof(tzOffsetString), "%ld", tzOffset);
  WiFiManagerParameter custom_tz("tz", "Timezone Offset (seconds)", tzOffsetString, 8);
  
  wifiManager.addParameter(&custom_mqtt);
  wifiManager.addParameter(&custom_user);
  wifiManager.addParameter(&custom_pass);
  wifiManager.addParameter(&custom_tz);
  
  if (!wifiManager.autoConnect("WaterTrackerAP")) {
    Serial.println("Failed to connect, restarting...");
    ESP.restart();
    delay(1000);
  }
  
  // Update and save MQTT settings
  strcpy(mqtt_server, custom_mqtt.getValue());
  strcpy(mqtt_user, custom_user.getValue());
  strcpy(mqtt_pass, custom_pass.getValue());
  
  // Update timezone offset from the AP portal parameter
  tzOffset = atol(custom_tz.getValue());
  Serial.print("Timezone offset set to: ");
  Serial.println(tzOffset);
  
  saveSettings();
  
  Serial.println("Connected to WiFi");
  Serial.print("MQTT Server: ");
  Serial.println(mqtt_server);
  Serial.print("MQTT Username: ");
  Serial.println(mqtt_user);
  
  // --- MQTT Setup ---
  client.setServer(mqtt_server, 1883);
  client.setCallback(callback);
  
  // --- NTPClient Setup for Daily Reset ---
  utcOffsetInSeconds = tzOffset;  // Use the tzOffset value as the offset
  timeClient.setTimeOffset(utcOffsetInSeconds);
  timeClient.begin();
  timeClient.update();
  lastResetDay = timeClient.getEpochTime() / 86400UL;
  
  // --- Hardware Setup ---
  pinMode(BUTTON_PIN, INPUT_PULLUP);
  debouncer.attach(BUTTON_PIN);
  debouncer.interval(25);
  
  FastLED.addLeds<WS2812, LED_PIN, GRB>(leds, NUM_LEDS);
  FastLED.setBrightness(128);  // Set brightness to a visible level
  
  // Startup animation: light each LED in blue sequentially
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = CRGB::Blue;
    FastLED.show();
    delay(100);
  }
  delay(1000);
  displayClear();
  displayCount();
}

void loop() {
  debouncer.update();
  
  // Update NTP time and check for daily reset at midnight
  timeClient.update();
  unsigned long currentDay = timeClient.getEpochTime() / 86400UL;
  int hours = timeClient.getHours();
  int minutes = timeClient.getMinutes();
  int seconds = timeClient.getSeconds();
  
  if (currentDay != lastResetDay && hours == 0 && minutes == 0 && seconds < 10) {
    resetCount();
    lastResetDay = currentDay;
    Serial.println("Daily reset executed at midnight");
  }
  
  // Attempt MQTT reconnect periodically (non-blocking)
  if (!client.connected() && millis() - lastMqttAttempt > MQTT_RETRY_INTERVAL) {
    lastMqttAttempt = millis();
    tryReconnect();
  }
  client.loop();
  
  // --- Button Handling using Bounce2 ---
  if (debouncer.fell()) {
    // Button pressed: record press start time
    buttonPressTime = millis();
  }
  
  if (debouncer.rose()) {
    // Button released: measure duration
    unsigned long pressDuration = millis() - buttonPressTime;
    if (pressDuration < LONG_PRESS_THRESHOLD) {
      // Short press: increment the count
      incrementCount();
    } else {
      // Long press: reset the count
      resetCount();
    }
    delay(50); // Simple debounce delay after release
  }
}

// --- MQTT Callback and Communication Functions ---

void callback(char* topic, byte* payload, unsigned int length) {
  String message;
  for (unsigned int i = 0; i < length; i++) {
    message += (char)payload[i];
  }
  Serial.print("Received message on ");
  Serial.print(topic);
  Serial.print(": ");
  Serial.println(message);
  
  if (String(topic) == "watertracker/command") {
    if (message.equalsIgnoreCase("increment")) {
      incrementCount();
    } else if (message.equalsIgnoreCase("reset")) {
      resetCount();
    } else if (message.equalsIgnoreCase("getStatus")) {
      publishStatus();
    } else if (message.startsWith("setCount:")) {
      int newCount = message.substring(String("setCount:").length()).toInt();
      glassCount = newCount;
      displayCount();
      publishStatus();
      saveSettings();
    } else if (message.equalsIgnoreCase("help")) {
      publishHelp();
    } else if (message.equalsIgnoreCase("reminder")) {
      playReminderAnimation();
    }
  }
}

void tryReconnect() {
  String clientId = "WaterTracker-";
  clientId += String(random(0xffff), HEX);
  if (client.connect(clientId.c_str(), mqtt_user, mqtt_pass)) {
    Serial.println("MQTT connected");
    client.subscribe("watertracker/command");
    publishStatus();
    publishDiscovery();
  } else {
    Serial.print("MQTT connection failed, rc=");
    Serial.println(client.state());
  }
}

void publishDiscovery() {
  String glassCountConfig = "{\"name\": \"Water Tracker Glass Count\","
                            "\"state_topic\": \"watertracker/status\","
                            "\"unit_of_measurement\": \"glasses\","
                            "\"value_template\": \"{{ value_json.glassCount }}\","
                            "\"unique_id\": \"watertracker_glass_count\"}";
  client.publish("homeassistant/sensor/watertracker_glass_count/config", glassCountConfig.c_str(), true);
}

void publishStatus() {
  String payload = "{\"glassCount\": " + String(glassCount) + "}";
  if (client.connected()) {
    client.publish("watertracker/status", payload.c_str());
  } else {
    Serial.println("MQTT not connected; skipping publishStatus()");
  }
}

void publishHelp() {
  String payload = "Commands: increment, reset, getStatus, setCount:<value>, help, reminder";
  if (client.connected()) {
    client.publish("watertracker/status", payload.c_str());
  }
}

// --- Count and LED Update Functions ---

void incrementCount() {
  if (glassCount < NUM_LEDS) {
    glassCount++;
    displayCount();
    publishStatus();
    saveSettings();
  }
}

void resetCount() {
  glassCount = 0;
  lastResetTime = millis();
  displayClear();
  displayCount();
  publishStatus();
  saveSettings();
}

void displayCount() {
  FastLED.clear(true);
  for (int i = 0; i < glassCount; i++) {
    leds[i] = CRGB::Blue;
  }
  FastLED.show();
}

void displayClear() {
  FastLED.clear(true);
}

void playReminderAnimation() {
  Serial.println("Playing reminder animation");
  for (int i = 0; i < NUM_LEDS; i++) {
    leds[i] = CRGB::Orange;  // Changed color per your update
    FastLED.show();
    delay(100);
  }
  delay(1000);
  displayClear();
  displayCount();
}

// --- EEPROM Persistence Functions ---

void loadSettings() {
  EEPROM.get(ADDR_GLASS_COUNT, glassCount);
  EEPROM.get(ADDR_LAST_RESET, lastResetTime);
  EEPROM.get(ADDR_MQTT_SERVER, mqtt_server);
  EEPROM.get(ADDR_MQTT_USER, mqtt_user);
  EEPROM.get(ADDR_MQTT_PASS, mqtt_pass);
  EEPROM.get(ADDR_TZ_OFFSET, tzOffset);
  
  Serial.print("Loaded glassCount: ");
  Serial.println(glassCount);
  Serial.print("Loaded lastResetTime: ");
  Serial.println(lastResetTime);
  Serial.print("Loaded MQTT Server: ");
  Serial.println(mqtt_server);
  Serial.print("Loaded MQTT Username: ");
  Serial.println(mqtt_user);
  Serial.print("Loaded Timezone Offset: ");
  Serial.println(tzOffset);
}

void saveSettings() {
  EEPROM.put(ADDR_GLASS_COUNT, glassCount);
  EEPROM.put(ADDR_LAST_RESET, lastResetTime);
  EEPROM.put(ADDR_MQTT_SERVER, mqtt_server);
  EEPROM.put(ADDR_MQTT_USER, mqtt_user);
  EEPROM.put(ADDR_MQTT_PASS, mqtt_pass);
  EEPROM.put(ADDR_TZ_OFFSET, tzOffset);
  EEPROM.commit();
  
  Serial.print("Saved glassCount: ");
  Serial.println(glassCount);
  Serial.print("Saved lastResetTime: ");
  Serial.println(lastResetTime);
  Serial.print("Saved MQTT Server: ");
  Serial.println(mqtt_server);
  Serial.print("Saved MQTT Username: ");
  Serial.println(mqtt_user);
  Serial.print("Saved Timezone Offset: ");
  Serial.println(tzOffset);
}

Credits

Brian Wente
6 projects • 8 followers
Contact

Comments

Please log in or sign up to comment.