Watering your plants automatically

This summer, I had some plants on my balcony, which I wanted to be watered automatically.

I didn’t want to use any sensors, so I was using the temperature and the weather conditions from OpenWeatherMap (at 6AM) to determine the approximate amount of the required water.

I was using the following algorithm to switch the pump for the watering on and off with a FRITZ!DECT 210, but you easily can adapt it to any other infrastructure you want, I think.

#!/usr/bin/python
import sys
import os
import requests
import json
import time
from datetime import date

try:
response = requests.get("http://api.openweathermap.org/data/2.5/weather?id=YOURCITYID&appid=YOURPRIVATEKEY")
x = response.json()

if x['cod'] != '404':
y = x['main']
temp = y['temp']
temp_float = float(temp)
temp_float = temp_float-273.15
temp_float = round(temp_float, 1)
if temp_float == -0.0:
temp_float = 0.0
z = x['weather']
icon = z[0]['icon']

else:
temp_float = 18.0
icon = '01d'

except:
temp_float = 18.0
icon = '01d'

#adjust the amount of the water, depending of the temperature (it's average summer sunrise time at the moment!)
#180s = 1l of water
timer = 270 # 1,5l
if temp_float >= 16.0:
timer = 360 # 2,0l
if temp_float >= 20.0:
timer = 450 # 2,5l

#heavy clouds/overcast, reduce the amount of the water a bit
if icon == '04d' or icon == '04n':
timer = timer*0.75
#rain/thunderstorm, reduce the amount of the water to the minimum
elif icon == '09d' or icon == '09n' or icon == '10d' or icon == '10n' or icon == '11d' or icon == '11n':
timer = timer*0.5

str_on = ""
str_off = ""

while str_on != '1\n':
process = os.popen('sudo ./fritz_on.sh')
str_on = process.read()
process.close()

time.sleep(timer)

while str_off != '0\n':
process = os.popen('sudo ./fritz_off.sh')
str_off = process.read()
process.close()

#write the values to the logfile
today = date.today()
water = timer*0.00556
today_str = str(today)
icon_str = str(icon)
temp_str = str(temp_float)
water_str = str(water)
line = today_str + "," + icon_str + "," + temp_str + "," + water_str + "\n"
f = open('logfile.csv','a')
f.write(line)
f.close()

For documenting the watering process, I was using Gnuplot. The following picture shows an example of a typical diagram.

Raspberry Pi Zero W + Pimoroni Scroll pHAT HD = IoT Display

For a long time already, I always wanted to have some kind of display that permanently shows me different information. – Information that is useful for me personally. Recently there are a lot of new display technologies, like OLED and electronic paper display, but also conventional multi line LCDs became cheaper and cheaper during the last years. But most of these displays are meant to be read from a short distance, and my idea of such an information display was different. I wanted it to be:

Readable from all over the apartment

Convenient to read in all light conditions

While reading the latest news about the Raspberry Pi, I some when found out that Pimoroni has released a LED display for the Raspberry Pi, called Scroll pHAT HD. That display looked like it was exactly what I was waiting for. Some may say that scrolling LED displays are a bit old fashioned, but in my opinion they are not. When I’m sitting at the dinning table in the morning and having breakfast, reading the display is fun and it reassures¬† me that the Scroll pHAT HD is just the right display for my purpose.

So, what to finally show on such a display? Actually, there’s not too much to think about this in my case, a small and compact summary of my weather station website, of course! ūüôā I came to the conclusion that the following information would be most useful and interesting for me:

Current weekday, date and time

Indoor temperature and indoor air quality

Outdoor weather data and conditions

Local gas price

Weekday, date and time are easy to get in Python, indoor temperature comes from my FRITZ!DECT 200, air quality from my air quality sensor via ThingSpeak, outdoor weather data and conditions from Weather Underground and the local gas price is provided by Tankerk√∂nig. Here we go…

