Skip to content

Commit 65e8e79

Browse files
authored
Merge pull request #2963 from jedgarpark/espresso-water-meter
first commit water meter code
2 parents b20191b + 15aa60f commit 65e8e79

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed

Espresso_Water_Meter/code.py

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
# SPDX-FileCopyrightText: 2025 John Park for Adafruit Industries
2+
#
3+
# SPDX-License-Identifier: MIT
4+
'''
5+
Espresso Tank Meter
6+
Feather ESP32-S2 with RCWL-1601 Ultrasonic distance sensor
7+
'''
8+
9+
import time
10+
import os
11+
import ssl
12+
import microcontroller
13+
import supervisor
14+
import socketpool
15+
import wifi
16+
import board
17+
import alarm
18+
import neopixel
19+
import adafruit_hcsr04
20+
import adafruit_minimqtt.adafruit_minimqtt as MQTT
21+
from adafruit_io.adafruit_io import IO_MQTT
22+
import adafruit_requests
23+
import adafruit_max1704x
24+
25+
# Initialize the sonar sensor
26+
sonar = adafruit_hcsr04.HCSR04(trigger_pin=board.A0, echo_pin=board.A1)
27+
28+
# Initialize the battery monitor
29+
i2c = board.I2C() # uses board.SCL and board.SDA
30+
battery_monitor = adafruit_max1704x.MAX17048(i2c)
31+
32+
# Define colors (hex values)
33+
WHITE = 0xFFFFFF
34+
BLUE = 0x0000FF
35+
GREEN = 0x00FF00
36+
YELLOW = 0xFFFF00
37+
RED = 0xFF0000
38+
PINK = 0xbb00bb
39+
CYAN = 0x00bbbb
40+
OFF = 0x000000
41+
42+
# Initialize the NeoPixel
43+
pixel = neopixel.NeoPixel(board.NEOPIXEL, 1, brightness=0.25)
44+
# Show yellow on startup
45+
pixel.fill(YELLOW)
46+
47+
# Operating hours (24-hour format with minutes, e.g., "6:35" and "16:00")
48+
OPENING_TIME = "6:00"
49+
CLOSING_TIME = "22:30"
50+
# Normal operation check interval
51+
NORMAL_CHECK_MINUTES = 5
52+
# Sleep duration in seconds during operating hours
53+
SLEEP_DURATION = 60 * NORMAL_CHECK_MINUTES
54+
# Display duration in seconds
55+
DISPLAY_DURATION = 1
56+
# Number of samples to average
57+
NUM_SAMPLES = 5
58+
59+
def parse_time(time_str):
60+
"""Convert time string (HH:MM format) to hours and minutes."""
61+
# pylint: disable=redefined-outer-name
62+
parts = time_str.split(':')
63+
return int(parts[0]), int(parts[1])
64+
65+
def get_average_distance():
66+
"""Take multiple distance readings and return the average."""
67+
distances = []
68+
for _ in range(NUM_SAMPLES):
69+
try:
70+
distance = sonar.distance
71+
distances.append(distance)
72+
time.sleep(0.1) # Short delay between readings
73+
except RuntimeError:
74+
print("Error reading distance")
75+
continue
76+
77+
# Only average valid readings
78+
if distances:
79+
return sum(distances) / len(distances)
80+
return None
81+
82+
def set_pixel_color(distance):
83+
"""Set NeoPixel color based on distance."""
84+
if distance is None:
85+
pixel.fill(OFF)
86+
return
87+
88+
if distance < 2:
89+
pixel.fill(WHITE)
90+
elif 2 <= distance < 10:
91+
pixel.fill(BLUE)
92+
elif 10 <= distance < 16:
93+
pixel.fill(GREEN)
94+
elif 18 <= distance < 20:
95+
pixel.fill(YELLOW)
96+
else: # distance >= 22
97+
pixel.fill(RED)
98+
99+
# Wait for things to settle before reading sonar
100+
time.sleep(0.1)
101+
102+
# Get average distance
103+
avg_distance = get_average_distance()
104+
105+
if avg_distance is not None:
106+
107+
if avg_distance >= 22:
108+
# pylint: disable=invalid-name
109+
avg_distance = 22
110+
print(f"Average distance: {avg_distance:.1f} cm")
111+
# Set color based on average distance
112+
set_pixel_color(avg_distance)
113+
114+
# Check battery status
115+
battery_voltage = battery_monitor.cell_voltage
116+
battery_percent = battery_monitor.cell_percent
117+
print(f"Battery: {battery_percent:.1f}% ({battery_voltage:.2f}V)")
118+
119+
# Try connecting to WiFi
120+
try:
121+
122+
print("Connecting to %s" % os.getenv("CIRCUITPY_WIFI_SSID"))
123+
# Show pink while attempting to connect
124+
pixel.fill(PINK)
125+
wifi.radio.connect(os.getenv("CIRCUITPY_WIFI_SSID"), os.getenv("CIRCUITPY_WIFI_PASSWORD"))
126+
print("Connected to %s" % os.getenv("CIRCUITPY_WIFI_SSID"))
127+
# Show cyan on successful connection
128+
pixel.fill(CYAN)
129+
time.sleep(1) # Brief pause to show the connection success
130+
# pylint: disable=broad-except
131+
except Exception as e:
132+
print("Failed to connect to WiFi. Error:", e, "\nBoard will hard reset in 30 seconds.")
133+
pixel.fill(OFF)
134+
time.sleep(10)
135+
microcontroller.reset()
136+
137+
# Create a socket pool
138+
pool = socketpool.SocketPool(wifi.radio)
139+
requests = adafruit_requests.Session(pool, ssl.create_default_context())
140+
141+
# Initialize a new MQTT Client object
142+
mqtt_client = MQTT.MQTT(
143+
broker="io.adafruit.com",
144+
username=os.getenv("ADAFRUIT_AIO_USERNAME"),
145+
password=os.getenv("ADAFRUIT_AIO_KEY"),
146+
socket_pool=pool,
147+
ssl_context=ssl.create_default_context(),
148+
)
149+
150+
# Initialize Adafruit IO MQTT "helper"
151+
io = IO_MQTT(mqtt_client)
152+
153+
try:
154+
# If Adafruit IO is not connected...
155+
if not io.is_connected:
156+
print("Connecting to Adafruit IO...")
157+
io.connect()
158+
159+
# Get current time from AIO time service
160+
aio_username = os.getenv("ADAFRUIT_AIO_USERNAME")
161+
aio_key = os.getenv("ADAFRUIT_AIO_KEY")
162+
timezone = os.getenv("TIMEZONE")
163+
# pylint: disable=line-too-long
164+
TIME_URL = f"https://io.adafruit.com/api/v2/{aio_username}/integrations/time/strftime?x-aio-key={aio_key}&tz={timezone}"
165+
TIME_URL += "&fmt=%25Y-%25m-%25d+%25H%3A%25M%3A%25S.%25L+%25j+%25u+%25z+%25Z"
166+
167+
print("Getting time from Adafruit IO...")
168+
response = requests.get(TIME_URL)
169+
time_str = response.text.strip() # Remove any leading/trailing whitespace
170+
print("Current time:", time_str)
171+
172+
# Parse the current time from the time string
173+
time_parts = time_str.split()
174+
current_time = time_parts[1].split(':')
175+
current_hour = int(current_time[0])
176+
current_minute = int(current_time[1])
177+
178+
# Get opening and closing hours and minutes
179+
opening_hour, opening_minute = parse_time(OPENING_TIME)
180+
closing_hour, closing_minute = parse_time(CLOSING_TIME)
181+
182+
# Convert all times to minutes for easier comparison
183+
current_minutes = current_hour * 60 + current_minute
184+
opening_minutes = opening_hour * 60 + opening_minute
185+
closing_minutes = closing_hour * 60 + closing_minute
186+
187+
# Check if we're within operating hours
188+
if opening_minutes <= current_minutes < closing_minutes:
189+
print(f"Within operating hours ({OPENING_TIME} to {CLOSING_TIME}), proceeding with measurement")
190+
191+
# Explicitly pump the message loop
192+
io.loop()
193+
194+
# Send the distance data
195+
print(f"Publishing {avg_distance:.1f} to espresso water level feed")
196+
io.publish("espresso-water-tank-level", f"{avg_distance:.1f}")
197+
198+
# Send the battery data
199+
print(f"Publishing {battery_percent:.1f} to battery level feed")
200+
io.publish("espresso-water-sensor-battery", f"{battery_percent:.1f}")
201+
202+
203+
# Make sure the message gets sent
204+
io.loop()
205+
206+
print("Water level sent successfully")
207+
208+
# Keep NeoPixel lit for DISPLAY_DURATION seconds
209+
time.sleep(DISPLAY_DURATION)
210+
211+
# Use normal check interval during operating hours
212+
# # pylint: disable=invalid-name
213+
sleep_seconds = SLEEP_DURATION
214+
print(f"Next check in {NORMAL_CHECK_MINUTES} minutes")
215+
else:
216+
print(f"Outside operating hours ({OPENING_TIME} to {CLOSING_TIME}), going back to sleep")
217+
# Calculate time until next opening
218+
if current_minutes >= closing_minutes:
219+
# After closing, calculate time until opening tomorrow
220+
minutes_until_open = (24 * 60 - current_minutes) + opening_minutes
221+
else:
222+
# Before opening, calculate time until opening today
223+
minutes_until_open = opening_minutes - current_minutes
224+
225+
# Convert minutes to seconds for sleep duration
226+
sleep_seconds = minutes_until_open * 60
227+
hours_until_open = minutes_until_open // 60
228+
minutes_remaining = minutes_until_open % 60
229+
if minutes_remaining:
230+
print(f"Sleeping until {OPENING_TIME} ({hours_until_open} hours, {minutes_remaining} minutes)")
231+
else:
232+
print(f"Sleeping until {OPENING_TIME} ({hours_until_open} hours)")
233+
234+
response.close()
235+
236+
# pylint: disable=broad-except
237+
except Exception as e:
238+
print("Failed to get or send data, or connect. Error:", e,
239+
"\nBoard will hard reset in 30 seconds.")
240+
pixel.fill(OFF)
241+
time.sleep(30)
242+
microcontroller.reset()
243+
244+
else:
245+
print("Failed to get valid distance readings")
246+
pixel.fill(OFF)
247+
# pylint: disable=invalid-name
248+
sleep_seconds = SLEEP_DURATION # Use normal interval if we couldn't get readings
249+
250+
# Prepare for deep sleep
251+
pixel.brightness = 0 # Turn off NeoPixel
252+
253+
# Flush the serial output before sleep
254+
# pylint: disable=pointless-statement
255+
supervisor.runtime.serial_bytes_available
256+
time.sleep(0.05)
257+
258+
# Create time alarm
259+
time_alarm = alarm.time.TimeAlarm(monotonic_time=time.monotonic() + sleep_seconds)
260+
261+
# Enter deep sleep
262+
alarm.exit_and_deep_sleep_until_alarms(time_alarm)

0 commit comments

Comments
 (0)