mirror of
https://github.com/pimoroni/grow-python
synced 2025-10-25 15:19:23 +00:00
Add watering settings to UI
This commit is contained in:
@@ -41,6 +41,7 @@ channel1:
|
||||
wet_point: 0.7
|
||||
dry_point: 27.6
|
||||
auto_water: True
|
||||
watering_delay: 60
|
||||
channel2:
|
||||
water_level: 0.8
|
||||
warn_level: 0.2
|
||||
@@ -49,6 +50,7 @@ channel2:
|
||||
wet_point: 0.7
|
||||
dry_point: 27.6
|
||||
auto_water: True
|
||||
watering_delay: 60
|
||||
channel3:
|
||||
water_level: 0.8
|
||||
warn_level: 0.2
|
||||
@@ -57,6 +59,7 @@ channel3:
|
||||
wet_point: 0.7
|
||||
dry_point: 27.6
|
||||
auto_water: True
|
||||
watering_delay: 60
|
||||
general:
|
||||
alarm_enable: True
|
||||
alarm_interval: 1.0
|
||||
@@ -73,6 +76,7 @@ Grow has three channels which are separated into the sections `channel1`, `chann
|
||||
* `auto_water` - Whether to run the attached pump (True to auto-water, False for manual watering)
|
||||
* `wet_point` - Value for the sensor in saturated soil (in Hz)
|
||||
* `dry_point` - Value for the sensor in totally dry soil (in Hz)
|
||||
* `watering_delay` - Delay between waterings (in seconds)
|
||||
|
||||
## General Settings
|
||||
|
||||
|
||||
@@ -1,383 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import logging
|
||||
import math
|
||||
import pathlib
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
|
||||
import RPi.GPIO as GPIO
|
||||
import ST7735
|
||||
from fonts.ttf import RobotoMedium as UserFont
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
import yaml
|
||||
from grow import Piezo
|
||||
from grow.moisture import Moisture
|
||||
from grow.pump import Pump
|
||||
|
||||
|
||||
class Channel:
|
||||
bar_colours = [
|
||||
(192, 225, 254), # Blue
|
||||
(196, 255, 209), # Green
|
||||
(255, 243, 192), # Yellow
|
||||
(254, 192, 192), # Red
|
||||
]
|
||||
|
||||
label_colours = [
|
||||
(32, 137, 251), # Blue
|
||||
(100, 255, 124), # Green
|
||||
(254, 219, 82), # Yellow
|
||||
(254, 82, 82), # Red
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
display_channel,
|
||||
sensor_channel,
|
||||
pump_channel,
|
||||
water_level=0.5,
|
||||
alarm_level=0.5,
|
||||
pump_speed=0.7,
|
||||
pump_time=0.7,
|
||||
watering_delay=30,
|
||||
wet_point=0.7,
|
||||
dry_point=26.7,
|
||||
icon=None,
|
||||
auto_water=False,
|
||||
enabled=False,
|
||||
):
|
||||
self.channel = display_channel
|
||||
self.sensor = Moisture(sensor_channel)
|
||||
self.pump = Pump(pump_channel)
|
||||
self.water_level = water_level
|
||||
self.alarm_level = alarm_level
|
||||
self.auto_water = auto_water
|
||||
self.pump_speed = pump_speed
|
||||
self.pump_time = pump_time
|
||||
self.watering_delay = watering_delay
|
||||
self.wet_point = wet_point
|
||||
self.dry_point = dry_point
|
||||
self.last_dose = time.time()
|
||||
self.icon = icon
|
||||
self.enabled = enabled
|
||||
self.alarm = False
|
||||
|
||||
self.sensor.set_wet_point(wet_point)
|
||||
self.sensor.set_dry_point(dry_point)
|
||||
|
||||
def indicator_color(self, value, r=None):
|
||||
if r is None:
|
||||
r = self.bar_colours
|
||||
if value == 1.0:
|
||||
return r[-1]
|
||||
if value == 0.0:
|
||||
return r[0]
|
||||
|
||||
value *= len(r) - 1
|
||||
a = int(math.floor(value))
|
||||
b = a + 1
|
||||
blend = float(value - a)
|
||||
|
||||
r, g, b = [int(((r[b][i] - r[a][i]) * blend) + r[a][i]) for i in range(3)]
|
||||
|
||||
return (r, g, b)
|
||||
|
||||
def update_from_yml(self, config):
|
||||
if config is not None:
|
||||
self.pump_speed = config.get("pump_speed", self.pump_speed)
|
||||
self.pump_time = config.get("pump_time", self.pump_time)
|
||||
self.alarm_level = config.get("alarm_level", self.alarm_level)
|
||||
self.water_level = config.get("water_level", self.water_level)
|
||||
self.watering_delay = config.get("watering_delay", self.watering_delay)
|
||||
self.auto_water = config.get("auto_water", self.auto_water)
|
||||
self.enabled = config.get("enabled", self.enabled)
|
||||
self.wet_point = config.get("wet_point", self.wet_point)
|
||||
self.dry_point = config.get("dry_point", self.dry_point)
|
||||
icon = config.get("icon", None)
|
||||
if icon is not None:
|
||||
self.icon = Image.open(icon)
|
||||
|
||||
pass
|
||||
|
||||
def __str__(self):
|
||||
return """Channel: {channel}
|
||||
Enabled: {enabled}
|
||||
Alarm level: {alarm_level}
|
||||
Auto water: {auto_water}
|
||||
Water level: {water_level}
|
||||
Pump speed: {pump_speed}
|
||||
Pump time: {pump_time}
|
||||
Delay: {watering_delay}
|
||||
Wet point: {wet_point}
|
||||
Dry point: {dry_point}
|
||||
""".format(
|
||||
**self.__dict__
|
||||
)
|
||||
|
||||
def water(self):
|
||||
if not self.auto_water:
|
||||
return False
|
||||
if time.time() - self.last_dose > self.watering_delay:
|
||||
self.pump.dose(self.pump_speed, self.pump_time, blocking=False)
|
||||
self.last_dose = time.time()
|
||||
return True
|
||||
return False
|
||||
|
||||
def render(self, image, font, selected=False):
|
||||
draw = ImageDraw.Draw(image)
|
||||
x = [21, 61, 101][self.channel - 1]
|
||||
|
||||
# Saturation amounts from each sensor
|
||||
c = 1.0 - self.sensor.saturation
|
||||
active = self.sensor.active and self.enabled
|
||||
|
||||
if active:
|
||||
# Draw background bars
|
||||
draw.rectangle(
|
||||
(x, int(c * HEIGHT), x + 37, HEIGHT),
|
||||
self.indicator_color(c) if active else (229, 229, 229),
|
||||
)
|
||||
|
||||
# Draw plant image
|
||||
x -= 3
|
||||
y = HEIGHT - self.icon.height
|
||||
pl = self.icon
|
||||
if not active:
|
||||
pl = pl.convert("LA").convert("RGB")
|
||||
image.paste(pl, (x, y), mask=self.icon)
|
||||
|
||||
# Channel selection icons
|
||||
x += 15
|
||||
draw.rectangle(
|
||||
(x, 2, x + 15, 17),
|
||||
self.indicator_color(c, self.label_colours) if active else (129, 129, 129),
|
||||
)
|
||||
|
||||
if selected:
|
||||
selected_x = x - 2
|
||||
draw.rectangle(
|
||||
(selected_x, 0, selected_x + 19, 20),
|
||||
self.indicator_color(c, self.label_colours)
|
||||
if active
|
||||
else (129, 129, 129),
|
||||
)
|
||||
|
||||
# TODO: replace with graphic, since PIL has no anti-aliasing
|
||||
draw.polygon(
|
||||
[(selected_x, 20), (selected_x + 9, 25), (selected_x + 19, 20)],
|
||||
fill=self.indicator_color(c, self.label_colours)
|
||||
if active
|
||||
else (129, 129, 129),
|
||||
)
|
||||
|
||||
# TODO: replace number text with graphic
|
||||
|
||||
tw, th = font.getsize("{}".format(self.channel))
|
||||
draw.text(
|
||||
(x + int(math.ceil(8 - (tw / 2.0))), 2),
|
||||
"{}".format(self.channel),
|
||||
font=font,
|
||||
fill=(255, 255, 255),
|
||||
)
|
||||
|
||||
def update(self):
|
||||
if not self.enabled:
|
||||
return
|
||||
sat = self.sensor.saturation
|
||||
if sat < self.water_level:
|
||||
if self.water():
|
||||
logging.info(
|
||||
"Watering Channel: {} - rate {:.2f} for {:.2f}sec".format(
|
||||
self.channel, self.pump_speed, self.pump_time
|
||||
)
|
||||
)
|
||||
if sat < self.alarm_level and not self.alarm:
|
||||
logging.warning(
|
||||
"Alarm on Channel: {} - saturation is {:.2f} (warn level {:.2f})".format(
|
||||
self.channel, sat, self.alarm_level
|
||||
)
|
||||
)
|
||||
self.alarm = True
|
||||
|
||||
|
||||
BUTTONS = [5, 6, 16, 24]
|
||||
LABELS = ["A", "B", "X", "Y"]
|
||||
|
||||
channel_selected = 0
|
||||
alarm = False
|
||||
|
||||
plants = []
|
||||
|
||||
# Load all of the plant icons
|
||||
for x in range(1, 15):
|
||||
plants.append(Image.open("icons/flat-{}.png".format(x)))
|
||||
|
||||
# Pick a random selection of plant icons to display on screen
|
||||
channels = [
|
||||
Channel(1, 1, 1, icon=random.choice(plants)),
|
||||
Channel(2, 2, 2, icon=random.choice(plants)),
|
||||
Channel(3, 3, 3, icon=random.choice(plants)),
|
||||
]
|
||||
|
||||
logging.basicConfig(
|
||||
format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s",
|
||||
level=logging.INFO,
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
|
||||
|
||||
def handle_button(pin):
|
||||
global channel_selected, alarm
|
||||
index = BUTTONS.index(pin)
|
||||
label = LABELS[index]
|
||||
|
||||
if label == "A": # Select Channel
|
||||
channel_selected += 1
|
||||
channel_selected %= len(channels)
|
||||
|
||||
if label == "B": # Cancel Alarm
|
||||
alarm = False
|
||||
for channel in channels:
|
||||
channel.alarm = False
|
||||
|
||||
if label == "X": # Set Wet Point
|
||||
channels[channel_selected].sensor.set_wet_point()
|
||||
|
||||
if label == "Y": # Set Dry Point
|
||||
channels[channel_selected].sensor.set_dry_point()
|
||||
|
||||
|
||||
GPIO.setmode(GPIO.BCM)
|
||||
GPIO.setwarnings(False)
|
||||
GPIO.setup(BUTTONS, GPIO.IN, pull_up_down=GPIO.PUD_UP)
|
||||
|
||||
for pin in BUTTONS:
|
||||
GPIO.add_event_detect(pin, GPIO.FALLING, handle_button, bouncetime=150)
|
||||
|
||||
|
||||
# Only the ALPHA channel is used from these images
|
||||
icon_drop = Image.open("icons/icon-drop.png")
|
||||
icon_nodrop = Image.open("icons/icon-nodrop.png")
|
||||
icon_rightarrow = Image.open("icons/icon-rightarrow.png")
|
||||
icon_snooze = Image.open("icons/icon-snooze.png")
|
||||
|
||||
CHANNEL_W = 40
|
||||
CHANNEL_M = 2
|
||||
|
||||
# Set up the ST7735 SPI Display
|
||||
display = ST7735.ST7735(
|
||||
port=0, cs=1, dc=9, backlight=12, rotation=270, spi_speed_hz=80000000
|
||||
)
|
||||
display.begin()
|
||||
WIDTH, HEIGHT = display.width, display.height
|
||||
|
||||
# Set up our canvas and prepare for drawing
|
||||
image = Image.new("RGBA", (WIDTH, HEIGHT), color=(255, 255, 255))
|
||||
font = ImageFont.truetype(UserFont, 14)
|
||||
draw = ImageDraw.Draw(image)
|
||||
|
||||
|
||||
def icon(image, icon, position, color):
|
||||
col = Image.new("RGBA", (20, 20), color=color)
|
||||
image.paste(col, position, mask=icon)
|
||||
|
||||
|
||||
def update():
|
||||
global alarm
|
||||
|
||||
for channel in channels:
|
||||
channel.update()
|
||||
if channel.alarm:
|
||||
alarm = True
|
||||
|
||||
|
||||
def render():
|
||||
t = time.time()
|
||||
|
||||
# Icon backdrops
|
||||
draw.rectangle((0, 0, 19, 19), (32, 138, 251))
|
||||
draw.rectangle((0, HEIGHT - 19, 19, HEIGHT), (255, 255, 255))
|
||||
draw.rectangle((WIDTH - 20, 0, WIDTH, 19), (75, 166, 252))
|
||||
draw.rectangle((WIDTH - 20, HEIGHT - 19, WIDTH, HEIGHT), (254, 218, 80))
|
||||
|
||||
# Icons
|
||||
icon(image, icon_rightarrow, (0, 0), (255, 255, 255))
|
||||
icon(image, icon_drop, (WIDTH - 20, 0), (255, 255, 255))
|
||||
icon(image, icon_nodrop, (WIDTH - 20, HEIGHT - 20), (255, 255, 255))
|
||||
|
||||
draw.rectangle((21, 0, 138, HEIGHT), (255, 255, 255)) # Erase channel area
|
||||
|
||||
for channel in channels:
|
||||
channel.render(image, font, channel_selected == channel.channel - 1)
|
||||
|
||||
# Draw the snooze icon- will be pulsing red if the alarm state is True
|
||||
r = 129
|
||||
if alarm:
|
||||
r = int(((math.sin(t * 3 * math.pi) + 1.0) / 2.0) * 255)
|
||||
icon(image, icon_snooze, (0, HEIGHT - 20), (r, 129, 129))
|
||||
|
||||
|
||||
def main():
|
||||
alarm_enable = True
|
||||
alarm_interval = 10.0
|
||||
piezo = Piezo()
|
||||
time_last_beep = time.time()
|
||||
|
||||
settings_file = "settings.yml"
|
||||
if len(sys.argv) > 1:
|
||||
settings_file = sys.argv[1]
|
||||
settings_file = pathlib.Path(settings_file)
|
||||
if settings_file.is_file():
|
||||
try:
|
||||
config = yaml.safe_load(open(settings_file))
|
||||
except yaml.parser.ParserError as e:
|
||||
raise yaml.parser.ParserError(
|
||||
"Error parsing settings file: {} ({})".format(settings_file, e)
|
||||
)
|
||||
|
||||
for channel in channels:
|
||||
ch = config.get("channel{}".format(channel.channel), None)
|
||||
channel.update_from_yml(ch)
|
||||
|
||||
settings = config.get("general", None)
|
||||
if settings is not None:
|
||||
alarm_enable = settings.get("alarm_enable", alarm_enable)
|
||||
alarm_interval = settings.get("alarm_interval", alarm_interval)
|
||||
|
||||
print("Channels:")
|
||||
for channel in channels:
|
||||
print(channel)
|
||||
|
||||
print(
|
||||
"""Settings:
|
||||
Alarm Enabled: {}
|
||||
Alarm Interval: {:.2f}s
|
||||
""".format(
|
||||
alarm_enable, alarm_interval
|
||||
)
|
||||
)
|
||||
|
||||
while True:
|
||||
update()
|
||||
render()
|
||||
display.display(image.convert("RGB"))
|
||||
|
||||
#w, h = image.size
|
||||
#image.convert("RGB").resize((w * 4, h * 4), Image.NEAREST).save("display.png")
|
||||
#break
|
||||
|
||||
if alarm_enable and alarm and time.time() - time_last_beep > alarm_interval:
|
||||
piezo.beep(440, 0.1, blocking=False)
|
||||
threading.Timer(0.3, piezo.beep, args=[440, 0.1], kwargs={"blocking":False}).start()
|
||||
threading.Timer(0.6, piezo.beep, args=[440, 0.1], kwargs={"blocking":False}).start()
|
||||
time_last_beep = time.time()
|
||||
|
||||
# 5 FPS
|
||||
time.sleep(1.0 / 10)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -528,6 +528,13 @@ class ChannelEditView(ChannelView, EditView):
|
||||
"format": lambda value: "Yes" if value else "No",
|
||||
"help": "Enable/disable this channel",
|
||||
},
|
||||
{
|
||||
"title": "Auto Water",
|
||||
"prop": "auto_water",
|
||||
"mode": "bool",
|
||||
"format": lambda value: "Yes" if value else "No",
|
||||
"help": "Enable/disable watering",
|
||||
},
|
||||
{
|
||||
"title": "Wet Point",
|
||||
"prop": "wet_point",
|
||||
@@ -552,6 +559,39 @@ class ChannelEditView(ChannelView, EditView):
|
||||
"help": "Frequency for fully dried soil",
|
||||
"context": "hz",
|
||||
},
|
||||
{
|
||||
"title": "Pump Time",
|
||||
"prop": "pump_time",
|
||||
"inc": 0.05,
|
||||
"min": 0.05,
|
||||
"max": 2.0,
|
||||
"mode": "float",
|
||||
"round": 2,
|
||||
"format": lambda value: f"{value:0.2f}sec",
|
||||
"help": "Time to run pump"
|
||||
},
|
||||
{
|
||||
"title": "Pump Speed",
|
||||
"prop": "pump_speed",
|
||||
"inc": 0.05,
|
||||
"min": 0.05,
|
||||
"max": 1.0,
|
||||
"mode": "float",
|
||||
"round": 2,
|
||||
"format": lambda value: f"{value*100:0.0f}%",
|
||||
"help": "Speed of pump"
|
||||
},
|
||||
{
|
||||
"title": "Watering Delay",
|
||||
"prop": "watering_delay",
|
||||
"inc": 10,
|
||||
"min": 30,
|
||||
"max": 500,
|
||||
"mode": "int",
|
||||
"format": lambda value: f"{value:0.0f}sec",
|
||||
"help": "Delay between waterings"
|
||||
},
|
||||
|
||||
]
|
||||
EditView.__init__(self, image, options)
|
||||
ChannelView.__init__(self, image, channel)
|
||||
@@ -582,9 +622,9 @@ class Channel:
|
||||
title=None,
|
||||
water_level=0.5,
|
||||
warn_level=0.5,
|
||||
pump_speed=0.7,
|
||||
pump_time=0.7,
|
||||
watering_delay=30,
|
||||
pump_speed=0.5,
|
||||
pump_time=0.2,
|
||||
watering_delay=60,
|
||||
wet_point=0.7,
|
||||
dry_point=26.7,
|
||||
icon=None,
|
||||
@@ -880,6 +920,10 @@ class Config:
|
||||
"warn_level",
|
||||
"wet_point",
|
||||
"dry_point",
|
||||
"watering_delay",
|
||||
"auto_water",
|
||||
"pump_time",
|
||||
"pump_speed"
|
||||
]
|
||||
|
||||
self.general_settings = [
|
||||
|
||||
@@ -1,17 +1,29 @@
|
||||
channel1:
|
||||
auto_water: false
|
||||
dry_point: 27
|
||||
enabled: true
|
||||
pump_speed: 0.5
|
||||
pump_time: 0.5
|
||||
warn_level: 0.2
|
||||
watering_delay: 60
|
||||
wet_point: 3
|
||||
channel2:
|
||||
auto_water: false
|
||||
dry_point: 27
|
||||
enabled: true
|
||||
pump_speed: 0.5
|
||||
pump_time: 0.5
|
||||
warn_level: 0.5
|
||||
watering_delay: 60
|
||||
wet_point: 3
|
||||
channel3:
|
||||
auto_water: false
|
||||
dry_point: 27
|
||||
enabled: true
|
||||
pump_speed: 0.5
|
||||
pump_time: 0.5
|
||||
warn_level: 0.4
|
||||
watering_delay: 60
|
||||
wet_point: 3
|
||||
general:
|
||||
alarm_enable: true
|
||||
|
||||
Reference in New Issue
Block a user