Because of it’s compact size and the built in WI-FI, I connected the Scroll pHAT HD to a Raspberry Pi Zero W. In front of the display, I’ve placed a custom made diffusor foil for better readability. The foil was the front of a binder that I found in an office supply store, simple but very efficient. The Raspberry Pi Zero W has a small plastic case and sits on an original Raspberry Pi power supply, directly connected to a power outlet (you may also want to see the 3D printed case from this guy, which holds the Raspberry Pi Zero W and the Scroll pHAT HD within one enclosure). In the video below, you can see the finished result. The script that is used runs every ten minutes and displays the information six times in a row. Except weekday, date and time, all the other information is only gathered once¬†(at the beginning of the script) and it does not change during the ten minutes time period.

The script that I use is based on the advanced scrolling example from Pimoroni. This is already a good starting point for your own scripts. I especially like that you can put connected information into a single line, which then, after the horizontal scrolling of the line has finished, scrolls vertically to the next line of connected information.

But like usual, quite a bit of extra work on the script was required, until it finally worked in the way, it was supposed to. First of all, it is not possible to use mutated vowels (or other Unicode characters) within Python 2.7 without taking care of the encoding. There are a lot of discussions about that topic on the Internet and I ended up with doing something that actually should no be done, because I was just not able to find a better and cleaner solution that actually works. So, I’m aware that there still could be a bit room for improvement here.

Also, the font file that is originally provided by Pimoroni lacks of some characters that I want to display, that means that I had to modify it to show my own, custom characters/symbols, like the smileys, the superscript nine and the Euro sign. Actually this is not very hard to do. You just have to find an unused character in the font that you want to turn into a custom one and then edit the corresponding entry in the font file font5x7.py. If you are using Notepad++ and select ‘0xff’ somewhere, it will automatically highlight all other occurrences of ‘0xff’. This will help you to clearly see and modify the character.

Euro

For using the additional characters from the font file in the text you want to show on the display, you have to use unichr() in the code.

The major problem when adapting the script  for my purpose was actually, that the length of the text to display is not always the same. Since the weather conditions can be different, the total amount of the characters (responsively the total amount of the horizontal pixel) can vary. This means that the scrolling speed has to be a bit faster for longer texts and a bit slower for shorter texts, so that it can be displayed six times within the ten minutes time period, without having different waiting times at the end.

One should think that the scrolling speed and the total amount of the horizontal pixel are linearly dependent, but after a lot of trial and error, I have learned that this is not the case. Finally, I ended up with making some real world tests with different texts and deviating an equation that describes the connection between the total amount of he horizontal pixel and the necessary delay (responsively  the scrolling speed). All in all, it was a bit tricky, but I think the result is quite satisfying.

Graph

Below, you can see the entire script for the information display. It includes gathering all the information and displaying it in the way that is shown in the video above. The script also includes translation tables for the weather conditions and for the weekday that you can remove, if English is your first language. Of course, you can also use the tables to make your own translations into a different language than German.

Please note, that for reading the indoor temperature, the Bash script from the article ‘Reading the temperature from FRITZ!DECT devices’ is necessary. Additionally, YOURCHANNELID, YOURTHINGSPEAKAPIKEY, YOURAPIKEY, YOURFIELDNAME, YOURAPIKEYHERE, YOURSTATIONIDHERE, and DESIREDSTATIONID have to be replaced with your personal information.

#!/usr/bin/env python
# encoding: utf-8
import sys
import os
import shutil
import time
import urllib2
import json
import unicodedata
import scrollphathd
from scrollphathd.fonts import font5x7


reload(sys)
sys.setdefaultencoding("utf-8")


# Get the temperature from the FRITZ!DECT 200
try:
    process = os.popen('sudo ./fritztemp.sh')
    fritztemp = process.read()
    process.close()

    fritztemp = fritztemp.replace(',', '.')
    fritztemp = fritztemp.replace('\n', '')
except:
    fritztemp = '-'

indoortemp = 'Innentemperatur: ' + fritztemp + ' ' + unichr(20) + 'C'


