The Zambretti Forecaster, an alternative weather forecast

If you want to try a different approach for a weather forecast than my simple weather forecast, based on the barometric pressure history, then you could use the so called Zambretti Forecaster. The Zambretti Forecaster is a weather forecasting tool in the form of a circular slide rule that was introduced by the company Negretti and Zambra in 1920. A more detailed description about the functionality of the Zambretti Forecaster and how to put it into code can be found here.

Below, you can find my Python implementation of the Zambretti Forecaster algorithm. It uses the current barometric pressure, the trend of the barometric pressure (over the last six hours) and the current month of the year as an input. The output of the Zambretti Forecaster is one of 26 different weather conditions. I mapped these weather conditions to a set of four forecast icons (sun, cloud, rain and thunderstorm), to make it more simple. The forecast icon is then shown together with an icon (arrow) that represents the current trend of the barometric pressure. Of course, you could also use more than these four icons, to make the forecast better if you want.

#!/usr/bin/python
 
import os
import shutil
import rrdtool
 
# Filtering the pressure change by using the average from different periods should give better, more stable results
# Read the pressure values for now/t=0, t=-0.5h, t=-1.0h, t=-1.5h, t=-2.0h, t=-2.5h, t=-3.0h, t=-3.5h, t=-4.0h, t=-4.5h, t=-5.0h, t=-5.5h and t=-6.0h
press0 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 600', '-s -600')
press1 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -1800', '-e -1800')
press2 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -3600', '-e -3600')
press3 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -5400', '-e -5400')
press4 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -7200', '-e -7200')
press5 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -9000', '-e -9000')
press6 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -10800', '-e -10800')
press7 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -12600', '-e -12600')
press8 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -14400', '-e -14400')
press9 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -16200', '-e -16200')
press10 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -18000', '-e -18000')
press11 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -19800', '-e -19800')
press12 = rrdtool.fetch("./../pressure_info/pressure.rrd", 'AVERAGE', '-r 60', '-s -21600', '-e -21600')

# Calculate the single differences and normalize them to a change in pressure over 1h
pressurerdiff1 = (press0[2][0][1] - press1[2][0][1])*2
pressurerdiff2 = (press0[2][0][1] - press2[2][0][1])
pressurerdiff3 = (press0[2][0][1] - press3[2][0][1])/1.5
pressurerdiff4 = (press0[2][0][1] - press4[2][0][1])/2
pressurerdiff5 = (press0[2][0][1] - press5[2][0][1])/2.5
pressurerdiff6 = (press0[2][0][1] - press6[2][0][1])/3
pressurerdiff7 = (press0[2][0][1] - press7[2][0][1])/3.5
pressurerdiff8 = (press0[2][0][1] - press8[2][0][1])/4
pressurerdiff9 = (press0[2][0][1] - press9[2][0][1])/4.5
pressurerdiff10 = (press0[2][0][1] - press10[2][0][1])/5
pressurerdiff11 = (press0[2][0][1] - press11[2][0][1])/5.5
pressurerdiff12 = (press0[2][0][1] - press12[2][0][1])/6

# Calculate the average of the differences
pressurerdiff = (pressurerdiff1 + pressurerdiff2 + pressurerdiff3 + pressurerdiff4 + pressurerdiff5 + pressurerdiff6 + pressurerdiff7 + pressurerdiff8 + pressurerdiff9 + pressurerdiff10 + pressurerdiff11 + pressurerdiff12)/12

# Get the current pressure
currentpress = press0[2][0][1]

# Calculate the trend
if pressurerdiff < -0.25:
    trend = -1
elif pressurerdiff >= -0.25 and pressurerdiff <= 0.25:
    trend = 0
elif pressurerdiff > 0.25:
    trend = 1

# Get the current month
today = datetime.date.today()
date = datetime.datetime.strptime(str(today), "%Y-%m-%d")
month = int(date.month)

