I wanted a way to monitor the water level (well, the presence of water) in my dog’s water bowl. I had an ESP8266 lying around, so I decided to use that to send a message to AWS IoT whenever the water level dips below a certain point.
I have the device sending a JSON payload to AWS IoT via MQTT, which then routes the message (extracting the liquid detected status) to Cloudwatch Metrics, which I then display (and alert on using Pushover) in Grafana via the Cloudwatch datasource.
Obviously using AWS IoT is overkill, and you could route these messages straight to Home Assistant (with the Mosquitto integration) for example, or any other message broker, but I wanted to use this opportunity to play around with AWS IoT.
Ingredients Link to heading
AWS IoT setup Link to heading
- Create a new thing in the AWS IoT console
- Download the root CA certificate, device certificate, and device private key
- Create a new policy and attach it to the thing
The policy needs the following permissions:
- iot:Connect
- iot:Publish
- Set up the message routing rule to route messages to Cloudwatch Metrics
Wiring Link to heading
The datasheet for the water level sensor can be found here. The sensor has 4 pins: VCC, GND, OUT, and output level (not used in this case).
Ignoring the colours of my jumper wires, if you look at the colour of the wires on the sensor under the heat shrink, connect the brown wire (wire 4) to 5V out on the ESP8266, the blue wire (wire 2) to ground, and the yellow wire (wire 3) to D4 (GPIO2).
Coding the ESP8266 Link to heading
Using the Arudino IDE, add the following board manager URL:
https://arduino.esp8266.com/stable/package_esp8266com_index.json
Install the ESP8266 board manager, and select the correct board (“Generic ESP8266 Module”) from the Tools menu.
The code below reads the sensor pin (D4) and sends a message to AWS IoT whenever the sensor state changes. The code also sends a message every 60 seconds to ensure the connection is maintained.
#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <time.h>
#include <FS.h>
// Pin definitions
const int SENSOR_PIN = 2; // GPIO2 (D4)
const int LED_PIN = LED_BUILTIN; // Built-in LED
// Wifi credentials
const char* ssid = "MuhInnernet";
const char* password = "hunter2";
// AWS IoT config
const char* AWS_ENDPOINT = "aaahahahaha-ats.iot.ap-southeast-2.amazonaws.com";
const int AWS_PORT = 8883;
const char* AWS_TOPIC = "water/sensor/doggo-1";
// Time config
const char* NTP_SERVER = "pool.ntp.org";
const long GMT_OFFSET_SEC = 0;
const int DAYLIGHT_OFFSET_SEC = 0;
// Timing config
int lastState = 2;
unsigned long lastChangeTime = 0;
const int DEBOUNCE_DELAY = 500; // 500ms sensor response time
unsigned long lastPublishTime = 0;
const int PUBLISH_INTERVAL = 60000; // Publish every 60 seconds
const int LED_FLASH_DURATION = 100;
unsigned long ledOffTime = 0;
// AWS root CA certificate
static const char AWS_CERT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
)EOF";
// Device certificate
static const char AWS_CERT_CRT[] PROGMEM = R"KEY(
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
)KEY";
// Device private key
static const char AWS_CERT_PRIVATE[] PROGMEM = R"KEY(
-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----
)KEY";
// Initialize wifi and MQTT clients
WiFiClientSecure espClient;
PubSubClient client(espClient);
void flashLED() {
digitalWrite(LED_PIN, LOW); // LED on (LOW for ESP8266 built-in LED)
ledOffTime = millis() + LED_FLASH_DURATION;
}
void checkLED() {
if (ledOffTime > 0 && millis() >= ledOffTime) {
digitalWrite(LED_PIN, HIGH); // LED off
ledOffTime = 0;
}
}
void setup_wifi() {
WiFi.mode(WIFI_STA);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
}
void connect_aws() {
// Configure certificates
BearSSL::X509List cert(AWS_CERT_CA);
BearSSL::X509List client_crt(AWS_CERT_CRT);
BearSSL::PrivateKey key(AWS_CERT_PRIVATE);
espClient.setTrustAnchors(&cert);
espClient.setClientRSACert(&client_crt, &key);
// Set time via NTP
configTime(GMT_OFFSET_SEC, DAYLIGHT_OFFSET_SEC, NTP_SERVER);
time_t now = time(nullptr);
while (now < 24 * 3600) {
delay(100);
now = time(nullptr);
}
// Connect to AWS IoT
client.setServer(AWS_ENDPOINT, AWS_PORT);
while (!client.connected()) {
String clientId = "ESP8266-" + String(random(0xffff), HEX);
if (!client.connect(clientId.c_str())) {
delay(5000); // Wait 5 seconds before retrying
}
}
}
void publishSensorStatus(bool liquidDetected) {
if (!client.connected()) {
connect_aws();
}
// Create JSON payload
char payload[256];
time_t now = time(nullptr);
snprintf(payload, sizeof(payload),
"{"
"\"device_id\":\"Dog-1\","
"\"timestamp\":%ld,"
"\"liquid_detected\":%d"
"}",
(long)now,
liquidDetected ? 1 : 0);
// Publish to AWS IoT
client.publish(AWS_TOPIC, payload);
// Flash LED to indicate message sent
flashLED();
}
void setup() {
Serial.begin(115200);
// Configure pins
pinMode(SENSOR_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, HIGH); // LED off initially
setup_wifi();
connect_aws();
}
void loop() {
// Ensure MQTT connection is maintained
if (!client.connected()) {
connect_aws();
}
client.loop();
// Read sensor (accounting for NPN logic)
int liquidDetected = !digitalRead(SENSOR_PIN) ? 1 : 0;
// Check for state change with debounce
if (liquidDetected != lastState &&
(millis() - lastChangeTime) > DEBOUNCE_DELAY) {
lastState = liquidDetected;
lastChangeTime = millis();
publishSensorStatus(liquidDetected);
}
// Periodic status update even if no change
if (millis() - lastPublishTime > PUBLISH_INTERVAL) {
publishSensorStatus(lastState);
lastPublishTime = millis();
}
// Check if LED needs to be turned off
checkLED();
delay(10);
}
Grafana Link to heading
I set up a Cloudwatch datasource in Grafana, and created a “Stat” graph to display the liquid detected status.
The panel JSON is:
{
"id": 1,
"type": "stat",
"title": "Dog Water 1",
"fieldConfig": {
"defaults": {
"mappings": [
{
"options": {
"0": {
"color": "red",
"index": 1,
"text": "Empty"
},
"1": {
"color": "green",
"index": 0,
"text": "Full"
}
},
"type": "value"
}
],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green",
"value": null
},
{
"color": "red",
"value": 80
}
]
},
"color": {
"mode": "thresholds"
}
},
"overrides": []
},
"pluginVersion": "11.4.0",
"targets": [
{
"datasource": {
"type": "cloudwatch",
"uid": "pa9ximnasveo0c"
},
"dimensions": {},
"expression": "",
"id": "",
"label": "",
"logGroups": [],
"matchExact": true,
"metricEditorMode": 0,
"metricName": "dog-1",
"metricQueryType": 0,
"namespace": "WaterLevel",
"period": "",
"queryLanguage": "CWLI",
"queryMode": "Metrics",
"refId": "A",
"region": "default",
"sqlExpression": "",
"statistic": "Minimum"
}
],
"datasource": {
"type": "cloudwatch",
"uid": "pa9ximnasveo0c"
},
"options": {
"reduceOptions": {
"values": false,
"calcs": [
"lastNotNull"
],
"fields": ""
},
"orientation": "auto",
"textMode": "auto",
"wideLayout": true,
"colorMode": "value",
"graphMode": "none",
"justifyMode": "auto",
"showPercentChange": false,
"percentChangeColorMode": "standard"
}
}
I use the “Minimum” statistic because I only want to know if the value is 0 or 1, and the “Minimum” statistic will return 0 (empty) if any of the values in the time period are 0. Otherwise it will return 1 (full).
It seems to work pretty well, it would be nice to have a water level sensor, but the dog bowls I have don’t really suit that (these ones from Kmart), so this is the next best thing, and I get alerts basically as soon as the water level drops to a point where I should be refilling them.