# Get the air quality from ThingSpeak
try:
    f = urllib2.urlopen('http://api.thingspeak.com/channels/YOURCHANNELID/feeds/last.json?api_key=YOURTHINGSPEAKAPIKEY')
    json_string = f.read()
    parsed_json = json.loads(json_string)
    ppm = parsed_json['YOURFIELDNAME']
    f.close()

    ppm_float = float(ppm)

    smiley = ''
    if (ppm_float < 1000):
       smiley = ' ' + unichr(40) + unichr(2) + unichr(41)
    elif (ppm_float >= 1000 and ppm_float < 1500):
       smiley = ' ' + unichr(40) + unichr(3) + unichr(41)
    elif (ppm_float >= 1500):
       smiley = ' ' + unichr(40) + unichr(4) + unichr(41)
except:
    ppm = '-'
    smiley = ''

airquality = 'Luftqualit' + unichr(145) + 't: ' + ppm + ' ppm' + smiley


# Get the weather data from Weather Underground
try:
    f = urllib2.urlopen('http://api.wunderground.com/api/YOURAPIKEYHERE/geolookup/conditions/q/pws:YOURSTATIONIDHERE.json')
    json_string = f.read()
    parsed_json = json.loads(json_string)
    temp_c = parsed_json['current_observation']['temp_c']
    relative_humidity = parsed_json['current_observation']['relative_humidity']
    relative_humidity = relative_humidity.replace('%', '')
    pressure_in = parsed_json['current_observation']['pressure_in']
    pressure_trend = parsed_json['current_observation']['pressure_trend']
    conditions = parsed_json['current_observation']['weather']
    f.close()

    temp_str = '%.1f' % temp_c
    press_float = float(pressure_in)
    press_float = press_float*33.8637526
    press_str =  '%.1f' % press_float

    trend = ''
    if pressure_trend == '0':
      trend = ' ' + unichr(28)
    elif pressure_trend == '+':
      trend = ' ' + unichr(30)
    elif pressure_trend == '-':
      trend = ' ' + unichr(31)

    conditions = conditions.replace('Light ', '')
    conditions = conditions.replace('Heavy ', '')

    # Make a German translation
    if conditions == 'Drizzle':
      conditions = conditions.replace('Drizzle', 'Nieselregen')
    elif conditions == 'Rain':
      conditions = conditions.replace('Rain', 'Regen')
    elif conditions == 'Snow':
      conditions = conditions.replace('Snow', 'Schnee')
    elif conditions == 'Snow Grains':
      conditions = conditions.replace('Snow Grains', 'Griesel')
    elif conditions == 'Ice Crystals':
      conditions = conditions.replace('Ice Crystals', 'Eisgl' + unichr(145) + 'tte')
    elif conditions == 'Ice Pellets':
      conditions = conditions.replace('Ice Pellets', 'Graupel')
    elif conditions == 'Hail':
      conditions = conditions.replace('Hail', 'Hagel')
    elif conditions == 'Mist':
      conditions = conditions.replace('Mist', 'Feuchter Dunst')
    elif conditions == 'Fog':
      conditions = conditions.replace('Fog', 'Nebel')
    elif conditions == 'Fog Patches':
      conditions = conditions.replace('Fog Patches', 'Nebelschwaden')
    elif conditions == 'Smoke':
      conditions = conditions.replace('Smoke', 'Rauch')
    elif conditions == 'Volcanic Ash':
      conditions = conditions.replace('Volcanic Ash', 'Vulkanasche')
    elif conditions == 'Widespread Dust':
      conditions = conditions.replace('Widespread Dust', 'Verbreitet staubig')
    elif conditions == 'Sand':
      conditions = conditions.replace('Sand', 'Sandig')
    elif conditions == 'Haze':
      conditions = conditions.replace('Haze', 'Dunst')
    elif conditions == 'Spray':
      conditions = conditions.replace('Spray', 'Spr' + unichr(177) + 'hregen')
    elif conditions == 'Dust Whirls':
      conditions = conditions.replace('Dust Whirls', 'Wirbelwind')
    elif conditions == 'Sandstorm':
      conditions = conditions.replace('Sandstorm', 'Sandsturm')
    elif conditions == 'Low Drifting Snow':
      conditions = conditions.replace('Low Drifting Snow', 'Schneeverwehungen')
    elif conditions == 'Low Drifting Widespread Dust':
      conditions = conditions.replace('Low Drifting Widespread Dust', 'Staubverwehungen')
    elif conditions == 'Low Drifting Sand':
      conditions = conditions.replace('Low Drifting Sand', 'Sandverwehungen')
    elif conditions == 'Blowing Snow':
      conditions = conditions.replace('Blowing Snow', 'Schneetreiben')
    elif conditions == 'Blowing Widespread Dust':
      conditions = conditions.replace('Blowing Widespread Dust', 'Staubtreiben')
    elif conditions == 'Blowing Sand':
      conditions = conditions.replace('Blowing Sand', 'Sandtreiben')
    elif conditions == 'Rain Mist':
      conditions = conditions.replace('Rain Mist', 'Spr' + unichr(177) + 'hregen')
    elif conditions == 'Rain Showers':
      conditions = conditions.replace('Rain Showers', 'Regenschauer')
    elif conditions == 'Snow Showers':
      conditions = conditions.replace('Snow Showers', 'Schneeschauer')
    elif conditions == 'Snow Blowing Snow Mist':
      conditions = conditions.replace('Snow Blowing Snow Mist', 'Schneegest' + unichr(169) + 'ber')
    elif conditions == 'Ice Pellet Showers':
      conditions = conditions.replace('Ice Pellet Showers', 'Graupelschauer')
    elif conditions == 'Hail Showers':
      conditions = conditions.replace('Hail Showers', 'Hagelschauer')
    elif conditions == 'Small Hail Showers':
      conditions = conditions.replace('Small Hail Showers', 'Kleink' + unichr(169) + 'rniger Hagelschauer')
    elif conditions == 'Thunderstorm':
      conditions = conditions.replace('Thunderstorm', 'Gewitter')
    elif conditions == 'Thunderstorms and Rain':
      conditions = conditions.replace('Thunderstorms and Rain', 'Gewitter und Regen')
    elif conditions == 'Thunderstorms and Snow':
      conditions = conditions.replace('Thunderstorms and Snow', 'Gewitter und Schnee')
    elif conditions == 'Thunderstorms and Ice Pellets':
      conditions = conditions.replace('Thunderstorms and Ice Pellets', 'Gewitter und Graupel')
    elif conditions == 'Thunderstorms with Hail':
      conditions = conditions.replace('Thunderstorms with Hail', 'Gewitter mit Hagel')
    elif conditions == 'Thunderstorms with Small Hail':
      conditions = conditions.replace('Thunderstorms with Small Hail', 'Gewitter mit kleink' + unichr(169) + 'rnigem Hagel')
    elif conditions == 'Freezing Drizzle':
      conditions = conditions.replace('Freezing Drizzle', 'Ueberfrierender Nieselregen')
    elif conditions == 'Freezing Rain':
      conditions = conditions.replace('Freezing Rain', 'Ueberfrierender Regen')
    elif conditions == 'Freezing Fog':
      conditions = conditions.replace('Freezing Fog', 'Ueberfrierender Nebel')
    elif conditions == 'Patches of Fog':
      conditions = conditions.replace('Patches of Fog', 'Nebelfelder')
    elif conditions == 'Shallow Fog':
      conditions = conditions.replace('Shallow Fog', 'Bodennebel')
    elif conditions == 'Partial Fog':
      conditions = conditions.replace('Partial Fog ', 'Teilweise neblig')
    elif conditions == 'Overcast':
      conditions = conditions.replace('Overcast', 'Bedeckt')
    elif conditions == 'Clear':
      conditions = conditions.replace('Clear', 'Klar')
    elif conditions == 'Partly Cloudy':
      conditions = conditions.replace('Partly Cloudy', 'Teilweise bew' + unichr(169) + 'lkt')
    elif conditions == 'Mostly Cloudy':
      conditions = conditions.replace('Mostly Cloudy', 'Meist bew' + unichr(169) + 'lkt')
    elif conditions == 'Cloudy':
      conditions = conditions.replace('Cloudy', 'Bew' + unichr(169) + 'lkt')
    elif conditions == 'Scattered Clouds':
      conditions = conditions.replace('Scattered Clouds', 'Aufgelockerte Bew' + unichr(169) + 'lkung')
    elif conditions == 'Small Hail':
      conditions = conditions.replace('Small Hail', 'Graupel')
    elif conditions == 'Squalls':
      conditions = conditions.replace('Squalls', 'Sturmb' + unichr(169) + 'en')
    elif conditions == 'Funnel Cloud':
      conditions = conditions.replace('Funnel Cloud', 'Trichterwolke')
    elif conditions == 'Unknown Precipitation':
      conditions = conditions.replace('Unknown Precipitation', 'Unbekannter Niederschlag')
    elif conditions == 'Unknown':
      conditions = conditions.replace('Unknown', 'Unbekannt')