# Use the Zambretti-algorithm to finally make the forecast
# --------------------------------------------------------
# Falling Conditions
# ------------------
if trend == -1:
    shutil.copyfile('/home/pi/pressure_info/DownRight.png', '/var/www/html/Arrow.png')
    zambretti = 0.0009746*currentpress*currentpress-2.1068*currentpress+1138.7019
    if month < 4 | month > 9:
        zambretti = zambretti + 1
    zambretti = int(round(zambretti))
    if zambretti == 1:
        forecast = 'Settled Fine'
        shutil.copyfile('/home/pi/pressure_info/Sun.png', '/var/www/html/Forecast.png')
    elif zambretti == 2:
        forecast = 'Fine Weather'
        shutil.copyfile('/home/pi/pressure_info/Sun.png', '/var/www/html/Forecast.png')
    elif zambretti == 3:
        forecast = 'Fine Becoming Less Settled'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 4:
        forecast = 'Fairly Fine Showers Later'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 5:
        forecast = 'Showery Becoming unsettled'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 6:
        forecast = 'Unsettled, Rain later'
        shutil.copyfile('/home/pi/pressure_info/Rain.png', '/var/www/html/Forecast.png')
    elif zambretti == 7:
        forecast = 'Rain at times, worse later'
        shutil.copyfile('/home/pi/pressure_info/Rain.png', '/var/www/html/Forecast.png')
    elif zambretti == 8:
        forecast = 'Rain at times, becoming very unsettled'
        shutil.copyfile('/home/pi/pressure_info/Rain.png', '/var/www/html/Forecast.png')
    elif zambretti == 9:
        forecast = 'Very Unsettled, Rain'
        shutil.copyfile('/home/pi/pressure_info/Rain.png', '/var/www/html/Forecast.png')
# Steady Conditions
# -----------------
elif trend == 0:
    shutil.copyfile('/home/pi/pressure_info/Right.png', '/var/www/html/Arrow.png')
    zambretti = 138.24-0.133*currentpress
    zambretti = int(round(zambretti))
    if zambretti == 1:
        forecast = 'Settled Fine'
        shutil.copyfile('/home/pi/pressure_info/Sun.png', '/var/www/html/Forecast.png')
    elif zambretti == 2:
        forecast = 'Fine Weather'
        shutil.copyfile('/home/pi/pressure_info/Sun.png', '/var/www/html/Forecast.png')
    elif zambretti == 3:
        forecast = 'Fine, Possibly showers'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 4:
        forecast = 'Fairly Fine, Showers likely'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 5:
        forecast = 'Showery Bright Intervals'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 6:
        forecast = 'Changeable some rain'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 7:
        forecast = 'Unsettled, rain at times'
        shutil.copyfile('/home/pi/pressure_info/Rain.png', '/var/www/html/Forecast.png')
    elif zambretti == 8:
        forecast = 'Rain at Frequent Intervals'
        shutil.copyfile('/home/pi/pressure_info/Rain.png', '/var/www/html/Forecast.png')
    elif zambretti == 9:
        forecast = 'Very Unsettled, Rain'
        shutil.copyfile('/home/pi/pressure_info/Rain.png', '/var/www/html/Forecast.png')
    elif zambretti == 10:
        forecast = 'Stormy, much rain'
        shutil.copyfile('/home/pi/pressure_info/Storm.png', '/var/www/html/Forecast.png')
# Rising Conditions
# -----------------
elif trend == 1:
    shutil.copyfile('/home/pi/pressure_info/UpRight.png', '/var/www/html/Arrow.png')
    zambretti = 142.57-0.1376*currentpress
    if month < 4 | month > 9:
        zambretti = zambretti + 1
    zambretti = int(round(zambretti))
    if zambretti == 1:
        forecast = 'Settled Fine'
        shutil.copyfile('/home/pi/pressure_info/Sun.png', '/var/www/html/Forecast.png')
    elif zambretti == 2:
        forecast = 'Fine Weather'
        shutil.copyfile('/home/pi/pressure_info/Sun.png', '/var/www/html/Forecast.png')
    elif zambretti == 3:
        forecast = 'Becoming Fine'
        shutil.copyfile('/home/pi/pressure_info/Sun.png', '/var/www/html/Forecast.png')
    elif zambretti == 4:
        forecast = 'Fairly Fine, Improving'
        shutil.copyfile('/home/pi/pressure_info/Sun.png', '/var/www/html/Forecast.png')
    elif zambretti == 5:
        forecast = 'Fairly Fine, Possibly showers, early'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 6:
        forecast = 'Showery Early, Improving'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 7:
        forecast = 'Changeable, Improving'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 8:
        forecast = 'Rather Unsettled Clearing Later'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 9:
        forecast = 'Unsettled, Probably Improving'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 10:
        forecast = 'Unsettled, short fine Intervals'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 11:
        forecast = 'Very Unsettled, Finer at times'
        shutil.copyfile('/home/pi/pressure_info/SunCloud.png', '/var/www/html/Forecast.png')
    elif zambretti == 12:
        forecast = 'Stormy, possibly improving'
        shutil.copyfile('/home/pi/pressure_info/Storm.png', '/var/www/html/Forecast.png')
    elif zambretti == 13:
        shutil.copyfile('/home/pi/pressure_info/Storm.png', '/var/www/html/Forecast.png')
        forecast = 'Stormy, much rain'

 

