Files
grow-python/examples/new/monitor.py
2020-08-28 13:12:40 +01:00

493 lines
14 KiB
Python

#!/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
BUTTONS = [5, 6, 16, 24]
LABELS = ["A", "B", "X", "Y"]
DISPLAY_WIDTH = 160
DISPLAY_HEIGHT = 80
# 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")
def icon(image, icon, position, color):
col = Image.new("RGBA", (20, 20), color=color)
image.paste(col, position, mask=icon)
class View:
def __init__(self):
pass
def button_a(self):
pass
def button_b(self):
pass
def button_x(self):
pass
def button_y(self):
pass
def update(self):
pass
def render(self, canvas):
pass
class MainView(View):
def __init__(self, channels=None):
self.channels = channels
View.__init__(self)
def render(self, canvas):
draw = ImageDraw.Draw(canvas)
width, height = canvas.size
draw.rectangle((0, 0, width, height), (255, 255, 255))
for channel in self.channels:
channel.render(image, font)
# Icon backdrops
draw.rectangle((0, 0, 19, 19), (32, 138, 251))
# Icons
icon(image, icon_rightarrow, (0, 0), (255, 255, 255))
class DetailView(View):
def __init__(self, channel=None):
self.channel = channel
View.__init__(self)
def render(self, canvas):
draw = ImageDraw.Draw(canvas)
width, height = canvas.size
draw.rectangle((0, 0, width, height), (255, 255, 255))
self.channel.render_detail(canvas, font)
# Icon backdrops
draw.rectangle((0, 0, 19, 19), (32, 138, 251))
draw.rectangle((DISPLAY_WIDTH - 30, 0, DISPLAY_WIDTH, 19), (75, 166, 252))
# Icons
icon(image, icon_rightarrow, (0, 0), (255, 255, 255))
# Edit
draw.text(
(DISPLAY_WIDTH - 28, 3),
"Edit",
font=font,
fill=(255, 255, 255),
)
class EditView(View):
def __init__(self, channel=None):
self.channel = channel
View.__init__(self)
def render(self, canvas):
draw = ImageDraw.Draw(canvas)
width, height = canvas.size
draw.rectangle((0, 0, width, height), (255, 255, 255))
self.channel.render_edit(canvas, font)
# Icon backdrops
draw.rectangle((0, 0, 19, 19), (138, 138, 138))
draw.rectangle((DISPLAY_WIDTH - 38, 0, DISPLAY_WIDTH, 19), (75, 166, 252))
draw.rectangle((0, DISPLAY_HEIGHT - 19, 60, DISPLAY_WIDTH), (32, 137, 251))
draw.rectangle((DISPLAY_WIDTH - 60, DISPLAY_HEIGHT - 19, DISPLAY_WIDTH, DISPLAY_HEIGHT), (254, 219, 82))
# Icons
icon(image, icon_rightarrow, (0, 0), (255, 255, 255))
# Edit
draw.text(
(DISPLAY_WIDTH - 36, 3),
"Done",
font=font,
fill=(255, 255, 255)
)
draw.text(
(0, HEIGHT - 19),
"Set Wet",
font=font,
fill=(255, 255, 255)
)
draw.text(
(DISPLAY_WIDTH - 60, HEIGHT - 19),
"Set Dry",
font=font,
fill=(255, 255, 255)
)
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,
title=None,
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.title = "Channel {}".format(display_channel) if title is None else title
self.sensor.set_wet_point(wet_point)
self.sensor.set_dry_point(dry_point)
def indicator_color(self, value, r=None):
value = 1.0 - value
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_edit(self, image, font):
draw = ImageDraw.Draw(image)
draw.text(
(23, 3),
"{}".format(self.title),
font=font,
fill=(0, 0, 0)
)
draw.text(
(5, 30),
"Sat: {:.2f}%".format(self.sensor.saturation * 100),
font=font,
fill=(0, 0, 0)
)
def render_detail(self, image, font):
draw = ImageDraw.Draw(image)
draw.text(
(23, 3),
"{}".format(self.title),
font=font,
fill=(0, 0, 0),
)
graph_height = DISPLAY_HEIGHT - 20
draw.rectangle((
0, 20,
DISPLAY_WIDTH, DISPLAY_HEIGHT
), (60, 60, 60))
offset_x = 20
offset_y = 20
for x, value in enumerate(self.sensor.history[:DISPLAY_WIDTH]):
color = self.indicator_color(value)
h = value * graph_height
draw.rectangle((x, DISPLAY_HEIGHT - h, x + 1, DISPLAY_HEIGHT), color)
alarm_line = self.alarm_level * graph_height
draw.rectangle((0, DISPLAY_HEIGHT - alarm_line, DISPLAY_WIDTH, DISPLAY_HEIGHT - alarm_line + 1), (255, 0, 0))
draw.rectangle((DISPLAY_WIDTH - 50, DISPLAY_HEIGHT - alarm_line - 16, DISPLAY_WIDTH, DISPLAY_HEIGHT - alarm_line + 1), (255, 0, 0))
draw.text(
(DISPLAY_WIDTH - 47, DISPLAY_HEIGHT - alarm_line - 15),
"Alarm",
font=font,
fill=(255, 255, 255),
)
def render(self, image, font):
draw = ImageDraw.Draw(image)
x = [21, 61, 101][self.channel - 1]
# Saturation amounts from each sensor
saturation = self.sensor.saturation
active = self.sensor.active and self.enabled
if active:
# Draw background bars
draw.rectangle(
(x, int((1.0 - saturation) * HEIGHT), x + 37, HEIGHT),
self.indicator_color(saturation) if active else (229, 229, 229),
)
# Channel selection icons
x += 15
draw.rectangle(
(x, 2, x + 15, 17),
self.indicator_color(saturation, 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 * 100, self.alarm_level * 100
)
)
self.alarm = True
# 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)
# Pick a random selection of plant icons to display on screen
channels = [
Channel(1, 1, 1),
Channel(2, 2, 2),
Channel(3, 3, 3),
]
current_view = 0
current_subview = 0
views = [
MainView(channels=channels),
(DetailView(channel=channels[0]), EditView(channel=channels[0])),
(DetailView(channel=channels[1]), EditView(channel=channels[1])),
(DetailView(channel=channels[2]), EditView(channel=channels[2]))
]
def handle_button(pin):
global current_view, current_subview, alarm
index = BUTTONS.index(pin)
label = LABELS[index]
if label == "A": # Select View
if current_subview == 0:
current_view += 1
current_view %= len(views)
current_subview = 0
print("Switched to view {}".format(current_view))
if label == "B": # Cancel Alarm
alarm = False
for channel in channels:
channel.alarm = False
if label == "X":
view = views[current_view]
if isinstance(view, tuple):
current_subview += 1
current_subview %= len(view)
print("Switched to subview {}".format(current_subview))
if label == "Y":
pass
def main():
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=200)
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:
view = views[current_view]
if isinstance(view, tuple):
view = view[current_subview]
view.update()
view.render(image)
display.display(image.convert("RGB"))
#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()