except:
    temp_str = '-'
    relative_humidity = '-'
    press_str = '-'
    trend = ''
    conditions = '-'

weather = 'Aussentemperatur: ' + temp_str + ' ' + unichr(20) + 'C -' + ' Aussenluftfeuchtigkeit: ' + relative_humidity + ' % -' + ' Relativer Luftdruck: ' + press_str + ' hPa' + trend + ' -' + ' Wetterbedingungen: '  + conditions


# Get the gas price from Tankerkönig
try:
    f = urllib2.urlopen('https://creativecommons.tankerkoenig.de/json/detail.php?id=DESIREDSTATIONID&apikey=YOURAPIKEY')
    json_string = f.read()
    parsed_json = json.loads(json_string)
    gas = parsed_json['station']['e5']
    f.close()

    gas_str = '%.2f' % gas
except:
    gas_str = '-'

if gas_str == '-':
    gasprice = 'Benzinpreis Super E5: ' + gas_str + ' ' + unichr(91)
else:
    gasprice = 'Benzinpreis Super E5: ' + gas_str + unichr(36) + ' ' + unichr(91)


space = '    '
hyphen = ' - '
indoor = indoortemp + hyphen + airquality + space
weather = weather + space
gasprice = gasprice + space


# Show the thext on the display
for x in range(0, 6):
    try:
        # Get the weekday, date and time
        datetime = time.strftime('%A, %d.%m.%Y - %H:%M Uhr')
        # Make a German translation
        datetime = datetime.replace('Monday', 'Montag')
        datetime = datetime.replace('Tuesday', 'Dienstag')
        datetime = datetime.replace('Wednesday', 'Mittwoch')
        datetime = datetime.replace('Thursday', 'Donnerstag')
        datetime = datetime.replace('Friday', 'Freitag')
        datetime = datetime.replace('Saturday', 'Samstag')
        datetime = datetime.replace('Sunday', 'Sonntag')

        datetime = datetime + space

        scrollphathd.clear()
        scrollphathd.set_brightness(0.2)

        lines = [datetime,
                 indoor,
                 weather,
                 gasprice]

        line_height = scrollphathd.DISPLAY_HEIGHT + 2
        offset_left = 0

        lengths = [0] * len(lines)
        for line, text in enumerate(lines):
            lengths[line] = scrollphathd.write_string(text, x=offset_left, y=line_height * line, font=euro5x7)
            offset_left += lengths[line]

        scrollphathd.set_pixel(offset_left - 1, (len(lines) * line_height) - 1, 0)

        period = 96
        length = lengths[0] + lengths[1] + lengths[2] + lengths[3]
        # Factor bigger  -> Delay shorter -> Text runs faster
        # Factor smaller -> Delay longer  -> Text runs slower
        delay = period/(length*float(2.8))
        # The relationship between the amount of horizontal pixels and the scroll time is not linearly dependent 
        # Therefore, do a correction based on meassurements to adapt it to the conditions observed in reality
        delay = ((-0.0013889*length)+2.83472)*delay

        scrollphathd.scroll_to(0, 0)
        scrollphathd.show()

        # Scroll the text
        for current_line, line_length in enumerate(lengths):
            # Delay a slightly longer time at the start of each line
            time.sleep(delay*10)

            # Scroll to the end of the current line
            for x in range(line_length):
                scrollphathd.scroll(1, 0)
                scrollphathd.show()
                time.sleep(delay)

            if current_line != len(lines) - 1:
                # Progress to the next line by scrolling upwards
                for y in range(line_height):
                    scrollphathd.scroll(0, 1)
                    scrollphathd.show()
                    time.sleep(delay)
    except:
        scrollphathd.clear()
        sys.exit(-1)