Moving from Weather Underground to OpenWeatherMap

Since Weather Underground has announced the End of Service for their API by the end of 2018, I stopped uploading data from my weather station to Weather Underground and started to look for an alternative API in order to still being able to display the current weather conditions on my Pimoroni Scroll pHAT HD display and the current air pressure on my GUI for rtl_433, along with the received temperature and humidity.

With OpenWeatherMap, I found a good substitute for that. They also provide an API that can be used for free for a limited amount of calls. For the average hobbyist, the limitations of the API does not matter. Like for Weather Underground in the past, you also have to get an API-Key, before you will be able to use the API.

In the following picture, you can see a small Windows program (similar to my rtl_433-GUI) that I wrote for learning about the OpenWeatherMap-API. It displays the name of the city where the data is coming from, an icon that represents the current weather conditions and the values for temperature, humidity and air pressure. A complete description of the weather conditions and the weather icons used by OpenweatherMap can be found here.

Wetterstation_OWM

I will leave all the Weather Underground related articles here unchanged for reference, but additionally, I will provide a working example on how to use the Openweathermap-API in this article, so that you will be able to adapt the code that uses the Weather Underground-API, to now use the OpenWeatherMap-API.

Using the OpenWeatherMap-API is straightforward and very similar to using the one from Weather Underground. Anyway, there is one thing that I’ve noticed when testing the API. It sometimes delivers the air pressure using one of two different ways. I pay attention to this in my example code so that it should work, either if the API delivers the normalized pressure at sea level via ‘pressure’, or via ‘sea_level’. Below, you can see the code that I use within the program that I’ve shown above. Only thing you have to do before you can use the code, is to replace YOURCITYID with the id of the city you want to get the data for and YOURAPPID with your personal API-Key. How to determine the City-Id is described here.

import requests
import json

try:
    OWM_str = 'http://api.openweathermap.org/data/2.5/weather?id=YOURCITYID&appid=YOURAPPID'
    response = requests.get(OWM_str)
    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)
        temp = str(temp_float)
        humi = y['humidity']
        temp_humi = float(humi)
        temp_humi = round(temp_humi, 1)
        humi = str(temp_humi)
        if 'sea_level' not in y:
            press = y['pressure']
            press_float = float(press)
            press_float = round(press_float, 1)
            press = str(press_float)
        else:
            press = y['sea_level']
            press_float = float(press)
            press_float = round(press_float, 1)
            press = str(press_float)
        z = x['weather']
        icon = z[0]['icon']
        icon = icon + '.png'
        city = x['name']
    else:
        temp = "--.-"
        humi = "--.-"
        press = '---.-'
        icon = '-'
        city = '-'
except:
    temp = "--.-"
    humi = "--.-"
    press = "---.-"
    icon = '-'
    city = '-'

Turning your Raspberry Pi (3 or Zero W) into a Bluetooth beacon

The idea behind a Bluetooth beacon is that things around you are allowed to interact with you in a certain way.  For example, they can give you the address of a website or provide information by using a special app that you have installed for that particular purpose.

In this article, I want to focus on the possibility of broadcasting the URL of a website, since this is a very easy and universal way to provide location based information to someone. My aim for using a Bluetooth beacon at my home was to broadcast the URL of the Weather Underground page of my weather station, using the Eddystone URL format by Google, so that others around can easily access these information with the help of their Android smartphones.

Although, Bluetooth beacons may not be very poplar in daily life up to now, the idea behind it is very fascinating and since it is very easy to create one with a Rasberry Pi (3 or Zero W), it is well worth doing.

There is already a very good description over here, of how to use a Raspberry Pi 3 as an Eddystone URL beacon.  All in all, there are just three easy steps necessary:

