Battery-powered E-Ink weather display for our home.
Battery-powered E-Ink weather display for our home. The device wakes up early in the morning, fetches latest weather forecast information, updates the info to the E-Ink display, and goes back to deep sleep until tomorrow.
You can read more about the build in my blog post.
Goals:
2023 UPDATE: Battery lasted roughly 4 months, when updating the screen 6 times a day. Updating the screen more times a day ended up making the UX better. The information is always recent enough. When updating only once in the morning, I sometimes ended up checking the latest weather status from a phone app later during the day.
Challenges:
Hardware bought but not needed in the end:
The project has two separate parts: render and rasp.
render
Generates HTML that will be eventually rendered as PNG. The image contains the weather forecast. render
is exposed via Google Cloud Function. It's the perfect tool for this type of task. The endpoint is quite rarely called and latencies don't matter that much.
rasp
Runs on Raspberry Pi Zero.
All code related to the hardware that will display the weather image. This part doesn't know anything about weather, it just downloads a PNG from given URL and renders it on e-Ink display.
Note: this is a rough guide written from memory.
Most of the software lives in Google Cloud. This off-loads a lot of processing away from the Raspberry Pi device, mostly to optimize battery-life. Deployment is done via GH actions, but the initial setup was roughly:
GCP_SERVICE_ACCOUNT_KEY
secret.NODE_ENV=production
and API_KEY=<new random key>
. The API key is just there to prevent random http calls to consume GCP quota. Headless Chrome rendering seems to work well with 1GB of memory.Download correct image from here: https://www.raspberrypi.com/software/operating-systems/b
Flash it to an SD card with balenaEtcher https://www.balena.io/etcher/ (or use RPIs own flasher)
Boot the raspberry, and do initial setup
sudo raspi-config
In your router, make sure to assign a static local IP address for the device
Install display updating code
Download zip
curl -H "Authorization: token <token>" -L https://api.github.com/repos/kimmobrunfeldt/eink-weather-display/zipball/main > main.zip
or sudo apt install git
and
git clone https://<user>:<personal_access_token>@github.com/kimmobrunfeldt/eink-weather-display.git
You can create a limited Github personal access token, which only can clone that repo. I found git to be the easiest, it was easy to just git pull
any new changes.
sudo apt install python3-pip
~pip install pipenv
~ Edit: I wasn't able to get pipenv working due to pijuice being system-wide package. Ended up going with all-system-wide packages.
cd eink-weather-display && pip install Pillow==9.3.0 google-cloud-logging requests python-dotenv pytz
Install Python deps
Setup env variables: cp .env.example .env
and fill in the details
Follow installation guide from https://www.waveshare.com/wiki/10.3inch_e-Paper_HAT to get the E-Ink display working
After install, test that the demo software (in C) works
sudo apt install pijuice-base
Enable I2C interface
More debugging info:
To allow PIJuice to turn on without a battery, go to general settings and enable "Turn on without battery" or similar option.
Make sure to use correct PIJuice battery profile (PJLIPO_12000 for me)
If using pijuice_cli
, remember to apply changes! It was quite hidden down below.
Test that the PIJuice works with battery too
cd rasp/IT8951
and follow install instructions (inside virtualenv if using one)
Pijuice setup using pijuice_cli
. Remember to save the changes inside each setup screen!
After that's done, you can test the PiJuice + E-Ink display together.
Setup crontab. Run refresh on boot, and shutdown device if on battery.
@reboot (cd /home/pi/eink-weather-display/rasp; ./wait-for-network.sh; python main.py;) >> /home/pi/cron.log 2>&1
# Every minute
* * * * * (cd /home/pi/eink-weather-display/rasp; python shutdown_if_on_battery.py;) >> /home/pi/cron.log 2>&1
Side note: I did all the steps until here using Raspberry PI GPIO headers. However they ended up being too tall for the frame. Instead of soldering GPIO pins to make everything fit, I checked if the IT8951 controller was possible to use via its USB interface.
And fortunately, it was!
rasp/usb-it8951/
and follow README.md instructions to get it build and working. Build it in Raspberry Pi.
main.py
accordinglysudo apt install imagemagick
The following files are under Apache 2.0 license:
render/**/*
rasp/*.py
(just top level python code, not IT8961 subdirs)Note! Since the display updates only once or twice a day, everything has been designed that in mind. The forecast always starts 9AM, and doesn't show any real observations during the day.
npm i
npm start
npm run render
to run the CLI tool that renders HTML to src/templates/render.html
The cloud function and CLI support basic image operations to offload that work from Raspberry: rotate
, flip
, flip
, padding(Top|Right|Bottom|Left)
, resizeToWidth
, resizeToHeight
. See sharp for their docs. For example --flip
with CLI or ?flip=true
with CF.
LAT="60.222"
LON="24.83"
LOCATION="Espoo"
BATTERY="100"
TIMEZONE="Europe/Helsinki"
curl -vv -o weather.png \
-H "x-api-key: $API_KEY" \
"https://europe-west3-weather-display-367406.cloudfunctions.net/weather-display?lat=$LAT&lon=$LON&locationName=$LOCATION&batteryLevel=$BATTERY&timezone=$TIMEZONE"
Links
fmi::forecast::harmonie::surface::point::simple
The model can return data up to 50h from now.
{
"Pressure": 1015.7,
"GeopHeight": 26.3,
"Temperature": 6.4,
"DewPoint": 4.9,
"Humidity": 92.8,
"WindDirection": 127,
"WindSpeedMS": 1.97,
"WindUMS": -1.37,
"WindVMS": 1.37,
"PrecipitationAmount": 0.38,
"TotalCloudCover": 100,
"LowCloudCover": 100,
"MediumCloudCover": 0,
"HighCloudCover": 58.9,
"RadiationGlobal": 4.4,
"RadiationGlobalAccumulation": 682913.3,
"RadiationNetSurfaceLWAccumulation": -1537350,
"RadiationNetSurfaceSWAccumulation": 613723.9,
"RadiationSWAccumulation": 14.2,
"Visibility": 7441.7,
"WindGust": 3.6,
"time": "2022-11-02T07:00:00.000Z",
"location": {
"lat": 60.222,
"lon": 24.83
}
}
ecmwf::forecast::surface::point::simple
The model can return data up to 10 days from now.
{
"GeopHeight": 37.6,
"Temperature": 5.8,
"Pressure": 1016,
"Humidity": 95.7,
"WindDirection": null,
"WindSpeedMS": null,
"WindUMS": -1.8,
"WindVMS": -0.1,
"MaximumWind": null,
"WindGust": null,
"DewPoint": null,
"TotalCloudCover": null,
"WeatherSymbol3": null,
"LowCloudCover": null,
"MediumCloudCover": null,
"HighCloudCover": null,
"Precipitation1h": 0,
"PrecipitationAmount": null,
"RadiationGlobalAccumulation": null,
"RadiationLWAccumulation": null,
"RadiationNetSurfaceLWAccumulation": null,
"RadiationNetSurfaceSWAccumulation": null,
"RadiationDiffuseAccumulation": null,
"LandSeaMask": null,
"time": "2022-11-02T07:00:00.000Z",
"location": {
"lat": 2764063,
"lon": 8449330.5
}
}
fmi::observations::weather::hourly::simple
Observations are returned 24 hours in past.
{
"TA_PT1H_AVG": 1.9,
"WS_PT1H_AVG": 1.5,
"WD_PT1H_AVG": 205,
"PRA_PT1H_ACC": 0,
"time": "2022-11-14T08:00:00.000Z",
"location": {
"lat": 60.17797,
"lon": 24.78743
}
}
ParameterName descriptions
variable | label | base_phenomenon | unit | stat_function | agg_period |
---|---|---|---|---|---|
1 | TA_PT1H_AVG | Air temperature | Temperature | degC | avg |
2 | TA_PT1H_MAX | Highest temperature | Temperature | degC | max |
3 | TA_PT1H_MIN | Lowest temperature | Temperature | degC | min |
4 | RH_PT1H_AVG | Relative humidity | Humidity | % | avg |
5 | WS_PT1H_AVG | Wind speed | Wind | m/s | avg |
6 | WS_PT1H_MAX | Maximum wind speed | Wind | m/s | max |
7 | WS_PT1H_MIN | Minimum wind speed | Wind | m/s | min |
8 | WD_PT1H_AVG | Wind direction | Wind | deg | avg |
9 | PRA_PT1H_ACC | Precipitation amount | Amount of precipitation | mm | acc |
10 | PRI_PT1H_MAX | Maximum precipitation intensity | Amount of precipitation | mm/h | max |
11 | PA_PT1H_AVG | Air pressure | Air pressure | hPa | avg |
12 | WAWA_PT1H_RANK | Present weather (auto) | Weather | rank |
There are two methods:
Run until ssh -o ConnectTimeout=2 raspzero2 ; do ; done;
on laptop and move to next step.
SSH connection needs to be active before the main.py shuts down the device.
Press the physical on switch in PiJuice board.
Method 2: SSH while plugged on power.
Take the display off the wall, and plug micro usb to PiJuice usb connector.
Wait until on
ssh raspzero2
The device won't automatically shutdown while power is connected.
*After SSH session
Use shutd
(alias shutd='cd ~/eink-weather-display/rasp && python shutdown.py'
) to shutdown the Pi after SSH session.
This ensures that the RTC alarm is set correctly.
Logs are at GCP Logging Explorer
Power should turn on automatically when cable is connected
Power should keep on even on battery if any SSH session is active, unless max uptime is exceeded (safely timeout to avoid draining battery)
git pull
is executed once a day within Raspberry Pi
To plot battery level and other measurements history and predicted levels, run ./battery-graph.sh && open graph.png
.
If you don't want to re-fetch data from GCP Logs on consecutive runs, use ./battery-graph.sh -l true && open graph.png
and it'll use locally saved files instead.