diff --git a/examples/new/monitor.py b/examples/new/monitor.py index 6a07e76..a18e1f2 100644 --- a/examples/new/monitor.py +++ b/examples/new/monitor.py @@ -9,6 +9,7 @@ import threading import RPi.GPIO as GPIO import ST7735 +import ltr559 from fonts.ttf import RobotoMedium as UserFont from PIL import Image, ImageDraw, ImageFont @@ -47,8 +48,9 @@ def icon(image, icon, position, color): class View: - def __init__(self): - pass + def __init__(self, image): + self._image = image + self._draw = ImageDraw.Draw(image) def button_a(self): return False @@ -65,12 +67,11 @@ class View: def update(self): pass - def render(self, canvas): + def render(self): pass def label( self, - canvas, position="X", text="", bgcolor=(0, 0, 0), @@ -80,8 +81,7 @@ class View: if position not in ["A", "B", "X", "Y"]: raise ValueError(f"Invalid label position {position}") - draw = ImageDraw.Draw(canvas) - text_w, text_h = draw.textsize(text, font=font) + text_w, text_h = self._draw.textsize(text, font=font) text_h = 11 text_w += margin * 2 text_h += margin * 2 @@ -97,17 +97,16 @@ class View: x2, y2 = x + text_w, y + text_h - draw.rectangle((x, y, x2, y2), bgcolor) - draw.text((x + margin, y + margin - 1), text, font=font, fill=textcolor) + self._draw.rectangle((x, y, x2, y2), bgcolor) + self._draw.text((x + margin, y + margin - 1), text, font=font, fill=textcolor) class MainView(View): - def __init__(self, channels=None): + def __init__(self, image, channels=None): self.channels = channels - View.__init__(self) + View.__init__(self, image) - def render_channel(self, canvas, channel, font): - draw = ImageDraw.Draw(image) + def render_channel(self, channel, font): x = [21, 61, 101][channel.channel - 1] # Saturation amounts from each sensor @@ -116,65 +115,92 @@ class MainView(View): if active: # Draw background bars - draw.rectangle( - (x, int((1.0 - saturation) * HEIGHT), x + 37, HEIGHT), + self._draw.rectangle( + (x, int((1.0 - saturation) * DISPLAY_HEIGHT), x + 37, DISPLAY_HEIGHT), channel.indicator_color(saturation) if active else (229, 229, 229), ) # Channel selection icons x += 15 col = channel.indicator_color(saturation, channel.label_colours) - draw.rectangle( - (x, 2, x + 15, 17), - col if active else (129, 129, 129), - ) + if channel.alarm: + icon( + self._image, + icon_snooze, + (x - 2, 0), + col if active else (129, 129, 129) + ) + else: + self._draw.rectangle( + (x, 2, x + 15, 17), + col if active else (129, 129, 129), + ) # TODO: replace number text with graphic tw, th = font.getsize("{}".format(channel.channel)) - draw.text( + self._draw.text( (x + int(math.ceil(8 - (tw / 2.0))), 2), "{}".format(channel.channel), font=font, fill=(255, 255, 255) if active else (200, 200, 200), ) - def render(self, canvas): - draw = ImageDraw.Draw(canvas) - width, height = canvas.size - draw.rectangle((0, 0, width, height), (255, 255, 255)) + def render(self): + self._draw.rectangle((0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT), (255, 255, 255)) for channel in self.channels: - self.render_channel(canvas, channel, font) + self.render_channel(channel, font) # Icon backdrops - draw.rectangle((0, 0, 19, 19), (32, 138, 251)) + self._draw.rectangle((0, 0, 19, 19), (32, 138, 251)) # Icons - icon(image, icon_rightarrow, (0, 0), (255, 255, 255)) + icon(self._image, icon_rightarrow, (0, 0), (255, 255, 255)) - alarm.render(canvas, (0, DISPLAY_HEIGHT - 19)) + alarm.render((0, DISPLAY_HEIGHT - 19)) -class DetailView(View): - def __init__(self, channel=None): +class ChannelView(View): + def draw_status(self, subtle=False): + self._draw.rectangle((0, 20, DISPLAY_WIDTH, 40), (50, 50, 50)) + + self._draw.text( + (3, 23), + f"{self.channel.sensor.saturation * 100:.2f}% ({self.channel.sensor.moisture:.2f}Hz)", + font=font, + fill=(150, 150, 150) if subtle else (255, 255, 255), + ) + + def draw_next_button(self, disabled=False): + if disabled: + # Draw disabled "Next" button + self._draw.rectangle((0, 0, 19, 19), (138, 138, 138)) + icon(self._image, icon_rightarrow, (0, 0), (150, 150, 150)) + else: + self._draw.rectangle((0, 0, 19, 19), COLOR_BLUE) + icon(self._image, icon_rightarrow, (0, 0), COLOR_WHITE) + + +class DetailView(ChannelView): + def __init__(self, image, channel=None): self.channel = channel - View.__init__(self) + View.__init__(self, image) - def render(self, canvas): - draw = ImageDraw.Draw(canvas) - width, height = canvas.size - draw.rectangle((0, 0, width, height), (255, 255, 255)) + def render(self): + width, height = self._image.size + self._draw.rectangle((0, 0, width, height), (255, 255, 255)) - draw.text( + self._draw.text( (23, 3), "{}".format(self.channel.title), font=font, fill=(0, 0, 0), ) - graph_height = DISPLAY_HEIGHT - 20 + graph_height = DISPLAY_HEIGHT - 40 - draw.rectangle((0, 20, DISPLAY_WIDTH, DISPLAY_HEIGHT), (60, 60, 60)) + self._draw.rectangle((0, 40, DISPLAY_WIDTH, 40 + graph_height), (10, 10, 10)) + self.draw_status() offset_x = 20 offset_y = 20 @@ -182,47 +208,42 @@ class DetailView(View): for x, value in enumerate(self.channel.sensor.history[:DISPLAY_WIDTH]): color = self.channel.indicator_color(value) h = value * graph_height - draw.rectangle((x, DISPLAY_HEIGHT - h, x + 1, DISPLAY_HEIGHT), color) + x = DISPLAY_WIDTH - x - 1 + self._draw.rectangle((x, DISPLAY_HEIGHT - h, x + 1, DISPLAY_HEIGHT), color) - alarm_line = self.channel.alarm_level * graph_height - draw.rectangle( + alarm_line = int(self.channel.alarm_level * graph_height) + r = 255 + if self.channel.alarm: + r = int(((math.sin(time.time() * 3 * math.pi) + 1.0) / 2.0) * 128) + 127 + self._draw.rectangle( ( 0, DISPLAY_HEIGHT - alarm_line, + DISPLAY_WIDTH - 40, + DISPLAY_HEIGHT - alarm_line + 1, + ), + (r, 0, 0), + ) + self._draw.rectangle( + ( + DISPLAY_WIDTH - 20, + 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), + (r, 0, 0), ) - draw.text( - (DISPLAY_WIDTH - 47, DISPLAY_HEIGHT - alarm_line - 15), - "Alarm", - font=font, - fill=(255, 255, 255), - ) + icon(self._image, icon_snooze, (DISPLAY_WIDTH - 40, DISPLAY_HEIGHT - alarm_line - 10), (r, 0, 0)) - # Icon backdrops - draw.rectangle((0, 0, 19, 19), (32, 138, 251)) - - # Icons - icon(image, icon_rightarrow, (0, 0), (255, 255, 255)) + self.draw_next_button() # Edit - self.label(canvas, "X", "Edit", textcolor=(255, 255, 255), bgcolor=COLOR_RED) + self.label("X", "Edit", textcolor=(255, 255, 255), bgcolor=COLOR_RED) -class EditView(View): - def __init__(self, channel=None): +class EditView(ChannelView): + def __init__(self, image, channel=None): self._options = [ { "title": "Alarm Level", @@ -258,21 +279,16 @@ class EditView(View): self._current_option = 0 self._change_mode = False self.channel = channel - View.__init__(self) + View.__init__(self, image) - def render(self, canvas): - draw = ImageDraw.Draw(canvas) - width, height = canvas.size - draw.rectangle((0, 0, width, height), (255, 255, 255)) + def render(self): + graph_height = DISPLAY_HEIGHT - 40 - draw.text((23, 3), "{}".format(self.channel.title), font=font, fill=(0, 0, 0)) + self._draw.rectangle((0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT), (255, 255, 255)) - draw.text( - (5, 25), - f"Now: {self.channel.sensor.saturation * 100:.2f}% {self.channel.sensor.moisture:.2f}Hz", - font=font, - fill=(0, 0, 0), - ) + self._draw.text((23, 3), "{}".format(self.channel.title), font=font, fill=(0, 0, 0)) + + self.draw_status(True) option = self._options[self._current_option] title = option["title"] @@ -280,23 +296,18 @@ class EditView(View): value = getattr(self.channel, prop) text = option["format"](value) mode = option.get("mode", "int") - draw.text((5, 40), f"{title} : {text}", font=font, fill=(0, 0, 0)) + self._draw.text((3, 43), f"{title} : {text}", font=font, fill=(0, 0, 0)) - draw.rectangle((0, 0, 19, 19), (138, 138, 138)) - - # Icons - icon(image, icon_rightarrow, (0, 0), (255, 255, 255)) + self.draw_next_button(True) if self._change_mode: - self.label(canvas, "Y", "Yes" if mode == "bool" else "++", textcolor=COLOR_WHITE, bgcolor=COLOR_YELLOW) - self.label(canvas, "B", "No" if mode == "bool" else "--", textcolor=COLOR_WHITE, bgcolor=COLOR_BLUE) + self.label("Y", "Yes" if mode == "bool" else "++", textcolor=COLOR_WHITE, bgcolor=COLOR_YELLOW) + self.label("B", "No" if mode == "bool" else "--", textcolor=COLOR_WHITE, bgcolor=COLOR_BLUE) else: - self.label( - canvas, "Y", "Change", textcolor=COLOR_WHITE, bgcolor=COLOR_YELLOW - ) - self.label(canvas, "B", "Next", textcolor=COLOR_WHITE, bgcolor=COLOR_BLUE) + self.label("Y", "Change", textcolor=COLOR_WHITE, bgcolor=COLOR_YELLOW) + self.label("B", "Next", textcolor=COLOR_WHITE, bgcolor=COLOR_BLUE) - self.label(canvas, "X", "Done", textcolor=COLOR_WHITE, bgcolor=COLOR_RED) + self.label("X", "Done", textcolor=COLOR_WHITE, bgcolor=COLOR_RED) def button_a(self): pass @@ -320,6 +331,7 @@ class EditView(View): else: self._current_option += 1 self._current_option %= len(self._options) + return True def button_x(self): if self._change_mode: @@ -388,17 +400,43 @@ class Channel: 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._wet_point = wet_point + self._dry_point = dry_point self.last_dose = time.time() self.icon = icon - self.enabled = enabled + self._enabled = enabled self.alarm = False self.title = f"Channel {display_channel}" if title is None else title self.sensor.set_wet_point(wet_point) self.sensor.set_dry_point(dry_point) + @property + def enabled(self): + return self._enabled + + @enabled.setter + def enabled(self, enabled): + self._enabled = enabled + + @property + def wet_point(self): + return self._wet_point + + @property + def dry_point(self): + return self._dry_point + + @wet_point.setter + def wet_point(self, wet_point): + self._wet_point = wet_point + self.sensor.set_wet_point(wet_point) + + @dry_point.setter + def dry_point(self, dry_point): + self._dry_point = dry_point + self.sensor.set_dry_point(dry_point) + def indicator_color(self, value, r=None): value = 1.0 - value @@ -444,7 +482,16 @@ Delay: {watering_delay} Wet point: {wet_point} Dry point: {dry_point} """.format( - **self.__dict__ + channel=self.channel, + enabled=self.enabled, + alarm_level=self.alarm_level, + auto_water=self.auto_water, + water_level=self.water_level, + pump_speed=self.pump_speed, + pump_time=self.pump_time, + watering_delay=self.watering_delay, + wet_point=self.wet_point, + dry_point=self.dry_point ) def water(self): @@ -470,17 +517,22 @@ Dry point: {dry_point} self.channel, self.pump_speed, self.pump_time ) ) - if sat < self.alarm_level and not self.alarm: + if sat < self.alarm_level: + if 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 + self.alarm = True + else: + self.alarm = False class Alarm: - def __init__(self, enabled=True, interval=10.0, beep_frequency=440): + def __init__(self, image, enabled=True, interval=10.0, beep_frequency=440): + self._image = image + self._draw = ImageDraw.Draw(image) self.piezo = Piezo() self.enabled = enabled self.interval = interval @@ -494,13 +546,14 @@ class Alarm: self.enabled = config.get("alarm_enable", self.enabled) self.interval = config.get("alarm_interval", self.interval) - def update(self): + def update(self, lights_out=False): if self._sleep_until is not None: if self._sleep_until > time.time(): return if ( self.enabled + and not lights_out and self._triggered and time.time() - self._time_last_beep > self.interval ): @@ -519,19 +572,18 @@ class Alarm: ).start() self._time_last_beep = time.time() - def render(self, canvas, position=(0, 0)): - draw = ImageDraw.Draw(canvas) + def render(self, position=(0, 0)): x, y = position # Draw the snooze icon- will be pulsing red if the alarm state is True - draw.rectangle((x, y, x + 19, y + 19), (255, 255, 255)) + self._draw.rectangle((x, y, x + 19, y + 19), (255, 255, 255)) r = 129 if self._triggered: - r = int(((math.sin(time.time() * 3 * math.pi) + 1.0) / 2.0) * 255) - icon(image, icon_snooze, (x, y - 1), (r, 129, 129)) + r = int(((math.sin(time.time() * 3 * math.pi) + 1.0) / 2.0) * 128) + 127 + icon(self._image, icon_snooze, (x, y - 1), (r, 129, 129)) if self._sleep_until is not None: # TODO maybe sleeping alarm icon? if self._sleep_until > time.time(): - draw.text((x, y), "zZ", font=font, fill=(255, 255, 255)) + self._draw.text((x, y), "zZ", font=font, fill=(255, 255, 255)) def trigger(self): self._triggered = True @@ -579,22 +631,24 @@ class ViewController: def update(self): self.view.update() - def render(self, canvas): - self.view.render(canvas) + def render(self): + self.view.render() def button_a(self): if not self.view.button_a(): self.next_view() def button_b(self): - self.view.button_b() + return self.view.button_b() def button_x(self): if not self.view.button_x(): self.next_subview() + return True + return True def button_y(self): - self.view.button_y() + return self.view.button_y() def handle_button(pin): @@ -620,10 +674,12 @@ 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 light sensor +light = ltr559.LTR559() # Set up our canvas and prepare for drawing -image = Image.new("RGBA", (WIDTH, HEIGHT), color=(255, 255, 255)) +image = Image.new("RGBA", (DISPLAY_WIDTH, DISPLAY_HEIGHT), color=(255, 255, 255)) font = ImageFont.truetype(UserFont, 14) @@ -636,14 +692,14 @@ channels = [ viewcontroller = ViewController( [ - 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])), + MainView(image, channels=channels), + (DetailView(image, channel=channels[0]), EditView(image, channel=channels[0])), + (DetailView(image, channel=channels[1]), EditView(image, channel=channels[1])), + (DetailView(image, channel=channels[2]), EditView(image, channel=channels[2])), ] ) -alarm = Alarm() +alarm = Alarm(image) def main(): @@ -695,10 +751,10 @@ Alarm Interval: {:.2f}s if channel.alarm: alarm.trigger() - alarm.update() + alarm.update(light.get_lux() < 4.0) viewcontroller.update() - viewcontroller.render(image) + viewcontroller.render() display.display(image.convert("RGB")) # 5 FPS