1. Enableing the Bluetooth device

2. Setting the Bluetooth device to “advertise and not-connectable”

3. Entering the Beacon Advertising Data

The third step includes encoding the URL which is the most difficult part, but luckily, there is already an Eddystone URL command calculator online available who does the encoding for you.

It is to mention that the lenght of the URL within the broadcast frame is limited, which means that, if you want to broadcast a longer URL, you have to use a service that shortens the URL for you. I am using https://goo.gl for that. Actually, I recommend to do that, even if the URL you want to broadcast fits into the Eddystone broadcast message, because you will be able to see the number of clicks within the statistics of the Google URL Shortener service, which is really nice. The commands for all three steps together are shown below (using some example URL).

sudo hciconfig hci0 up
sudo hciconfig hci0 leadv 3
sudo hcitool -i hci0 cmd 0x08 0x0008 17 02 01 06 03 03 aa fe 0f 16 aa fe 10 00 03 77 65 62 67 61 7a 65 72 08 00 00 00 00 00 00 00 00

This is already enough to turn your Raspberry Pi into a bluetooth beacon. After executing the commands, you should see a notification on your Android Smartphone, if Bluetooth is turned on. Normally, the notifications for Nearby are turned on by default. If you don’t see a notification, you should go to the Google settings and check the Nearby configuration.

Nearby

Even if your Bluetooth beacon is alredy working and broadcasting the URL correctely now, there is one more additional thing that you should do. The Eddystone format also transmits the TX power (the value between 10 and 03), so that a receiver can use that information to determine the distance to the beacon. For making that work correctely, you have to make a calibration, which simply means that you have to adjust the value for TX power within the Eddystone message. I have done that with the help of the APP Beacon Scanner. The rule for calibration is that you measure The TX power of the beacon at a distance of 1m (using Beacon Scanner), add 41 dBm and then use the result as the value for TX power. For me, that gave a good starting point for the calibration, but I had to play around a bit more with the value for TX power, so that the real distance to the beacon and the one shown by Beacon Scanner were a good match.

scanner

If you want to start the beacon automatically when you power up your Raspberry Pi, you can easily create a script with the three command lines and put it in the crontab with @reboot. But you have to take in account that Bluetooth may not available when the script is executed, so a delay (sleep command) at the beginning of the script  is necessary to make the beacon work. I use a delay of ten seconds for that, which works just fine.

With the help of your new Raspberry Pi Bluetooth beacon, you can now just tell your visitors to check the notifications of their Android Smartphone and to follow the link (bad luck for iPhone users 😉 ), if they are curious about the current weather condition details at your home location.

Finding an UPS for the Raspberry Pi

Even if the infrastructure in Germany is very good, occasional power outages for a few seconds, or even for minutes can happen once or twice a year. Every time this happens, I feel really afraid for my Raspberry Pi. Although, up to now, it was never a real problem and the Raspberry Pi just booted fine after the power came back, the uneasy feeling remains.

After the last power outage that lasted for about 15 minutes, I started to look for a way to keep my Raspberry Pi running during a power outage. If you search the Internet for an UPS for the Raspberry Pi, you basically will find three different solutions  for it.

A ‘real’ UPS with a built in lead acid battery

Solutions including a backup battery that connect to the Pi’s HAT

A power bank that can be charged and discharged at the same time

But all of these solutions have at least one big disadvantage that makes it unusable for me. A big UPS which is normally used for desktop PCs would be an overkill. It is just to big and expensive and also uses usually at least 10W during standby.

Also, I want to continue using the original case and don’t want to connect anything big to the Raspberry Pi directly, so this also excludes the second solution.  A software that shuts down the Raspberry Pi, if a power outage is detected is also unnecessary for my purpose. For me, it is only important to keep it running independently for as long as possible.

What I don’t like about the idea with the power bank is, that it is hard to find a power bank that can be charged and discharged at the same time and also, the power bank is not really designed in the way a UPS should work. I would rather have a battery that is constantly kept full than a battery that is charged and discharged all the time.