scrollphathd.clear()

Reading the temperature from FRITZ!DECT devices

If you own a FRITZ!Box router from the German company AVM that has an integrated DECT base station, you are able to connect devices from their FRITZ!DECT accessory line, like switchable power outlets and radiator controller via DECT.

The FRITZ!DECT 200, 210 and 300 are also equipped with a temperature sensor. With a recent firmware for the FRITZ!Box router, AVM now shows you a temperature diagram for the past 24 hours. Unfortunately, it only seems to be for informational purposes and it is not possible to download the temperature data from the router. Also, the data will be lost after you restart your router.

fritztemp

But what is not very well-known is that AVM provides access to the connected devices via their own Home Automation HTTP Interface. Once, you are logging in to the FRITZ!Box, using a Session-ID, you are able to use the mentioned HTTP interface. By using this interface, you are not only able to read the temperature from the FRITZ!DECT devices, you are also able to read a variety of other information and you are able to turn the power of the power outlets on and off and to control the temperature of the radiation controller. So, actually you could design your own personal Home Automation System, using the interface on your Raspberry Pi if you want. Since I am using the MyFRITZ!App provided by AVM to control the devices, I am mainly interested to only read the temperature. With the following Bash script, it is very easy to periodically read out the temperature from a connected FRITZ!DECT device. You just have to fill in the password of your FRITZ!Box and the unique AIN of your device, which is shown in the ‘Smart Home’ menue of the FRITZ!Box.

