mirror of
https://github.com/pimoroni/grow-python
synced 2025-10-25 15:19:23 +00:00
Tweaks, fixes, linting and docs
This commit is contained in:
12
Makefile
12
Makefile
@@ -27,7 +27,7 @@ check:
|
|||||||
@echo "Checking for trailing whitespace"
|
@echo "Checking for trailing whitespace"
|
||||||
@! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO
|
@! grep -IUrn --color "[[:blank:]]$$" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=PKG-INFO
|
||||||
@echo "Checking for DOS line-endings"
|
@echo "Checking for DOS line-endings"
|
||||||
@! grep -IUrn --color "
|
@! grep -lIUrn --color "
|
||||||
" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile
|
" --exclude-dir=sphinx --exclude-dir=.tox --exclude-dir=.git --exclude=Makefile
|
||||||
@echo "Checking library/CHANGELOG.txt"
|
@echo "Checking library/CHANGELOG.txt"
|
||||||
@cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION}
|
@cat library/CHANGELOG.txt | grep ^${LIBRARY_VERSION}
|
||||||
@@ -36,14 +36,14 @@ check:
|
|||||||
|
|
||||||
tag:
|
tag:
|
||||||
git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}"
|
git tag -a "v${LIBRARY_VERSION}" -m "Version ${LIBRARY_VERSION}"
|
||||||
|
|
||||||
python-readme: library/README.md
|
python-readme: library/README.md
|
||||||
|
|
||||||
python-license: library/LICENSE.txt
|
python-license: library/LICENSE.txt
|
||||||
|
|
||||||
library/README.rst: README.md library/CHANGELOG.txt
|
library/README.md: README.md library/CHANGELOG.txt
|
||||||
pandoc --from=markdown --to=rst -o library/README.rst README.md
|
cp README.md library/README.md
|
||||||
echo "" >> library/README.rst
|
printf "\n# Changelog\n" >> library/README.md
|
||||||
cat library/CHANGELOG.txt >> library/README.md
|
cat library/CHANGELOG.txt >> library/README.md
|
||||||
|
|
||||||
library/LICENSE.txt: LICENSE
|
library/LICENSE.txt: LICENSE
|
||||||
|
|||||||
21
README.md
21
README.md
@@ -29,21 +29,22 @@ curl -sSL https://get.pimoroni.com/grow | bash
|
|||||||
|
|
||||||
## Or... Install from PyPi and configure manually:
|
## Or... Install from PyPi and configure manually:
|
||||||
|
|
||||||
* Run `sudo pip install growhat`
|
* Install dependencies:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install python3-setuptools python3-pip python3-yaml python3-smbus python3-pil python3-spidev python3-rpi.gpio
|
||||||
|
```
|
||||||
|
|
||||||
|
* Run `sudo pip3 install growhat`
|
||||||
|
|
||||||
**Note** this wont perform any of the required configuration changes on your Pi, you may additionally need to:
|
**Note** this wont perform any of the required configuration changes on your Pi, you may additionally need to:
|
||||||
|
|
||||||
* Enable i2c: `raspi-config nonint do_i2c 0`
|
* Enable i2c: `sudo raspi-config nonint do_i2c 0`
|
||||||
* Enable SPI: `raspi-config nonint do_spi 0`
|
* Enable SPI: `sudo raspi-config nonint do_spi 0`
|
||||||
|
* Add the following to `/boot/config.txt`: `dtoverlay=spi0-cs,cs0_pin=14`
|
||||||
And install additional dependencies:
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo apt install python-numpy python-smbus python-pil python-setuptools
|
|
||||||
```
|
|
||||||
|
|
||||||
## Help & Support
|
## Help & Support
|
||||||
|
|
||||||
* GPIO Pinout - https://pinout.xyz/pinout/enviro_plus
|
* GPIO Pinout - https://pinout.xyz/pinout/grow_hat_mini
|
||||||
* Support forums - http://forums.pimoroni.com/c/support
|
* Support forums - http://forums.pimoroni.com/c/support
|
||||||
* Discord - https://discord.gg/hr93ByC
|
* Discord - https://discord.gg/hr93ByC
|
||||||
|
|||||||
14
REFERENCE.md
14
REFERENCE.md
@@ -1,7 +1,5 @@
|
|||||||
# Grow <!-- omit in toc -->
|
# Grow <!-- omit in toc -->
|
||||||
|
|
||||||
Grow
|
|
||||||
|
|
||||||
- [Getting Started](#getting-started)
|
- [Getting Started](#getting-started)
|
||||||
- [Requirements](#requirements)
|
- [Requirements](#requirements)
|
||||||
- [Python 3 & pip](#python-3--pip)
|
- [Python 3 & pip](#python-3--pip)
|
||||||
@@ -38,7 +36,7 @@ You should use Python 3, which may need installing on your Pi:
|
|||||||
|
|
||||||
```
|
```
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install python3 python3-pip
|
sudo apt install python3 python3-pip python3-setuptools
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Enabling i2c and spi
|
#### Enabling i2c and spi
|
||||||
@@ -50,6 +48,14 @@ sudo raspi-config nonint do_i2c 0
|
|||||||
sudo raspi-config nonint do_spi 0
|
sudo raspi-config nonint do_spi 0
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Installing Dependencies
|
||||||
|
|
||||||
|
The following dependencies are required:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo apt install python3-yaml python3-smbus python3-pil python3-spidev python3-rpi.gpio
|
||||||
|
```
|
||||||
|
|
||||||
### Installing the library
|
### Installing the library
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -174,7 +180,7 @@ while True:
|
|||||||
moisture1.active()
|
moisture1.active()
|
||||||
```
|
```
|
||||||
|
|
||||||
Returns `True` if the moisture sensor is connected and returning valid readings.
|
Returns `True` if the moisture sensor is connected and returning valid readings.
|
||||||
|
|
||||||
Checks if a pulse has happened within the last second, and that the reading is within a sensible range.
|
Checks if a pulse has happened within the last second, and that the reading is within a sensible range.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,35 @@
|
|||||||
# Watering Settings
|
# Monitoring and/or Watering Your Plants
|
||||||
|
|
||||||
The `grow-monitor-and-water.py` example can monitor and, optionally, automatically water all three Grow channels.
|
The `grow-monitor-and-water.py` example can monitor and, optionally, automatically water all three Grow channels.
|
||||||
|
|
||||||
It's configured using a settings file - `water.yml` - that looks like the following:
|
By default auto-watering is disabled and an alarm will sound every 1s if the `warn_level` is reached.
|
||||||
|
|
||||||
|
Run it with `python3 grow-monitor-and-water.py`.
|
||||||
|
|
||||||
|
## Monitoring
|
||||||
|
|
||||||
|
Grow can monitor the moisture level of your soil, sounding an alarm when it dries out.
|
||||||
|
|
||||||
|
Grow is configured using `settings.yml`. Your settings for monitoring only will look something like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
channel1:
|
||||||
|
warn_level: 0.2
|
||||||
|
icon: icons/flat-4.png
|
||||||
|
channel2:
|
||||||
|
warn_level: 0.2
|
||||||
|
channel3:
|
||||||
|
warn_level: 0.2
|
||||||
|
general:
|
||||||
|
alarm_enable: True
|
||||||
|
alarm_interval: 1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Watering
|
||||||
|
|
||||||
|
If you've got pumps attached to Grow and want to automatically water your plants, you'll need some extra configuration options.
|
||||||
|
|
||||||
|
See [Channel Settings](#channel-settings) and [General Settings](#general-settings) for more information on what these do.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
channel1:
|
channel1:
|
||||||
@@ -29,10 +56,10 @@ general:
|
|||||||
alarm_interval: 1.0
|
alarm_interval: 1.0
|
||||||
```
|
```
|
||||||
|
|
||||||
By default auto-watering is disabled and an alarm will sound every 1s if the `warn_level` is reached.
|
|
||||||
|
|
||||||
## Channel Settings
|
## Channel Settings
|
||||||
|
|
||||||
|
Grow has three channels which are separated into the sections `channel1`, `channel2` and `channel3`, each of these sections has the following configuration options:
|
||||||
|
|
||||||
* `water_level` - The level at which auto-watering should be triggered (soil saturation from 0.0 to 1.0)
|
* `water_level` - The level at which auto-watering should be triggered (soil saturation from 0.0 to 1.0)
|
||||||
* `warn_level` - The level at which the alarm should be triggered (soil saturation from 0.0 to 1.0)
|
* `warn_level` - The level at which the alarm should be triggered (soil saturation from 0.0 to 1.0)
|
||||||
* `pump_speed` - The speed at which the pump should be run (from 0.0 low speed to 1.0 full speed)
|
* `pump_speed` - The speed at which the pump should be run (from 0.0 low speed to 1.0 full speed)
|
||||||
@@ -42,5 +69,7 @@ By default auto-watering is disabled and an alarm will sound every 1s if the `wa
|
|||||||
|
|
||||||
## General Settings
|
## General Settings
|
||||||
|
|
||||||
|
An additional `general` section can be used for global settings:
|
||||||
|
|
||||||
* `alarm_enable` - Whether to enable the alarm
|
* `alarm_enable` - Whether to enable the alarm
|
||||||
* `alarm_interval` - The interval at which the alarm should beep (in seconds)
|
* `alarm_interval` - The interval at which the alarm should beep (in seconds)
|
||||||
|
|||||||
@@ -6,24 +6,22 @@ from fonts.ttf import RobotoMedium as UserFont
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
|
format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s",
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
datefmt='%Y-%m-%d %H:%M:%S')
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
|
||||||
logging.info("""lcd.py - Hello, World! example on the 0.96" LCD.
|
logging.info(
|
||||||
|
"""lcd.py - Hello, World! example on the 0.96" LCD.
|
||||||
|
|
||||||
Press Ctrl+C to exit!
|
Press Ctrl+C to exit!
|
||||||
|
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Create LCD class instance.
|
# Create LCD class instance.
|
||||||
disp = ST7735.ST7735(
|
disp = ST7735.ST7735(
|
||||||
port=0,
|
port=0, cs=1, dc=9, backlight=12, rotation=270, spi_speed_hz=10000000
|
||||||
cs=1,
|
|
||||||
dc=9,
|
|
||||||
backlight=12,
|
|
||||||
rotation=270,
|
|
||||||
spi_speed_hz=10000000
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Initialize display.
|
# Initialize display.
|
||||||
@@ -34,7 +32,7 @@ WIDTH = disp.width
|
|||||||
HEIGHT = disp.height
|
HEIGHT = disp.height
|
||||||
|
|
||||||
# New canvas to draw on.
|
# New canvas to draw on.
|
||||||
img = Image.new('RGB', (WIDTH, HEIGHT), color=(0, 0, 0))
|
img = Image.new("RGB", (WIDTH, HEIGHT), color=(0, 0, 0))
|
||||||
draw = ImageDraw.Draw(img)
|
draw = ImageDraw.Draw(img)
|
||||||
|
|
||||||
# Text settings.
|
# Text settings.
|
||||||
@@ -20,17 +20,29 @@ class Channel:
|
|||||||
(192, 225, 254), # Blue
|
(192, 225, 254), # Blue
|
||||||
(196, 255, 209), # Green
|
(196, 255, 209), # Green
|
||||||
(255, 243, 192), # Yellow
|
(255, 243, 192), # Yellow
|
||||||
(254, 192, 192) # Red
|
(254, 192, 192), # Red
|
||||||
]
|
]
|
||||||
|
|
||||||
label_colours = [
|
label_colours = [
|
||||||
(32, 137, 251), # Blue
|
(32, 137, 251), # Blue
|
||||||
(100, 255, 124), # Green
|
(100, 255, 124), # Green
|
||||||
(254, 219, 82), # Yellow
|
(254, 219, 82), # Yellow
|
||||||
(254, 82, 82), # Red
|
(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, icon=None, auto_water=False):
|
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,
|
||||||
|
icon=None,
|
||||||
|
auto_water=False,
|
||||||
|
):
|
||||||
self.channel = display_channel
|
self.channel = display_channel
|
||||||
self.sensor = Moisture(sensor_channel)
|
self.sensor = Moisture(sensor_channel)
|
||||||
self.pump = Pump(pump_channel)
|
self.pump = Pump(pump_channel)
|
||||||
@@ -77,13 +89,15 @@ class Channel:
|
|||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return """Channel: {channel}
|
return """Channel: {channel}
|
||||||
Water level: {water_level}
|
|
||||||
Alarm level: {alarm_level}
|
Alarm level: {alarm_level}
|
||||||
Auto water: {auto_water}
|
Auto water: {auto_water}
|
||||||
|
Water level: {water_level}
|
||||||
Pump speed: {pump_speed}
|
Pump speed: {pump_speed}
|
||||||
Pump time: {pump_time}
|
Pump time: {pump_time}
|
||||||
Delay: {watering_delay}
|
Delay: {watering_delay}
|
||||||
""".format(**self.__dict__)
|
""".format(
|
||||||
|
**self.__dict__
|
||||||
|
)
|
||||||
|
|
||||||
def water(self):
|
def water(self):
|
||||||
if not self.auto_water:
|
if not self.auto_water:
|
||||||
@@ -103,7 +117,10 @@ Delay: {watering_delay}
|
|||||||
active = self.sensor.active
|
active = self.sensor.active
|
||||||
|
|
||||||
# Draw background bars
|
# Draw background bars
|
||||||
draw.rectangle((x, int(c * HEIGHT), x + 37, HEIGHT), self.indicator_color(c) if active else (229, 229, 229))
|
draw.rectangle(
|
||||||
|
(x, int(c * HEIGHT), x + 37, HEIGHT),
|
||||||
|
self.indicator_color(c) if active else (229, 229, 229),
|
||||||
|
)
|
||||||
|
|
||||||
# Draw plant image
|
# Draw plant image
|
||||||
x -= 3
|
x -= 3
|
||||||
@@ -115,37 +132,58 @@ Delay: {watering_delay}
|
|||||||
|
|
||||||
# Channel selection icons
|
# Channel selection icons
|
||||||
x += 15
|
x += 15
|
||||||
draw.rectangle((x, 2, x + 15, 17), self.indicator_color(c, self.label_colours) if active else (129, 129, 129))
|
draw.rectangle(
|
||||||
|
(x, 2, x + 15, 17),
|
||||||
|
self.indicator_color(c, self.label_colours) if active else (129, 129, 129),
|
||||||
|
)
|
||||||
|
|
||||||
if selected:
|
if selected:
|
||||||
selected_x = x - 2
|
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))
|
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
|
# TODO: replace with graphic, since PIL has no anti-aliasing
|
||||||
draw.polygon([
|
draw.polygon(
|
||||||
(selected_x, 20),
|
[(selected_x, 20), (selected_x + 9, 25), (selected_x + 19, 20)],
|
||||||
(selected_x + 9, 25),
|
fill=self.indicator_color(c, self.label_colours)
|
||||||
(selected_x + 19, 20)
|
if active
|
||||||
],
|
else (129, 129, 129),
|
||||||
fill=self.indicator_color(c, self.label_colours) if active else (129, 129, 129))
|
)
|
||||||
|
|
||||||
# TODO: replace number text with graphic
|
# TODO: replace number text with graphic
|
||||||
|
|
||||||
tw, th = font.getsize("{}".format(self.channel))
|
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))
|
draw.text(
|
||||||
|
(x + int(math.ceil(8 - (tw / 2.0))), 2),
|
||||||
|
"{}".format(self.channel),
|
||||||
|
font=font,
|
||||||
|
fill=(255, 255, 255),
|
||||||
|
)
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
sat = self.sensor.saturation
|
sat = self.sensor.saturation
|
||||||
if sat < self.water_level:
|
if sat < self.water_level:
|
||||||
if self.water():
|
if self.water():
|
||||||
logging.info("Watering Channel: {} - rate {:.2f} for {:.2f}sec".format(self.channel, self.pump_speed, self.pump_time))
|
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:
|
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))
|
logging.warning(
|
||||||
|
"Alarm on Channel: {} - saturation is {:.2f} (warn level {:.2f})".format(
|
||||||
|
self.channel, sat, self.alarm_level
|
||||||
|
)
|
||||||
|
)
|
||||||
self.alarm = True
|
self.alarm = True
|
||||||
|
|
||||||
|
|
||||||
BUTTONS = [5, 6, 16, 24]
|
BUTTONS = [5, 6, 16, 24]
|
||||||
LABELS = ['A', 'B', 'X', 'Y']
|
LABELS = ["A", "B", "X", "Y"]
|
||||||
|
|
||||||
channel_selected = 0
|
channel_selected = 0
|
||||||
alarm = False
|
alarm = False
|
||||||
@@ -158,15 +196,16 @@ for x in range(1, 15):
|
|||||||
|
|
||||||
# Pick a random selection of plant icons to display on screen
|
# Pick a random selection of plant icons to display on screen
|
||||||
channels = [
|
channels = [
|
||||||
Channel(1, 1, 1, icon=random.choice(plants)),
|
Channel(1, 1, 1, icon=random.choice(plants)),
|
||||||
Channel(2, 2, 2, icon=random.choice(plants)),
|
Channel(2, 2, 2, icon=random.choice(plants)),
|
||||||
Channel(3, 3, 3, icon=random.choice(plants))
|
Channel(3, 3, 3, icon=random.choice(plants)),
|
||||||
]
|
]
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
|
format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s",
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
datefmt='%Y-%m-%d %H:%M:%S')
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_button(pin):
|
def handle_button(pin):
|
||||||
@@ -174,19 +213,19 @@ def handle_button(pin):
|
|||||||
index = BUTTONS.index(pin)
|
index = BUTTONS.index(pin)
|
||||||
label = LABELS[index]
|
label = LABELS[index]
|
||||||
|
|
||||||
if label == 'A': # Select Channel
|
if label == "A": # Select Channel
|
||||||
channel_selected += 1
|
channel_selected += 1
|
||||||
channel_selected %= len(channels)
|
channel_selected %= len(channels)
|
||||||
|
|
||||||
if label == 'B': # Cancel Alarm
|
if label == "B": # Cancel Alarm
|
||||||
alarm = False
|
alarm = False
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
channel.alarm = False
|
channel.alarm = False
|
||||||
|
|
||||||
if label == 'X': # Set Wet Point
|
if label == "X": # Set Wet Point
|
||||||
channels[channel_selected].sensor.set_wet_point()
|
channels[channel_selected].sensor.set_wet_point()
|
||||||
|
|
||||||
if label == 'Y': # Set Dry Point
|
if label == "Y": # Set Dry Point
|
||||||
channels[channel_selected].sensor.set_dry_point()
|
channels[channel_selected].sensor.set_dry_point()
|
||||||
|
|
||||||
|
|
||||||
@@ -209,12 +248,7 @@ CHANNEL_M = 2
|
|||||||
|
|
||||||
# Set up the ST7735 SPI Display
|
# Set up the ST7735 SPI Display
|
||||||
display = ST7735.ST7735(
|
display = ST7735.ST7735(
|
||||||
port=0,
|
port=0, cs=1, dc=9, backlight=12, rotation=270, spi_speed_hz=80000000
|
||||||
cs=1,
|
|
||||||
dc=9,
|
|
||||||
backlight=12,
|
|
||||||
rotation=270,
|
|
||||||
spi_speed_hz=80000000
|
|
||||||
)
|
)
|
||||||
display.begin()
|
display.begin()
|
||||||
WIDTH, HEIGHT = display.width, display.height
|
WIDTH, HEIGHT = display.width, display.height
|
||||||
@@ -256,7 +290,7 @@ def render():
|
|||||||
draw.rectangle((21, 0, 138, HEIGHT), (255, 255, 255)) # Erase channel area
|
draw.rectangle((21, 0, 138, HEIGHT), (255, 255, 255)) # Erase channel area
|
||||||
|
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
channel.render(image, font, channel_selected==channel.channel - 1)
|
channel.render(image, font, channel_selected == channel.channel - 1)
|
||||||
|
|
||||||
# Draw the snooze icon- will be pulsing red if the alarm state is True
|
# Draw the snooze icon- will be pulsing red if the alarm state is True
|
||||||
r = 129
|
r = 129
|
||||||
@@ -271,7 +305,7 @@ def main():
|
|||||||
piezo = Piezo()
|
piezo = Piezo()
|
||||||
time_last_beep = time.time()
|
time_last_beep = time.time()
|
||||||
|
|
||||||
settings_file = "water.yml"
|
settings_file = "settings.yml"
|
||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
settings_file = sys.argv[1]
|
settings_file = sys.argv[1]
|
||||||
settings_file = pathlib.Path(settings_file)
|
settings_file = pathlib.Path(settings_file)
|
||||||
@@ -279,7 +313,9 @@ def main():
|
|||||||
try:
|
try:
|
||||||
config = yaml.safe_load(open(settings_file))
|
config = yaml.safe_load(open(settings_file))
|
||||||
except yaml.parser.ParserError as e:
|
except yaml.parser.ParserError as e:
|
||||||
raise yaml.parser.ParserError("Error parsing settings file: {} ({})".format(settings_file, e))
|
raise yaml.parser.ParserError(
|
||||||
|
"Error parsing settings file: {} ({})".format(settings_file, e)
|
||||||
|
)
|
||||||
|
|
||||||
for channel in channels:
|
for channel in channels:
|
||||||
ch = config.get("channel{}".format(channel.channel), None)
|
ch = config.get("channel{}".format(channel.channel), None)
|
||||||
@@ -294,13 +330,14 @@ def main():
|
|||||||
for channel in channels:
|
for channel in channels:
|
||||||
print(channel)
|
print(channel)
|
||||||
|
|
||||||
print("""Settings:
|
print(
|
||||||
|
"""Settings:
|
||||||
Alarm Enabled: {}
|
Alarm Enabled: {}
|
||||||
Alarm Interval: {:.2f}s
|
Alarm Interval: {:.2f}s
|
||||||
""".format(
|
""".format(
|
||||||
alarm_enable,
|
alarm_enable, alarm_interval
|
||||||
alarm_interval
|
)
|
||||||
))
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
update()
|
update()
|
||||||
|
|||||||
10
examples/settings.yml
Normal file
10
examples/settings.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
channel1:
|
||||||
|
warn_level: 0.2
|
||||||
|
icon: icons/flat-4.png
|
||||||
|
channel2:
|
||||||
|
warn_level: 0.2
|
||||||
|
channel3:
|
||||||
|
warn_level: 0.2
|
||||||
|
general:
|
||||||
|
alarm_enable: True
|
||||||
|
alarm_interval: 1.0
|
||||||
@@ -29,17 +29,17 @@ pump_channel = 3
|
|||||||
moisture_channel = 3
|
moisture_channel = 3
|
||||||
|
|
||||||
# Default watering settings
|
# Default watering settings
|
||||||
dry_level = 0.7 # Saturation level considered dry
|
dry_level = 0.7 # Saturation level considered dry
|
||||||
dose_speed = 0.63 # Pump speed for water dose
|
dose_speed = 0.63 # Pump speed for water dose
|
||||||
dose_time = 0.96 # Time (in seconds) for water dose
|
dose_time = 0.96 # Time (in seconds) for water dose
|
||||||
|
|
||||||
# Here be dragons!
|
# Here be dragons!
|
||||||
FPS = 15 # Display framerate
|
FPS = 15 # Display framerate
|
||||||
NUM_SAMPLES = 10 # Number of saturation level samples to average over
|
NUM_SAMPLES = 10 # Number of saturation level samples to average over
|
||||||
DOSE_FREQUENCY = 30.0 # Minimum time between automatic waterings (in seconds)
|
DOSE_FREQUENCY = 30.0 # Minimum time between automatic waterings (in seconds)
|
||||||
|
|
||||||
BUTTONS = [5, 6, 16, 24]
|
BUTTONS = [5, 6, 16, 24]
|
||||||
LABELS = ['A', 'B', 'X', 'Y']
|
LABELS = ["A", "B", "X", "Y"]
|
||||||
|
|
||||||
p = Pump(pump_channel)
|
p = Pump(pump_channel)
|
||||||
m = Moisture(moisture_channel)
|
m = Moisture(moisture_channel)
|
||||||
@@ -53,12 +53,7 @@ last_dose = time.time()
|
|||||||
saturation = [1.0 for _ in range(NUM_SAMPLES)]
|
saturation = [1.0 for _ in range(NUM_SAMPLES)]
|
||||||
|
|
||||||
display = ST7735.ST7735(
|
display = ST7735.ST7735(
|
||||||
port=0,
|
port=0, cs=1, dc=9, backlight=12, rotation=270, spi_speed_hz=80000000
|
||||||
cs=1,
|
|
||||||
dc=9,
|
|
||||||
backlight=12,
|
|
||||||
rotation=270,
|
|
||||||
spi_speed_hz=80000000
|
|
||||||
)
|
)
|
||||||
|
|
||||||
display.begin()
|
display.begin()
|
||||||
@@ -68,25 +63,26 @@ image = Image.new("RGBA", (display.width, display.height), color=(0, 0, 0))
|
|||||||
draw = ImageDraw.Draw(image)
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
format='%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s',
|
format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s",
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
datefmt='%Y-%m-%d %H:%M:%S')
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def handle_button(pin):
|
def handle_button(pin):
|
||||||
global mode, last_dose, dose_time, dose_speed, dry_level
|
global mode, last_dose, dose_time, dose_speed, dry_level
|
||||||
index = BUTTONS.index(pin)
|
index = BUTTONS.index(pin)
|
||||||
label = LABELS[index]
|
label = LABELS[index]
|
||||||
if label == 'A': # Test
|
if label == "A": # Test
|
||||||
logging.info("Manual watering triggered.")
|
logging.info("Manual watering triggered.")
|
||||||
p.dose(dose_speed, dose_time, blocking=False)
|
p.dose(dose_speed, dose_time, blocking=False)
|
||||||
last_dose = time.time()
|
last_dose = time.time()
|
||||||
|
|
||||||
if label == 'B': # Switch setting
|
if label == "B": # Switch setting
|
||||||
mode += 1
|
mode += 1
|
||||||
mode %= 3 # Wrap 0, 1, 2 (Time, Speed, Dry level)
|
mode %= 3 # Wrap 0, 1, 2 (Time, Speed, Dry level)
|
||||||
|
|
||||||
if label == 'Y': # Inc. setting
|
if label == "Y": # Inc. setting
|
||||||
if mode == 0:
|
if mode == 0:
|
||||||
dose_time += 0.01
|
dose_time += 0.01
|
||||||
logging.info("Dose time increased to: {:.2f}".format(dose_time))
|
logging.info("Dose time increased to: {:.2f}".format(dose_time))
|
||||||
@@ -97,7 +93,7 @@ def handle_button(pin):
|
|||||||
dry_level += 0.01
|
dry_level += 0.01
|
||||||
logging.info("Dry level increased to: {:.2f}".format(dry_level))
|
logging.info("Dry level increased to: {:.2f}".format(dry_level))
|
||||||
|
|
||||||
if label == 'X': # Dec. setting
|
if label == "X": # Dec. setting
|
||||||
if mode == 0:
|
if mode == 0:
|
||||||
dose_time -= 0.01
|
dose_time -= 0.01
|
||||||
logging.info("Dose time decreased to: {:.2f}".format(dose_time))
|
logging.info("Dose time decreased to: {:.2f}".format(dose_time))
|
||||||
@@ -132,36 +128,76 @@ try:
|
|||||||
# has had the opportunity to catch up.
|
# has had the opportunity to catch up.
|
||||||
if avg_saturation < dry_level and (time.time() - last_dose) > DOSE_FREQUENCY:
|
if avg_saturation < dry_level and (time.time() - last_dose) > DOSE_FREQUENCY:
|
||||||
p.dose(dose_speed, dose_time)
|
p.dose(dose_speed, dose_time)
|
||||||
logging.info("Auto watering. Saturation: {:.2f} (Dry: {:.2f})".format(avg_saturation, dry_level))
|
logging.info(
|
||||||
|
"Auto watering. Saturation: {:.2f} (Dry: {:.2f})".format(
|
||||||
|
avg_saturation, dry_level
|
||||||
|
)
|
||||||
|
)
|
||||||
last_dose = time.time()
|
last_dose = time.time()
|
||||||
|
|
||||||
draw.rectangle((0, 0, display.width, display.height), (0, 0, 0))
|
draw.rectangle((0, 0, display.width, display.height), (0, 0, 0))
|
||||||
|
|
||||||
# Current and average saturation
|
# Current and average saturation
|
||||||
draw.text((5 + display.width // 2, 16), "Sat: {:.3f}".format(current_saturation), font=font, fill=(255, 255, 255))
|
draw.text(
|
||||||
draw.text((5 + display.width // 2, 32), "AVG: {:.3f}".format(avg_saturation), font=font, fill=(255, 255, 255))
|
(5 + display.width // 2, 16),
|
||||||
|
"Sat: {:.3f}".format(current_saturation),
|
||||||
|
font=font,
|
||||||
|
fill=(255, 255, 255),
|
||||||
|
)
|
||||||
|
draw.text(
|
||||||
|
(5 + display.width // 2, 32),
|
||||||
|
"AVG: {:.3f}".format(avg_saturation),
|
||||||
|
font=font,
|
||||||
|
fill=(255, 255, 255),
|
||||||
|
)
|
||||||
|
|
||||||
# Selected setting box
|
# Selected setting box
|
||||||
draw.rectangle((0, 16 + (16 * mode), display.width // 2, 31 + (16 * mode)), (30, 30, 30))
|
draw.rectangle(
|
||||||
|
(0, 16 + (16 * mode), display.width // 2, 31 + (16 * mode)), (30, 30, 30)
|
||||||
|
)
|
||||||
|
|
||||||
draw.text((5, 16), "Time: {:.2f}".format(dose_time), font=font, fill=(255, 255, 255) if mode == 0 else (128, 128, 128))
|
draw.text(
|
||||||
draw.text((5, 32), "Speed: {:.2f}".format(dose_speed), font=font, fill=(255, 255, 255) if mode == 1 else (128, 128, 128))
|
(5, 16),
|
||||||
draw.text((5, 48), "Dry lvl: {:.2f}".format(dry_level), font=font, fill=(255, 255, 255) if mode == 2 else (128, 128, 128))
|
"Time: {:.2f}".format(dose_time),
|
||||||
|
font=font,
|
||||||
|
fill=(255, 255, 255) if mode == 0 else (128, 128, 128),
|
||||||
|
)
|
||||||
|
draw.text(
|
||||||
|
(5, 32),
|
||||||
|
"Speed: {:.2f}".format(dose_speed),
|
||||||
|
font=font,
|
||||||
|
fill=(255, 255, 255) if mode == 1 else (128, 128, 128),
|
||||||
|
)
|
||||||
|
draw.text(
|
||||||
|
(5, 48),
|
||||||
|
"Dry lvl: {:.2f}".format(dry_level),
|
||||||
|
font=font,
|
||||||
|
fill=(255, 255, 255) if mode == 2 else (128, 128, 128),
|
||||||
|
)
|
||||||
|
|
||||||
# Button lavel backgrounds
|
# Button lavel backgrounds
|
||||||
draw.rectangle((0, 0, 42, 14), (255, 255, 255))
|
draw.rectangle((0, 0, 42, 14), (255, 255, 255))
|
||||||
draw.rectangle((display.width - 15, 0, display.width, 14), (255, 255, 255))
|
draw.rectangle((display.width - 15, 0, display.width, 14), (255, 255, 255))
|
||||||
draw.rectangle((display.width - 15, display.height - 14, display.width, display.height), (255, 255, 255))
|
draw.rectangle(
|
||||||
|
(display.width - 15, display.height - 14, display.width, display.height),
|
||||||
|
(255, 255, 255),
|
||||||
|
)
|
||||||
draw.rectangle((0, display.height - 14, 42, display.height), (255, 255, 255))
|
draw.rectangle((0, display.height - 14, 42, display.height), (255, 255, 255))
|
||||||
|
|
||||||
# Button labels
|
# Button labels
|
||||||
draw.text((5, 0), "test", font=font, fill=(0, 0, 0))
|
draw.text((5, 0), "test", font=font, fill=(0, 0, 0))
|
||||||
draw.text((5, display.height - 16), "next", font=font, fill=(0, 0, 0))
|
draw.text((5, display.height - 16), "next", font=font, fill=(0, 0, 0))
|
||||||
draw.text((display.width - 10, 0), "-", font=font, fill=(0, 0, 0))
|
draw.text((display.width - 10, 0), "-", font=font, fill=(0, 0, 0))
|
||||||
draw.text((display.width - 12, display.height - 15), "+", font=font, fill=(0, 0, 0))
|
draw.text(
|
||||||
|
(display.width - 12, display.height - 15), "+", font=font, fill=(0, 0, 0)
|
||||||
|
)
|
||||||
|
|
||||||
display.display(image)
|
display.display(image)
|
||||||
time.sleep(1.0 / FPS)
|
time.sleep(1.0 / FPS)
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("Dose Time: {:.2f} Dose Speed: {:.2f}, Dry Level: {:.2f}".format(dose_time, dose_speed, dry_level))
|
print(
|
||||||
|
"Dose Time: {:.2f} Dose Speed: {:.2f}, Dry Level: {:.2f}".format(
|
||||||
|
dose_time, dose_speed, dry_level
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
channel1:
|
|
||||||
water_level: 0.8
|
|
||||||
warn_level: 0.2
|
|
||||||
pump_speed: 0.7
|
|
||||||
pump_time: 0.7
|
|
||||||
auto_water: False
|
|
||||||
icon: icons/flat-4.png
|
|
||||||
channel2:
|
|
||||||
water_level: 0.8
|
|
||||||
warn_level: 0.2
|
|
||||||
pump_speed: 0.7
|
|
||||||
pump_time: 0.7
|
|
||||||
auto_water: False
|
|
||||||
channel3:
|
|
||||||
water_level: 0.8
|
|
||||||
warn_level: 0.2
|
|
||||||
pump_speed: 0.7
|
|
||||||
pump_time: 0.7
|
|
||||||
auto_water: False
|
|
||||||
general:
|
|
||||||
alarm_enable: True
|
|
||||||
alarm_interval: 1.0
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
Grow
|
|
||||||
=======
|
|
||||||
|
|
||||||
Designed for looking after plants, Grow monitors moisture levels and runs simple pumps to water plants. Learn more -
|
|
||||||
https://shop.pimoroni.com/products/grow
|
|
||||||
|
|
||||||
|Build Status| |Coverage Status| |PyPi Package| |Python Versions|
|
|
||||||
|
|
||||||
Installing
|
|
||||||
==========
|
|
||||||
|
|
||||||
The one-line installer enables the correct interfaces,
|
|
||||||
|
|
||||||
One-line (Installs from GitHub)
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
curl -sSL https://get.pimoroni.com/grow | bash
|
|
||||||
|
|
||||||
**Note** report issues with one-line installer here:
|
|
||||||
https://github.com/pimoroni/get
|
|
||||||
|
|
||||||
Or... Install and configure dependencies from GitHub:
|
|
||||||
-----------------------------------------------------
|
|
||||||
|
|
||||||
- ``git clone https://github.com/pimoroni/grow-python``
|
|
||||||
- ``cd grow-python``
|
|
||||||
- ``sudo ./install.sh``
|
|
||||||
|
|
||||||
**Note** Raspbian Lite users may first need to install git:
|
|
||||||
``sudo apt install git``
|
|
||||||
|
|
||||||
Or... Install from PyPi and configure manually:
|
|
||||||
-----------------------------------------------
|
|
||||||
|
|
||||||
- Run ``sudo pip install grow``
|
|
||||||
|
|
||||||
**Note** this wont perform any of the required configuration changes on
|
|
||||||
your Pi, you may additionally need to:
|
|
||||||
|
|
||||||
- Enable i2c: ``raspi-config nonint do_i2c 0``
|
|
||||||
- Enable SPI: ``raspi-config nonint do_spi 0``
|
|
||||||
|
|
||||||
And install additional dependencies:
|
|
||||||
|
|
||||||
::
|
|
||||||
|
|
||||||
sudo apt install python-numpy python-smbus python-pil python-setuptools
|
|
||||||
|
|
||||||
Help & Support
|
|
||||||
--------------
|
|
||||||
|
|
||||||
- GPIO Pinout - https://pinout.xyz/pinout/grow
|
|
||||||
- Support forums - http://forums.pimoroni.com/c/support
|
|
||||||
- Discord - https://discord.gg/hr93ByC
|
|
||||||
|
|
||||||
.. |Build Status| image:: https://travis-ci.com/pimoroni/grow-python.svg?branch=master
|
|
||||||
:target: https://travis-ci.com/pimoroni/grow-python
|
|
||||||
.. |Coverage Status| image:: https://coveralls.io/repos/github/pimoroni/grow-python/badge.svg?branch=master
|
|
||||||
:target: https://coveralls.io/github/pimoroni/grow-python?branch=master
|
|
||||||
.. |PyPi Package| image:: https://img.shields.io/pypi/v/growhat.svg
|
|
||||||
:target: https://pypi.python.org/pypi/growhat
|
|
||||||
.. |Python Versions| image:: https://img.shields.io/pypi/pyversions/growhat.svg
|
|
||||||
:target: https://pypi.python.org/pypi/growhat
|
|
||||||
|
|
||||||
0.0.1
|
|
||||||
-----
|
|
||||||
|
|
||||||
* Initial Release
|
|
||||||
@@ -5,7 +5,8 @@ version = 0.0.1
|
|||||||
author = Philip Howard, Paul Beech
|
author = Philip Howard, Paul Beech
|
||||||
author_email = paul@pimoroni.com
|
author_email = paul@pimoroni.com
|
||||||
description = Grow HAT Mini. A plant valet add-on for the Raspberry Pi
|
description = Grow HAT Mini. A plant valet add-on for the Raspberry Pi
|
||||||
long_description = file: README.rst
|
long_description = file: README.md
|
||||||
|
long_description_content_type = text/markdown
|
||||||
keywords = Raspberry Pi
|
keywords = Raspberry Pi
|
||||||
url = https://www.pimoroni.com
|
url = https://www.pimoroni.com
|
||||||
project_urls =
|
project_urls =
|
||||||
@@ -48,14 +49,14 @@ ignore =
|
|||||||
[pimoroni]
|
[pimoroni]
|
||||||
py2deps =
|
py2deps =
|
||||||
python-pip
|
python-pip
|
||||||
python-numpy
|
python-yaml
|
||||||
python-smbus
|
python-smbus
|
||||||
python-pil
|
python-pil
|
||||||
python-spidev
|
python-spidev
|
||||||
python-rpi.gpio
|
python-rpi.gpio
|
||||||
py3deps =
|
py3deps =
|
||||||
python3-pip
|
python3-pip
|
||||||
python3-numpy
|
python3-yaml
|
||||||
python3-smbus
|
python3-smbus
|
||||||
python3-pil
|
python3-pil
|
||||||
python3-spidev
|
python3-spidev
|
||||||
|
|||||||
@@ -22,12 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|||||||
SOFTWARE.
|
SOFTWARE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from setuptools import setup, __version__
|
from setuptools import setup
|
||||||
from pkg_resources import parse_version
|
|
||||||
|
|
||||||
minimum_version = parse_version('30.4.0')
|
|
||||||
|
|
||||||
if parse_version(__version__) < minimum_version:
|
|
||||||
raise RuntimeError("Package setuptools must be at least version {}".format(minimum_version))
|
|
||||||
|
|
||||||
setup()
|
setup()
|
||||||
|
|||||||
@@ -70,12 +70,3 @@ def atexit():
|
|||||||
sys.modules['atexit'] = atexit
|
sys.modules['atexit'] = atexit
|
||||||
yield atexit
|
yield atexit
|
||||||
del sys.modules['atexit']
|
del sys.modules['atexit']
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='function', autouse=False)
|
|
||||||
def numpy():
|
|
||||||
"""Mock numpy module."""
|
|
||||||
numpy = mock.MagicMock()
|
|
||||||
sys.modules['numpy'] = numpy
|
|
||||||
yield numpy
|
|
||||||
del sys.modules['numpy']
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ def test_moisture_setup(GPIO, smbus):
|
|||||||
def test_moisture_read_all(GPIO, smbus):
|
def test_moisture_read_all(GPIO, smbus):
|
||||||
from grow import moisture
|
from grow import moisture
|
||||||
moisture._is_setup = False
|
moisture._is_setup = False
|
||||||
|
|
||||||
result = moisture.read_all()
|
result = moisture.read_all()
|
||||||
|
|
||||||
assert type(result(1)) == float
|
assert type(result(1)) == float
|
||||||
|
|||||||
Reference in New Issue
Block a user