Finally, I found the PowerWalker DC SecureAdapter 12V. A small and lightweight DC UPS, especially designed for routers, security cameras and similar devices. This power supply contains a rechargeable 18650 Li-Ion battery as a backup. Only downside is that it outputs 12V and the Raspberry Pi needs 5V. So, one more step is necessary, if you want to use this nice little UPS together with your Raspberry Pi, you have to convert the output voltage. For that, I use a 15W DC-DC converter that can output 5V/3A maximum, just like the original Raspberry Pi power supply can do. You can find various DC/DC converters all around on Amazon, eBay, AliExpress and so on. Luckily, there are ones available that directly have a Micro USB connector on the output, so all you have to do is to connect the output of the UPS with the input of the DC/DC converter. For that, I use an adapter that has a screw terminal on one side and a 5.5mm/2.5mm DC socket on the other side, so finally not even soldering was required for putting it all together.

Below, you can see everything connected together and also a table from the box of the PowerWalker DC SecureAdapter that gives you an idea about the approximate backup times. If you assume that the DC/DC converter has an efficiency of about 90%, the Raspberry Pi  can run about 150 minutes with a power consumption of 10.8W (2.16A)!

USV

Another advantage of the PowerWalker DC SecureAdapter is, that the rechargeable 18650 Li-Ion battery is a very common rechargeable battery which can easily be found, and that it can be replaced on your own.

Although, there is always one voltage conversion more present than it would ideally need (230V to 12V to 5V, instead of 230V to 5V and 3.7V to 12V to 5V, instead of 3.7V to 5V), I think this solution is the closest one to what the perfect solution for me would be at the moment. Maybe some day, there will be a device like the PowerWalker DC SecureAdapter available that directly outputs 5V…

 

Solving Lighttpd hang issues

Although, everything was running fine on my Raspberry Pi 3, there was one thing that bothered me and took me quite a while to find a solution for it. Every now and then, Lighttpd started to hang for many seconds. After just waiting, or impatiently restarting Lighttpd, everything worked fine again for some time. Unnecessary to say that this situation was not very pleasing and a solution had to be found.

After looking around the internet, I got the strong feeling that the FastCGI configuration is causing the problem. In particular, PHP_FCGI_MAX_REQUESTS seems to be the one to blame.

I have PHP7 on my Raspberry Pi 3 and the parameters we are looking for are accessible with the following commands (including a backup of the original configuration). This can vary, depending on which version of PHP you are using.


cd /etc/lighttpd/conf-available/

sudo cp 15-fastcgi-php.conf 15-fastcgi-php.conf.bak

sudo nano 15-fastcgi-php.conf

Within the configuration, the following part is of special interest for us. This was what my default installation looked like.


...

"max-procs" =&gt; 1,