#!/bin/bash
# -----------
# definitions
# -----------
FBF="http://192.168.178.1/"
USER="root"
PASS="YOURFRITZBOXPASSWORD"
AIN="AINOFYOURFRITZDECTDEVICE"
# ---------------
# fetch challenge
# ---------------
CHALLENGE=$(curl -s "${FBF}/login_sid.lua" | grep -Po '(?<=<Challenge>).*(?=</Challenge>)')
# -----
# login
# -----
MD5=$(echo -n ${CHALLENGE}"-"${PASS} | iconv -f ISO8859-1 -t UTF-16LE | md5sum -b | awk '{print substr($0,1,32)}')
RESPONSE="${CHALLENGE}-${MD5}"
SID=$(curl -i -s -k -d "response=${RESPONSE}&username=${USER}" "${FBF}" | grep -Po -m 1 '(?<=sid=)[a-f\d]+')
# -----------------
# fetch temperature
# -----------------
TEMPINT=$(curl -s ${FBF}'/webservices/homeautoswitch.lua?ain='${AIN}'&switchcmd=gettemperature&sid='${SID})
TEMPFLOAT=$(echo $TEMPINT | sed 's/\B[0-9]\{1\}\>/,&/')
# ------------------
# output temperature
# ------------------
echo $TEMPFLOAT

It is also relatively easy to call the script within a python script. Supposed, you have saved the bash script under the name ‘fritztemp.sh’, you can use the following python code as a wrapper which you can include in your own scripts.

#!/usr/bin/python

import sys
import os

# read the temperature from the fritz device
process = os.popen('sudo ./fritztemp.sh')
str = process.read()
process.close()

if not str:
  sys.exit(1)

str = str.replace(',','.')
str = str.replace('\n','')

print str

I use the code shown here to read the temperature from one of my FRITZ!DECT200 devices on the lower floor of my apartment, store it in a RRDtool database and generate a graph, together with the temperature on the upper floor which is measured by a Bosch BMP180 pressure sensor.

insidetemp

You can easily see that the trend of the graph is exactly the same like it is in the diagram showed by the FRITZ!Box router itself.