"bin-environment" =&gt; (

&nbsp;&nbsp;&nbsp; "PHP_FCGI_CHILDREN" =&gt; "4",

&nbsp;&nbsp;&nbsp; "PHP_FCGI_MAX_REQUESTS" =&gt; "10000"

...

I changed the values for max-procs and PHP_FCGI_MAX_REQUESTS like shown below. For more detailed information about the function of the parameters, you should read the article Optimizing FastCGI performance.


...

"max-procs" =&gt; 3,

"bin-environment" =&gt; (

&nbsp;&nbsp;&nbsp; "PHP_FCGI_CHILDREN" =&gt; "4",

&nbsp;&nbsp;&nbsp; "PHP_FCGI_MAX_REQUESTS" =&gt; "500"

...

After changing the values, you have to restart Lighttpd, so that the changes will take effect.


sudo /etc/init.d/lighttpd restart

Right after restarting the web server, you can see the RAM consumption of the system increasing, due to the increase of max-procs. I am not 100% sure, if the values that I have chosen are the best ones for the Raspberry Pi 3, but up to now, Lighttpd is running just fine, without any hangs, delays or hiccups and the RAM consumption also seems to be fine.

Using the Steak Champ thermometer to make the perfect steak

If you like to make yourself a good steak every now and then, just like I do, you can go and take a Raspberry Pi and build yourself a full option steak thermometer that has eight temperature sensors, a touchscreen, a web server, Email notification and a fan cotroller for your barbecue, or instead, you can go and buy a steak thermometer that looks a bit simpler, but also has a lot of high-tech inside. 😉

Steak01

In the past, I used to use the ‘Perfect Steaks and Roasts’ app when frying a steak. Using this app works well, if your steak keeps it’s size during frying, but if it raises while you are frying it (some haunch steaks e.g. just like to do), the recommended frying time from the app will get totally useless. To avoid this problem, you have to look for a more sophisticated tool that helps you to get the best result.

In general, using a thermometer and monitoring the core temperature of the meat is a good and well-known idea. But of course, it does not have to be an ordinary thermometer, it has to be a high-tech one like the Steak Champ steak thermometer. 🙂 This thermometer does not have a normal display/gauge to show you the temperature, it has a multi-color LED that shows you the degree of the doneness of the steak. After activating the thermometer, the LED slowly flashes in green color. If it reaches ‘medium rare’, it starts to rapidly flash in green color. Same for ‘medium’ (yellow) and ‘medium well’ (red). If your desired degree of the donenes is reached, you have to get the steak out of the pan (or barbecue) and give it a rest until the rapid flashing of the LED stops. Then the making of the steak has finished and it’s ready to eat.

The battery inside the thermometer is promised to last for about 1000 steaks, then the thermometer will get useless. Changing the battery is not possible. If this is true, the price for the thermometer (about 45€) is quite OK. This, time will tell. But for now, it’s time for a test…

A dry-aged steak from the local butcher:

Steak02

Ready to be fried:

Steak03

Time to turn it around:

Steak04

It says ‘medium’, so give it a little rest and a portion of good steak pepper:

Steak05

Yummy! 😀

Steak06

Getting the stats out of Pi-Hole

If you want to be informed about your Pi-Hole statistics, but don’t want to visit the Pi-Hole dashboard all the time, then there is also an alternative way to get the statistics out of Pi-Hole, and use the numbers to present it in any other way that meets your demands. This could be e.g. showing it on a display (like the Scroll pHAT HD), uploading it to ThingSpeak, or whatever else you can think of.

Pi-Hole already provides you the opportunity to make a request for getting some statistic data right out of the box.  You just have to put the following line in the address bar of your browser and replace IPOFYOURPIRUNNINGPIHOLE with the IP address of your Raspberry Pi, on which Pi-Hole is running, and you will get a list with various numbers and the description for them.

http://IPOFYOURPIRUNNINGPIHOLE/admin/api.php

The result is given to you in the JSON data format and can easily be processed with Python, just like shown in previous scripts here in the blog before.

PiHoleStats

The following small Python script gets the data, extracts some interesting information,  formats it, and then outputs the final text on the command line.

#!/usr/bin/env python

import json
import urllib2

try:
    f = urllib2.urlopen('http://IPOFYOURPIRUNNINGPIHOLE/admin/api.php')
    json_string = f.read()
    parsed_json = json.loads(json_string)
    queries = parsed_json['dns_queries_today']
    adsblocked = parsed_json['ads_blocked_today']
    clients = parsed_json['unique_clients']
    f.close()
except:
    queries = '-'
    adsblocked = '-'
    clients = '-'

pihole = 'DNS-Queries: ' + str(queries) + ' - ' + 'Ads blocked: ' + str(adsblocked) + ' - ' + 'Devices: ' + str(clients)
print pihole

 

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()

Overview of my weather station website

The website of my weather station includes contents from most of the articles here in the blog, so I finally want to give you an overview, of how the website that I mentioned several times before looks like.

Because I included all the information on one single page, I put a navigation bar at the top of the page. If you click on one of the links, the page scrolls automatically to the correspondent area. By clicking on one of the graphs, you will get back to the top of the page, so that there is actually no excessively scrolling necessary, despite the fact that the page is actually rather long. If you stay on the website for a longer period of time, it will automatically reload itself after five minutes, so that it always shows you the latest values.

website

PHP scripts for your Raspberry Pi weather station

On the website of my Raspberry Pi weathers station, I show various graphs that illustrate the development of different weather and also system values. But there are also a few other interesting information to include that don’t need graphs.

Besides general information, like the current date and time, I also wanted to display the overall run time of the system, the current clock speed, the installed Kernel version, and the number of currently running processes. To get all of these information, I am using small PHP scripts.  Below, you can see the representation of these information, followed by the sources of the used scripts.

system

Current time and date:

<?php
	date_default_timezone_set("Europe/Berlin");
	setlocale(LC_TIME, "de_DE.utf8");
	$timestamp = time();
	$datum = date("d.m.Y",$timestamp);
	$uhrzeit = date("H:i",$timestamp);
	echo strftime("%A, ");
	echo $datum," – ",$uhrzeit," Uhr";
?>

Uptime:

<?php
	$ut = strtok(@exec("cat /proc/uptime"), ".");
	$days = sprintf("%2d", ($ut / (3600 * 24)));
	$hours = sprintf("%2d", (($ut % (3600 * 24))) / 3600);
	$min = sprintf("%2d", ($ut % (3600 * 24) % 3600) / 60);
	$sec = sprintf("%2d", ($ut % (3600 * 24) % 3600) % 60); 
	$uptime = array($days, $hours, $min, $sec); 

	echo ("Laufzeit: ");
	
	if ($uptime[0] == 1) {  
							echo ($uptime[0] . " Tag, "); 
						 }
	else {  
			echo ($uptime[0] . " Tage, "); 
		 }
		
	if ($uptime[1] == 1) {
							echo ($uptime[1] . " Stunde und ");
						 }
	else {  
			echo ($uptime[1] . " Stunden und "); 
		 }
	
	if ($uptime[2] == 1) {
							echo ($uptime[2] . " Minute");
						 }
	else {  
			echo ($uptime[2] . " Minuten"); 
		 }					 
?>

Clock, Kernel version and number of processes:

<?php
	$freq = @exec("echo $(($(cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq)/1000))");
	$proc = @exec("ps aux | wc -l");
	$vers = @exec("uname -r");
	
	echo ("Taktfrequenz: " . $freq . " MHz &nbsp;•&nbsp; Kernel Version: " . $vers . " &nbsp;•&nbsp; Prozesse: " . $proc ); 
?>

Just like for the system information, there are also a few weather and environment related information that do not need to use graphs, e.g the before described warning status of the lightning detector, the air quality, and the weather forecast. Furthermore, the current season, the times for sunrise and sunset and the lunar phase would be a useful addition on the weather station website. To calculate these times and states, of course, you can use PHP scripts again. To get the sunrise and sunset times of your desired location, you just have to adapt the coordinates in the scripts. When you are using the scripts for season and lunar phase, you can directly use the resulting string (followed by the file extension) to load an icon of the same name.

Version 1:

Hechingen

Version2 :

Benningen

Lunar phase:

<?php
function mondphase()
{
	date_default_timezone_set("Europe/Berlin");
	$timestamp = time();
	$day = date("j",$timestamp);
	$month = date("n",$timestamp);
	$year = date("Y",$timestamp);
		
	$c = $e = $jd = $b = 0;
	if ($month < 3)
	{
		$year = $year - 1;
		$month = $month + 12;
	}
	$month = $month + 1;
	$c = 365.25 * $year;
	$e = 30.6 * $month;
	$jd = $c + $e + $day - 694039.09;
	$jd = $jd / 29.5305882;
	$b = (int) $jd;
	$jd = $jd - $b;
	$b = round($jd * 8);
	if ($b >= 8 )
	{
		$b = 0;
	}
	
	switch ($b)
	{
		case 0:
			return 'NewMoon';
			break;
		case 1:
			return 'WaxingCrescent';
			break;
		case 2:
			return 'FirstQuarter';
			break;
		case 3:
			return 'WaxingGibbous';
			break;
		case 4:
			return 'FullMoon';
			break;
		case 5:
			return 'WaningGibbous';
			break;
		case 6:
			return 'ThirdQuarter';
			break;
		case 7:
			return 'WaningCrescent';
			break;
		default:
			return 'Unknown';
	}
}
?>

Current season:

<?php 
function season($theday) {
	if($theday >= "79" && $theday <= "171") {
	$season = "Spring";
	} elseif($theday >= "172" && $theday <= "264") {
	$season = "Summer";
	} elseif($theday >= "265" && $theday <= "355") {
	$season = "Autumn";
	} else {
	$season = "Winter";
	}
	   return $season;
}
?>

Sunrise time:

<?php
	$zenith = 90+50/60; 
	echo date_sunrise(time(), SUNFUNCS_RET_STRING,
	49.xx, 9.xx, $zenith, date("O")/100) . " Uhr"; 
?>

Sunset time:

<?php
	$zenith = 90+50/60;
	echo date_sunset(time(), SUNFUNCS_RET_STRING,
	49.xx, 9.xx, $zenith, date("O")/100) . " Uhr";
?>