You've already forked eink-calendar
mirror of
https://github.com/javierpena/eink-calendar.git
synced 2025-08-10 21:52:01 +02:00
First upload
This commit is contained in:
140
.gitignore
vendored
Normal file
140
.gitignore
vendored
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# ---> Python
|
||||||
|
# Byte-compiled / optimized / DLL files
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# C extensions
|
||||||
|
*.so
|
||||||
|
|
||||||
|
# Distribution / packaging
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# PyInstaller
|
||||||
|
# Usually these files are written by a python script from a template
|
||||||
|
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||||
|
*.manifest
|
||||||
|
*.spec
|
||||||
|
|
||||||
|
# Installer logs
|
||||||
|
pip-log.txt
|
||||||
|
pip-delete-this-directory.txt
|
||||||
|
|
||||||
|
# Unit test / coverage reports
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
|
.nox/
|
||||||
|
.coverage
|
||||||
|
.coverage.*
|
||||||
|
.cache
|
||||||
|
nosetests.xml
|
||||||
|
coverage.xml
|
||||||
|
*.cover
|
||||||
|
*.py,cover
|
||||||
|
.hypothesis/
|
||||||
|
.pytest_cache/
|
||||||
|
cover/
|
||||||
|
|
||||||
|
# Translations
|
||||||
|
*.mo
|
||||||
|
*.pot
|
||||||
|
|
||||||
|
# Django stuff:
|
||||||
|
*.log
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Flask stuff:
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
|
||||||
|
# Scrapy stuff:
|
||||||
|
.scrapy
|
||||||
|
|
||||||
|
# Sphinx documentation
|
||||||
|
docs/_build/
|
||||||
|
|
||||||
|
# PyBuilder
|
||||||
|
.pybuilder/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# Jupyter Notebook
|
||||||
|
.ipynb_checkpoints
|
||||||
|
|
||||||
|
# IPython
|
||||||
|
profile_default/
|
||||||
|
ipython_config.py
|
||||||
|
|
||||||
|
# pyenv
|
||||||
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
|
# .python-version
|
||||||
|
|
||||||
|
# pipenv
|
||||||
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
|
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||||
|
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||||
|
# install all needed dependencies.
|
||||||
|
#Pipfile.lock
|
||||||
|
|
||||||
|
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||||
|
__pypackages__/
|
||||||
|
|
||||||
|
# Celery stuff
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat.pid
|
||||||
|
|
||||||
|
# SageMath parsed files
|
||||||
|
*.sage.py
|
||||||
|
|
||||||
|
# Environments
|
||||||
|
.env
|
||||||
|
.venv
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Spyder project settings
|
||||||
|
.spyderproject
|
||||||
|
.spyproject
|
||||||
|
|
||||||
|
# Rope project settings
|
||||||
|
.ropeproject
|
||||||
|
|
||||||
|
# mkdocs documentation
|
||||||
|
/site
|
||||||
|
|
||||||
|
# mypy
|
||||||
|
.mypy_cache/
|
||||||
|
.dmypy.json
|
||||||
|
dmypy.json
|
||||||
|
|
||||||
|
# Pyre type checker
|
||||||
|
.pyre/
|
||||||
|
|
||||||
|
# pytype static type analyzer
|
||||||
|
.pytype/
|
||||||
|
|
||||||
|
# Cython debug symbols
|
||||||
|
cython_debug/
|
||||||
|
|
31
LICENSE
31
LICENSE
@@ -1,25 +1,26 @@
|
|||||||
BSD 2-Clause License
|
Copyright (c) <year> <owner>. All rights reserved.
|
||||||
|
|
||||||
Copyright (c) 2021, Javier Peña
|
Redistribution and use in source and binary forms, with or without modification,
|
||||||
All rights reserved.
|
are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without
|
1. Redistributions of source code must retain the above copyright notice,
|
||||||
modification, are permitted provided that the following conditions are met:
|
this list of conditions and the following disclaimer.
|
||||||
|
|
||||||
1. Redistributions of source code must retain the above copyright notice, this
|
|
||||||
list of conditions and the following disclaimer.
|
|
||||||
|
|
||||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||||
this list of conditions and the following disclaimer in the documentation
|
this list of conditions and the following disclaimer in the documentation
|
||||||
and/or other materials provided with the distribution.
|
and/or other materials provided with the distribution.
|
||||||
|
|
||||||
|
3. Neither the name of the copyright holder nor the names of its contributors
|
||||||
|
may be used to endorse or promote products derived from this software without
|
||||||
|
specific prior written permission.
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
||||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
111
README.md
Normal file
111
README.md
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
# Eink family calendar
|
||||||
|
|
||||||
|
## What is it?
|
||||||
|
I needed to have an updatable calendar to keep track of my children's school agenda,
|
||||||
|
as well as some post-school activities. So instead of using a whiteboard, I thought
|
||||||
|
it would be a nice holiday project to do it with a Raspberry Pi 2 and an e-ink
|
||||||
|
screen.
|
||||||
|
|
||||||
|
## What do you need
|
||||||
|
- A Raspberry Pi (any model would do, I did it with a Pi 2), with the Raspberry Pi OS.
|
||||||
|
- Eink display: [640x384, 7.5inch E-Ink display HAT for Raspberry Pi, yellow/black/white three-color](https://www.waveshare.com/product/displays/e-paper/epaper-1/7.5inch-e-paper-hat-c.htm).
|
||||||
|
- A [1x4 matrix keypad](https://www.adafruit.com/product/1332) to allow you to switch
|
||||||
|
between the different calendars. There are multiple clones in different shops.
|
||||||
|
- A photo frame to house the setup. I bought one at a local shop, just make sure it has
|
||||||
|
enough depth to host all the hardware.
|
||||||
|
- One or more Caldav calendars to display.
|
||||||
|
|
||||||
|
## Configuring
|
||||||
|
Once the basic setup and connections are done (TODO: add diagram for GPIO connections),
|
||||||
|
you will need to set up the `config.ini` file. This is the syntax:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[DEFAULT]
|
||||||
|
video = pygame
|
||||||
|
keyboard = pygame
|
||||||
|
|
||||||
|
[weather]
|
||||||
|
owm_api_key = <insert API key here>
|
||||||
|
owm_location = <insert location id here>
|
||||||
|
|
||||||
|
[calendar]
|
||||||
|
urls = http://example.com/caldavcal1, http://example.com/caldavcal2, http://example.com/caldavcal3
|
||||||
|
names = user1, user2, user3
|
||||||
|
username = user
|
||||||
|
password = password
|
||||||
|
```
|
||||||
|
|
||||||
|
### Driver configuration
|
||||||
|
We have two different drivers to be used for both video output and keyboard input.
|
||||||
|
This allows us to hack and tests locally without using the e-ink screen all the time.
|
||||||
|
|
||||||
|
- For video, we can use `pygame` for the [Pygame](https://github.com/pygame/pygame)
|
||||||
|
driver, or `eink` to use the e-ink screen.
|
||||||
|
- For keyboard input, we can use `pygame` to get input from your computer's keyboard,
|
||||||
|
or `gpio` to use the 1x4 keypad connected to the GPIO pins 29 ,31, 33, 35 and 37.
|
||||||
|
|
||||||
|
### Open Weather Map API configuration
|
||||||
|
You need to subscribe to the "Current Weather Data" API [link](https://openweathermap.org/api).
|
||||||
|
Note that you need to sign in as a user (it's free). Once you get the API key and the
|
||||||
|
location id for your town, add them to the `owm_api_key` and `owm_location` keys in
|
||||||
|
the configuration file.
|
||||||
|
|
||||||
|
### Calendar configuration
|
||||||
|
You can use any CalDav link; in my case, I set up 3 calendars in a [Synology](https://www.synology.com)
|
||||||
|
DiskStation. The code should adapt to any number of calendars, but keep in mind the
|
||||||
|
resolution and font size ;).
|
||||||
|
|
||||||
|
The `urls` parameter is a comma-separated list of CalDav urls. Since each of those
|
||||||
|
calendars will correspond to an actual person, `names` will contain the list of
|
||||||
|
names for each url. Make sure you spefify the same number of urls and names.
|
||||||
|
|
||||||
|
The `username` and `password` fields are self-explaining: include the user and
|
||||||
|
password to access the calendars.
|
||||||
|
|
||||||
|
If you want to use a different type of calendar, such as Google Calendar, you
|
||||||
|
will need to create a new driver. Patches are welcome :).
|
||||||
|
|
||||||
|
## Running
|
||||||
|
The scripts directory contains a simple launcher script using a virtual environment,
|
||||||
|
and a systemd unit file you can use to ensure the program runs on startup.
|
||||||
|
|
||||||
|
When the application is stopped, it should clear the e-ink display, which is a good
|
||||||
|
idea to avoid displa burnout. In some cases, it may not happen (for example if
|
||||||
|
you get a power disruption). You can use the `reset_eink.py` script in those
|
||||||
|
cases to clear the screen.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
When started, the calendar will show today's calendar for everyone, from 8:00 to
|
||||||
|
21:00 (times are not configurable at the moment). You can switch to a weekly, Monday-to-Friday
|
||||||
|
calendar for each person by pressing the 2, 3 or 4 buttons in the keypad, and
|
||||||
|
switch back to the daily calendar by pressing 1. Yes, that means that having
|
||||||
|
more than 3 calendars could require code changes ;).
|
||||||
|
|
||||||
|
After 9 PM, the calendar will enter in screensaver mode, and display the image
|
||||||
|
in the `img/night_image.jpg` file. The image will be converted to a 1-bit format,
|
||||||
|
so it's better if you use a 1-bit image already.
|
||||||
|
|
||||||
|
If there is a JPG or PNG file named after today's date, in DD-MM-YYYY format
|
||||||
|
(for example, 01-01-2022.png for January 1st, 2022), the screensaver will use that
|
||||||
|
image instead of the default one. Be creative!
|
||||||
|
|
||||||
|
## License
|
||||||
|
Refer to the LICENSE file for licensing details.
|
||||||
|
|
||||||
|
The weathericons-regular-webfont font is licensed under the [SIL OFL 1.1](http://scripts.sil.org/OFL)
|
||||||
|
license.
|
||||||
|
|
||||||
|
The [DejaVuSansMono-Bold font](https://dejavu-fonts.github.io/) is licensed under the
|
||||||
|
Bitstream Vera and Public Domain.
|
||||||
|
|
||||||
|
The `epd7in5bc.py` and `epdconfig.py` files are taken from the [Waveshare e-Paper repository](https://github.com/waveshare/e-Paper/),
|
||||||
|
including patches inspired by [this pull request](https://github.com/waveshare/e-Paper/pull/104)
|
||||||
|
to improve performance.
|
||||||
|
|
||||||
|
## Anything missing?
|
||||||
|
Feel free to contact me. This project was mainly set to scratch a personal itch,
|
||||||
|
but if it can be helpful to anyone, I'd be more than happy to improve it and
|
||||||
|
its documentation.
|
||||||
|
|
||||||
|
## Author
|
||||||
|
Javier Peña (@fj_pena).
|
20
config.ini
Normal file
20
config.ini
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
# Valid video drivers: pygame (for testing), eink for e-paper screen
|
||||||
|
video = pygame
|
||||||
|
# Valid keyboard drivers: pygame (for testing), gpio for 1x4 matrix keypad connected to GPIO pins
|
||||||
|
keyboard = pygame
|
||||||
|
|
||||||
|
[weather]
|
||||||
|
# OpenWeatherMap API key and location
|
||||||
|
# See https://openweathermap.org/appid
|
||||||
|
owm_api_key = <insert API key here>
|
||||||
|
# For the location code, search for your location in https://openweathermap.org/,
|
||||||
|
# then use the id in the resulting URL, for example https://openweathermap.org/city/3129046
|
||||||
|
owm_location = <insert location id here>
|
||||||
|
|
||||||
|
[calendar]
|
||||||
|
# URL of all caldav calendars, separated by commas
|
||||||
|
urls = http://example.com/caldavcal1, http://example.com/caldavcal2, http://example.com/caldavcal3
|
||||||
|
names = user1, user2, user3
|
||||||
|
username = user
|
||||||
|
password = password
|
38
drivers/caldavprovider.py
Normal file
38
drivers/caldavprovider.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import caldav
|
||||||
|
import icalendar
|
||||||
|
import pytz
|
||||||
|
import urllib3
|
||||||
|
|
||||||
|
urllib3.disable_warnings()
|
||||||
|
|
||||||
|
class CalDavProvider():
|
||||||
|
def __init__(self, username, password):
|
||||||
|
self.tz = pytz.timezone('Europe/Madrid')
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def get_calendar(self, url, date_start, date_end):
|
||||||
|
# print("%s %s" % (date_start, date_end))
|
||||||
|
client = caldav.DAVClient(url=url, username=self.username, password=self.password, ssl_verify_cert=False)
|
||||||
|
calendar = caldav.Calendar(client=client, url=url)
|
||||||
|
returned_events = []
|
||||||
|
|
||||||
|
events_found = calendar.date_search(
|
||||||
|
start=date_start, end=date_end,
|
||||||
|
compfilter='VEVENT', expand=True)
|
||||||
|
if events_found:
|
||||||
|
for event in events_found:
|
||||||
|
cal = icalendar.Calendar.from_ical(event.data)
|
||||||
|
single_event = {}
|
||||||
|
for event in cal.walk('vevent'):
|
||||||
|
date_start = event.get('dtstart')
|
||||||
|
duration = event.get('duration')
|
||||||
|
summary = event.get('summary')
|
||||||
|
single_event['event_start'] = date_start.dt.astimezone(self.tz)
|
||||||
|
single_event['event_end'] = (date_start.dt + duration.dt).astimezone(self.tz)
|
||||||
|
single_event['event_title'] = summary
|
||||||
|
returned_events.append(single_event)
|
||||||
|
|
||||||
|
return returned_events
|
25
drivers/einkdriver.py
Normal file
25
drivers/einkdriver.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from . import epd7in5bc
|
||||||
|
|
||||||
|
class EinkDriver():
|
||||||
|
def __init__(self, xres, yres):
|
||||||
|
self.epd = epd7in5bc.EPD()
|
||||||
|
self.epd.init()
|
||||||
|
self.epd.Clear()
|
||||||
|
self.xres = xres
|
||||||
|
self.yres = yres
|
||||||
|
|
||||||
|
# image1: black/white image
|
||||||
|
# image2: black/yellow image
|
||||||
|
def display(self, image1=None, image2=None):
|
||||||
|
self.epd.init()
|
||||||
|
self.epd.display(self.epd.getbuffer(image1), self.epd.getbuffer(image2))
|
||||||
|
self.epd.sleep()
|
||||||
|
|
||||||
|
def end(self):
|
||||||
|
print("Ending e-ink driver")
|
||||||
|
self.epd.init()
|
||||||
|
self.epd.Clear()
|
||||||
|
self.epd.sleep()
|
||||||
|
self.epd.Dev_exit()
|
||||||
|
print("e-ink driver finished")
|
||||||
|
|
197
drivers/epd7in5bc.py
Normal file
197
drivers/epd7in5bc.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# *****************************************************************************
|
||||||
|
# * | File : epd7in5bc.py
|
||||||
|
# * | Author : Waveshare team
|
||||||
|
# * | Function : Electronic paper driver
|
||||||
|
# * | Info :
|
||||||
|
# *----------------
|
||||||
|
# * | This version: V4.0
|
||||||
|
# * | Date : 2019-06-20
|
||||||
|
# # | Info : python demo
|
||||||
|
# -----------------------------------------------------------------------------
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documnetation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from . import epdconfig
|
||||||
|
|
||||||
|
# Display resolution
|
||||||
|
EPD_WIDTH = 640
|
||||||
|
EPD_HEIGHT = 384
|
||||||
|
|
||||||
|
class EPD:
|
||||||
|
def __init__(self):
|
||||||
|
self.reset_pin = epdconfig.RST_PIN
|
||||||
|
self.dc_pin = epdconfig.DC_PIN
|
||||||
|
self.busy_pin = epdconfig.BUSY_PIN
|
||||||
|
self.cs_pin = epdconfig.CS_PIN
|
||||||
|
self.width = EPD_WIDTH
|
||||||
|
self.height = EPD_HEIGHT
|
||||||
|
|
||||||
|
# Hardware reset
|
||||||
|
def reset(self):
|
||||||
|
epdconfig.digital_write(self.reset_pin, 1)
|
||||||
|
epdconfig.delay_ms(200)
|
||||||
|
epdconfig.digital_write(self.reset_pin, 0)
|
||||||
|
epdconfig.delay_ms(10)
|
||||||
|
epdconfig.digital_write(self.reset_pin, 1)
|
||||||
|
epdconfig.delay_ms(200)
|
||||||
|
|
||||||
|
def send_command(self, command):
|
||||||
|
epdconfig.digital_write(self.dc_pin, 0)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 0)
|
||||||
|
epdconfig.spi_writebyte([command])
|
||||||
|
epdconfig.digital_write(self.cs_pin, 1)
|
||||||
|
|
||||||
|
def send_data(self, data):
|
||||||
|
epdconfig.digital_write(self.dc_pin, 1)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 0)
|
||||||
|
epdconfig.spi_writebyte([data])
|
||||||
|
epdconfig.digital_write(self.cs_pin, 1)
|
||||||
|
|
||||||
|
def send_data_array(self, data):
|
||||||
|
epdconfig.digital_write(self.dc_pin, 1)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 0)
|
||||||
|
epdconfig.SPI.writebytes2(data)
|
||||||
|
epdconfig.digital_write(self.cs_pin, 1)
|
||||||
|
|
||||||
|
def ReadBusy(self):
|
||||||
|
logging.debug("e-Paper busy")
|
||||||
|
while(epdconfig.digital_read(self.busy_pin) == 0): # 0: idle, 1: busy
|
||||||
|
epdconfig.delay_ms(100)
|
||||||
|
logging.debug("e-Paper busy release")
|
||||||
|
|
||||||
|
def init(self):
|
||||||
|
if (epdconfig.module_init() != 0):
|
||||||
|
return -1
|
||||||
|
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
self.send_command(0x01) # POWER_SETTING
|
||||||
|
self.send_data(0x37)
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.send_command(0x00) # PANEL_SETTING
|
||||||
|
self.send_data(0xCF)
|
||||||
|
self.send_data(0x08)
|
||||||
|
|
||||||
|
self.send_command(0x30) # PLL_CONTROL
|
||||||
|
self.send_data(0x3A) # PLL: 0-15:0x3C, 15+:0x3A
|
||||||
|
|
||||||
|
self.send_command(0x82) # VCM_DC_SETTING
|
||||||
|
self.send_data(0x28) #all temperature range
|
||||||
|
|
||||||
|
self.send_command(0x06) # BOOSTER_SOFT_START
|
||||||
|
self.send_data(0xc7)
|
||||||
|
self.send_data(0xcc)
|
||||||
|
self.send_data(0x15)
|
||||||
|
|
||||||
|
self.send_command(0x50) # VCOM AND DATA INTERVAL SETTING
|
||||||
|
self.send_data(0x77)
|
||||||
|
|
||||||
|
self.send_command(0x60) # TCON_SETTING
|
||||||
|
self.send_data(0x22)
|
||||||
|
|
||||||
|
self.send_command(0x65) # FLASH CONTROL
|
||||||
|
self.send_data(0x00)
|
||||||
|
|
||||||
|
self.send_command(0x61) # TCON_RESOLUTION
|
||||||
|
self.send_data(self.width >> 8) # source 640
|
||||||
|
self.send_data(self.width & 0xff)
|
||||||
|
self.send_data(self.height >> 8) # gate 384
|
||||||
|
self.send_data(self.height & 0xff)
|
||||||
|
|
||||||
|
self.send_command(0xe5) # FLASH MODE
|
||||||
|
self.send_data(0x03)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def getbuffer(self, image):
|
||||||
|
img = image
|
||||||
|
imwidth, imheight = img.size
|
||||||
|
if(imwidth == self.width and imheight == self.height):
|
||||||
|
img = img.convert('1')
|
||||||
|
elif(imwidth == self.height and imheight == self.width):
|
||||||
|
img = img.rotate(90, expand=True).convert('1')
|
||||||
|
else:
|
||||||
|
logging.warning("Wrong image dimensions: must be " + str(self.width) + "x" + str(self.height))
|
||||||
|
# return a blank buffer
|
||||||
|
return [0x00] * (int(self.width/8) * self.height)
|
||||||
|
|
||||||
|
buf = bytearray(img.tobytes('raw'))
|
||||||
|
return buf
|
||||||
|
|
||||||
|
def display(self, imageblack, imagered):
|
||||||
|
buf = [0x00] * ((self.width // 2) * self.height)
|
||||||
|
|
||||||
|
for i in range(0, int(self.width / 8 * self.height)):
|
||||||
|
temp1 = imageblack[i]
|
||||||
|
temp2 = imagered[i]
|
||||||
|
j = 0
|
||||||
|
while (j < 8):
|
||||||
|
if ((temp2 & 0x80) == 0x00):
|
||||||
|
temp3 = 0x04 #red
|
||||||
|
elif ((temp1 & 0x80) == 0x00):
|
||||||
|
temp3 = 0x00 #black
|
||||||
|
else:
|
||||||
|
temp3 = 0x03 #white
|
||||||
|
|
||||||
|
temp3 = (temp3 << 4) & 0xFF
|
||||||
|
temp1 = (temp1 << 1) & 0xFF
|
||||||
|
temp2 = (temp2 << 1) & 0xFF
|
||||||
|
j += 1
|
||||||
|
if((temp2 & 0x80) == 0x00):
|
||||||
|
temp3 |= 0x04 #red
|
||||||
|
elif ((temp1 & 0x80) == 0x00):
|
||||||
|
temp3 |= 0x00 #black
|
||||||
|
else:
|
||||||
|
temp3 |= 0x03 #white
|
||||||
|
temp1 = (temp1 << 1) & 0xFF
|
||||||
|
temp2 = (temp2 << 1) & 0xFF
|
||||||
|
buf[i * 4 + (j//2)] = temp3
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
self.send_command(0x10)
|
||||||
|
self.send_data_array(buf)
|
||||||
|
self.send_command(0x04) # POWER ON
|
||||||
|
self.ReadBusy()
|
||||||
|
self.send_command(0x12) # display refresh
|
||||||
|
epdconfig.delay_ms(100)
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
def Clear(self):
|
||||||
|
buf = [0x33] * ((self.width // 2) * self.height)
|
||||||
|
self.send_command(0x10)
|
||||||
|
self.send_data_array(buf)
|
||||||
|
self.send_command(0x04) # POWER ON
|
||||||
|
self.ReadBusy()
|
||||||
|
self.send_command(0x12) # display refresh
|
||||||
|
epdconfig.delay_ms(100)
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
def sleep(self):
|
||||||
|
self.send_command(0x02) # POWER_OFF
|
||||||
|
self.ReadBusy()
|
||||||
|
|
||||||
|
self.send_command(0x07) # DEEP_SLEEP
|
||||||
|
self.send_data(0XA5)
|
||||||
|
|
||||||
|
def Dev_exit(self):
|
||||||
|
epdconfig.module_exit()
|
||||||
|
### END OF FILE ###
|
156
drivers/epdconfig.py
Normal file
156
drivers/epdconfig.py
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
# /*****************************************************************************
|
||||||
|
# * | File : epdconfig.py
|
||||||
|
# * | Author : Waveshare team
|
||||||
|
# * | Function : Hardware underlying interface
|
||||||
|
# * | Info :
|
||||||
|
# *----------------
|
||||||
|
# * | This version: V1.0
|
||||||
|
# * | Date : 2019-06-21
|
||||||
|
# * | Info :
|
||||||
|
# ******************************************************************************
|
||||||
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
# of this software and associated documnetation files (the "Software"), to deal
|
||||||
|
# in the Software without restriction, including without limitation the rights
|
||||||
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
# copies of the Software, and to permit persons to whom the Software is
|
||||||
|
# furished to do so, subject to the following conditions:
|
||||||
|
#
|
||||||
|
# The above copyright notice and this permission notice shall be included in
|
||||||
|
# all copies or substantial portions of the Software.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
# FITNESS OR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
# LIABILITY WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
# THE SOFTWARE.
|
||||||
|
#
|
||||||
|
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class RaspberryPi:
|
||||||
|
# Pin definition
|
||||||
|
RST_PIN = 17
|
||||||
|
DC_PIN = 25
|
||||||
|
CS_PIN = 8
|
||||||
|
BUSY_PIN = 24
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
import spidev
|
||||||
|
import RPi.GPIO
|
||||||
|
|
||||||
|
self.GPIO = RPi.GPIO
|
||||||
|
|
||||||
|
# SPI device, bus = 0, device = 0
|
||||||
|
self.SPI = spidev.SpiDev(0, 0)
|
||||||
|
|
||||||
|
def digital_write(self, pin, value):
|
||||||
|
self.GPIO.output(pin, value)
|
||||||
|
|
||||||
|
def digital_read(self, pin):
|
||||||
|
return self.GPIO.input(pin)
|
||||||
|
|
||||||
|
def delay_ms(self, delaytime):
|
||||||
|
time.sleep(delaytime / 1000.0)
|
||||||
|
|
||||||
|
def spi_writebyte(self, data):
|
||||||
|
self.SPI.writebytes(data)
|
||||||
|
|
||||||
|
def module_init(self):
|
||||||
|
self.GPIO.setmode(self.GPIO.BCM)
|
||||||
|
self.GPIO.setwarnings(False)
|
||||||
|
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
|
||||||
|
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
|
||||||
|
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
|
||||||
|
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
|
||||||
|
self.SPI.max_speed_hz = 4000000
|
||||||
|
self.SPI.mode = 0b00
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def module_exit(self):
|
||||||
|
logging.debug("spi end")
|
||||||
|
self.SPI.close()
|
||||||
|
|
||||||
|
logging.debug("close 5V, Module enters 0 power consumption ...")
|
||||||
|
self.GPIO.setmode(self.GPIO.BCM)
|
||||||
|
self.GPIO.output(self.RST_PIN, 0)
|
||||||
|
self.GPIO.output(self.DC_PIN, 0)
|
||||||
|
|
||||||
|
self.GPIO.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
class JetsonNano:
|
||||||
|
# Pin definition
|
||||||
|
RST_PIN = 17
|
||||||
|
DC_PIN = 25
|
||||||
|
CS_PIN = 8
|
||||||
|
BUSY_PIN = 24
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
import ctypes
|
||||||
|
find_dirs = [
|
||||||
|
os.path.dirname(os.path.realpath(__file__)),
|
||||||
|
'/usr/local/lib',
|
||||||
|
'/usr/lib',
|
||||||
|
]
|
||||||
|
self.SPI = None
|
||||||
|
for find_dir in find_dirs:
|
||||||
|
so_filename = os.path.join(find_dir, 'sysfs_software_spi.so')
|
||||||
|
if os.path.exists(so_filename):
|
||||||
|
self.SPI = ctypes.cdll.LoadLibrary(so_filename)
|
||||||
|
break
|
||||||
|
if self.SPI is None:
|
||||||
|
raise RuntimeError('Cannot find sysfs_software_spi.so')
|
||||||
|
|
||||||
|
import Jetson.GPIO
|
||||||
|
self.GPIO = Jetson.GPIO
|
||||||
|
|
||||||
|
def digital_write(self, pin, value):
|
||||||
|
self.GPIO.output(pin, value)
|
||||||
|
|
||||||
|
def digital_read(self, pin):
|
||||||
|
return self.GPIO.input(self.BUSY_PIN)
|
||||||
|
|
||||||
|
def delay_ms(self, delaytime):
|
||||||
|
time.sleep(delaytime / 1000.0)
|
||||||
|
|
||||||
|
def spi_writebyte(self, data):
|
||||||
|
self.SPI.SYSFS_software_spi_transfer(data[0])
|
||||||
|
|
||||||
|
def module_init(self):
|
||||||
|
self.GPIO.setmode(self.GPIO.BCM)
|
||||||
|
self.GPIO.setwarnings(False)
|
||||||
|
self.GPIO.setup(self.RST_PIN, self.GPIO.OUT)
|
||||||
|
self.GPIO.setup(self.DC_PIN, self.GPIO.OUT)
|
||||||
|
self.GPIO.setup(self.CS_PIN, self.GPIO.OUT)
|
||||||
|
self.GPIO.setup(self.BUSY_PIN, self.GPIO.IN)
|
||||||
|
self.SPI.SYSFS_software_spi_begin()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def module_exit(self):
|
||||||
|
logging.debug("spi end")
|
||||||
|
self.SPI.SYSFS_software_spi_end()
|
||||||
|
|
||||||
|
logging.debug("close 5V, Module enters 0 power consumption ...")
|
||||||
|
self.GPIO.output(self.RST_PIN, 0)
|
||||||
|
self.GPIO.output(self.DC_PIN, 0)
|
||||||
|
|
||||||
|
self.GPIO.cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
if os.path.exists('/sys/bus/platform/drivers/gpiomem-bcm2835'):
|
||||||
|
implementation = RaspberryPi()
|
||||||
|
else:
|
||||||
|
implementation = JetsonNano()
|
||||||
|
|
||||||
|
for func in [x for x in dir(implementation) if not x.startswith('_')]:
|
||||||
|
setattr(sys.modules[__name__], func, getattr(implementation, func))
|
||||||
|
|
||||||
|
|
||||||
|
### END OF FILE ###
|
||||||
|
|
52
drivers/gpiodriver.py
Normal file
52
drivers/gpiodriver.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import RPi.GPIO as GPIO
|
||||||
|
import time
|
||||||
|
|
||||||
|
row_channels = [6, 5, 19, 13]
|
||||||
|
column_channel = 26
|
||||||
|
|
||||||
|
class GPIODriver():
|
||||||
|
def __init__(self):
|
||||||
|
GPIO.setmode(GPIO.BCM)
|
||||||
|
GPIO.setup(row_channels, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
|
||||||
|
GPIO.setup(column_channel, GPIO.OUT)
|
||||||
|
|
||||||
|
def wait_for_keypress(self, timeout=60):
|
||||||
|
self.pressed = None
|
||||||
|
|
||||||
|
def callback_1(channel):
|
||||||
|
# print("Pressed key 1")
|
||||||
|
self.pressed = 1
|
||||||
|
|
||||||
|
def callback_2(channel):
|
||||||
|
# print("Pressed key 2")
|
||||||
|
self.pressed = 2
|
||||||
|
|
||||||
|
def callback_3(channel):
|
||||||
|
# print("Pressed key 3")
|
||||||
|
self.pressed = 3
|
||||||
|
|
||||||
|
def callback_4(channel):
|
||||||
|
# print("Pressed key 4")
|
||||||
|
self.pressed = 4
|
||||||
|
|
||||||
|
GPIO.add_event_detect(row_channels[0], GPIO.RISING, callback=callback_1, bouncetime=500)
|
||||||
|
GPIO.add_event_detect(row_channels[1], GPIO.RISING, callback=callback_2, bouncetime=500)
|
||||||
|
GPIO.add_event_detect(row_channels[2], GPIO.RISING, callback=callback_3, bouncetime=500)
|
||||||
|
GPIO.add_event_detect(row_channels[3], GPIO.RISING, callback=callback_4, bouncetime=500)
|
||||||
|
GPIO.output(column_channel, GPIO.HIGH)
|
||||||
|
start_time = datetime.now()
|
||||||
|
while not self.pressed:
|
||||||
|
time.sleep(1)
|
||||||
|
current_time = datetime.now()
|
||||||
|
delta = current_time - start_time
|
||||||
|
if delta.seconds > timeout:
|
||||||
|
for chan in row_channels:
|
||||||
|
GPIO.remove_event_detect(chan)
|
||||||
|
GPIO.output(column_channel, GPIO.LOW)
|
||||||
|
return None
|
||||||
|
|
||||||
|
for chan in row_channels:
|
||||||
|
GPIO.remove_event_detect(chan)
|
||||||
|
GPIO.output(column_channel, GPIO.LOW)
|
||||||
|
return self.pressed
|
61
drivers/pygamedriver.py
Normal file
61
drivers/pygamedriver.py
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import pygame
|
||||||
|
import sys
|
||||||
|
|
||||||
|
class PygameDriver():
|
||||||
|
def __init__(self, xres, yres):
|
||||||
|
pygame.init()
|
||||||
|
self.screen = pygame.display.set_mode((xres, yres))
|
||||||
|
self.xres = xres
|
||||||
|
self.yres = yres
|
||||||
|
|
||||||
|
# image1: black/white image
|
||||||
|
# image2: black/yellow image
|
||||||
|
def display(self, image1=None, image2=None):
|
||||||
|
if image1:
|
||||||
|
raw_str1 = image1.convert('RGBA').tobytes("raw", 'RGBA')
|
||||||
|
pygame_surface1 = pygame.image.fromstring(raw_str1, (self.xres, self.yres), 'RGBA')
|
||||||
|
self.screen.blit(pygame_surface1, (0, 0))
|
||||||
|
|
||||||
|
if image2:
|
||||||
|
raw_str2 = image2.convert('RGB').tobytes("raw", 'RGB')
|
||||||
|
pygame_surface2 = pygame.image.fromstring(raw_str2, (self.xres, self.yres), 'RGB')
|
||||||
|
pygame_surface2.set_colorkey((255, 255, 255))
|
||||||
|
|
||||||
|
image_pixel_array = pygame.PixelArray(pygame_surface2)
|
||||||
|
image_pixel_array.replace((0, 0, 0), (127, 100, 0))
|
||||||
|
del image_pixel_array
|
||||||
|
|
||||||
|
self.screen.blit(pygame_surface2, (0, 0))
|
||||||
|
|
||||||
|
pygame.display.flip()
|
||||||
|
|
||||||
|
def end(self):
|
||||||
|
pygame.quit()
|
||||||
|
|
||||||
|
class PygameKBDriver():
|
||||||
|
def __init__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def wait_for_keypress(self, timeout=60):
|
||||||
|
start_time = datetime.now()
|
||||||
|
while True:
|
||||||
|
current_time = datetime.now()
|
||||||
|
delta = current_time - start_time
|
||||||
|
if delta.seconds > timeout:
|
||||||
|
return None
|
||||||
|
events = pygame.event.get()
|
||||||
|
for event in events:
|
||||||
|
if event.type == pygame.QUIT:
|
||||||
|
pygame.quit()
|
||||||
|
sys.exit(0)
|
||||||
|
elif event.type == pygame.KEYDOWN:
|
||||||
|
if event.key == pygame.K_1:
|
||||||
|
return 1
|
||||||
|
elif event.key == pygame.K_2:
|
||||||
|
return 2
|
||||||
|
elif event.key == pygame.K_3:
|
||||||
|
return 3
|
||||||
|
elif event.key == pygame.K_4:
|
||||||
|
return 4
|
||||||
|
pygame.time.wait(1000)
|
BIN
fonts/DejaVuSansMono-Bold.ttf
Normal file
BIN
fonts/DejaVuSansMono-Bold.ttf
Normal file
Binary file not shown.
187
fonts/LICENSE-DejaVuSansMono.txt
Normal file
187
fonts/LICENSE-DejaVuSansMono.txt
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
|
||||||
|
Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below)
|
||||||
|
|
||||||
|
|
||||||
|
Bitstream Vera Fonts Copyright
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is
|
||||||
|
a trademark of Bitstream, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of the fonts accompanying this license ("Fonts") and associated
|
||||||
|
documentation files (the "Font Software"), to reproduce and distribute the
|
||||||
|
Font Software, including without limitation the rights to use, copy, merge,
|
||||||
|
publish, distribute, and/or sell copies of the Font Software, and to permit
|
||||||
|
persons to whom the Font Software is furnished to do so, subject to the
|
||||||
|
following conditions:
|
||||||
|
|
||||||
|
The above copyright and trademark notices and this permission notice shall
|
||||||
|
be included in all copies of one or more of the Font Software typefaces.
|
||||||
|
|
||||||
|
The Font Software may be modified, altered, or added to, and in particular
|
||||||
|
the designs of glyphs or characters in the Fonts may be modified and
|
||||||
|
additional glyphs or characters may be added to the Fonts, only if the fonts
|
||||||
|
are renamed to names not containing either the words "Bitstream" or the word
|
||||||
|
"Vera".
|
||||||
|
|
||||||
|
This License becomes null and void to the extent applicable to Fonts or Font
|
||||||
|
Software that has been modified and is distributed under the "Bitstream
|
||||||
|
Vera" names.
|
||||||
|
|
||||||
|
The Font Software may be sold as part of a larger software package but no
|
||||||
|
copy of one or more of the Font Software typefaces may be sold by itself.
|
||||||
|
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
||||||
|
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
||||||
|
FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING
|
||||||
|
ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
|
||||||
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||||
|
THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE
|
||||||
|
FONT SOFTWARE.
|
||||||
|
|
||||||
|
Except as contained in this notice, the names of Gnome, the Gnome
|
||||||
|
Foundation, and Bitstream Inc., shall not be used in advertising or
|
||||||
|
otherwise to promote the sale, use or other dealings in this Font Software
|
||||||
|
without prior written authorization from the Gnome Foundation or Bitstream
|
||||||
|
Inc., respectively. For further information, contact: fonts at gnome dot
|
||||||
|
org.
|
||||||
|
|
||||||
|
Arev Fonts Copyright
|
||||||
|
------------------------------
|
||||||
|
|
||||||
|
Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the fonts accompanying this license ("Fonts") and
|
||||||
|
associated documentation files (the "Font Software"), to reproduce
|
||||||
|
and distribute the modifications to the Bitstream Vera Font Software,
|
||||||
|
including without limitation the rights to use, copy, merge, publish,
|
||||||
|
distribute, and/or sell copies of the Font Software, and to permit
|
||||||
|
persons to whom the Font Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright and trademark notices and this permission notice
|
||||||
|
shall be included in all copies of one or more of the Font Software
|
||||||
|
typefaces.
|
||||||
|
|
||||||
|
The Font Software may be modified, altered, or added to, and in
|
||||||
|
particular the designs of glyphs or characters in the Fonts may be
|
||||||
|
modified and additional glyphs or characters may be added to the
|
||||||
|
Fonts, only if the fonts are renamed to names not containing either
|
||||||
|
the words "Tavmjong Bah" or the word "Arev".
|
||||||
|
|
||||||
|
This License becomes null and void to the extent applicable to Fonts
|
||||||
|
or Font Software that has been modified and is distributed under the
|
||||||
|
"Tavmjong Bah Arev" names.
|
||||||
|
|
||||||
|
The Font Software may be sold as part of a larger software package but
|
||||||
|
no copy of one or more of the Font Software typefaces may be sold by
|
||||||
|
itself.
|
||||||
|
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL
|
||||||
|
TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
|
||||||
|
Except as contained in this notice, the name of Tavmjong Bah shall not
|
||||||
|
be used in advertising or otherwise to promote the sale, use or other
|
||||||
|
dealings in this Font Software without prior written authorization
|
||||||
|
from Tavmjong Bah. For further information, contact: tavmjong @ free
|
||||||
|
. fr.
|
||||||
|
|
||||||
|
TeX Gyre DJV Math
|
||||||
|
-----------------
|
||||||
|
Fonts are (c) Bitstream (see below). DejaVu changes are in public domain.
|
||||||
|
|
||||||
|
Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski
|
||||||
|
(on behalf of TeX users groups) are in public domain.
|
||||||
|
|
||||||
|
Letters imported from Euler Fraktur from AMSfonts are (c) American
|
||||||
|
Mathematical Society (see below).
|
||||||
|
Bitstream Vera Fonts Copyright
|
||||||
|
Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera
|
||||||
|
is a trademark of Bitstream, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of the fonts accompanying this license (“Fonts”) and associated
|
||||||
|
documentation
|
||||||
|
files (the “Font Software”), to reproduce and distribute the Font Software,
|
||||||
|
including without limitation the rights to use, copy, merge, publish,
|
||||||
|
distribute,
|
||||||
|
and/or sell copies of the Font Software, and to permit persons to whom
|
||||||
|
the Font Software is furnished to do so, subject to the following
|
||||||
|
conditions:
|
||||||
|
|
||||||
|
The above copyright and trademark notices and this permission notice
|
||||||
|
shall be
|
||||||
|
included in all copies of one or more of the Font Software typefaces.
|
||||||
|
|
||||||
|
The Font Software may be modified, altered, or added to, and in particular
|
||||||
|
the designs of glyphs or characters in the Fonts may be modified and
|
||||||
|
additional
|
||||||
|
glyphs or characters may be added to the Fonts, only if the fonts are
|
||||||
|
renamed
|
||||||
|
to names not containing either the words “Bitstream” or the word “Vera”.
|
||||||
|
|
||||||
|
This License becomes null and void to the extent applicable to Fonts or
|
||||||
|
Font Software
|
||||||
|
that has been modified and is distributed under the “Bitstream Vera”
|
||||||
|
names.
|
||||||
|
|
||||||
|
The Font Software may be sold as part of a larger software package but
|
||||||
|
no copy
|
||||||
|
of one or more of the Font Software typefaces may be sold by itself.
|
||||||
|
|
||||||
|
THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT,
|
||||||
|
TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME
|
||||||
|
FOUNDATION
|
||||||
|
BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL,
|
||||||
|
SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN
|
||||||
|
ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
|
||||||
|
INABILITY TO USE
|
||||||
|
THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
|
Except as contained in this notice, the names of GNOME, the GNOME
|
||||||
|
Foundation,
|
||||||
|
and Bitstream Inc., shall not be used in advertising or otherwise to promote
|
||||||
|
the sale, use or other dealings in this Font Software without prior written
|
||||||
|
authorization from the GNOME Foundation or Bitstream Inc., respectively.
|
||||||
|
For further information, contact: fonts at gnome dot org.
|
||||||
|
|
||||||
|
AMSFonts (v. 2.2) copyright
|
||||||
|
|
||||||
|
The PostScript Type 1 implementation of the AMSFonts produced by and
|
||||||
|
previously distributed by Blue Sky Research and Y&Y, Inc. are now freely
|
||||||
|
available for general use. This has been accomplished through the
|
||||||
|
cooperation
|
||||||
|
of a consortium of scientific publishers with Blue Sky Research and Y&Y.
|
||||||
|
Members of this consortium include:
|
||||||
|
|
||||||
|
Elsevier Science IBM Corporation Society for Industrial and Applied
|
||||||
|
Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS)
|
||||||
|
|
||||||
|
In order to assure the authenticity of these fonts, copyright will be
|
||||||
|
held by
|
||||||
|
the American Mathematical Society. This is not meant to restrict in any way
|
||||||
|
the legitimate use of the fonts, such as (but not limited to) electronic
|
||||||
|
distribution of documents containing these fonts, inclusion of these fonts
|
||||||
|
into other public domain or commercial font collections or computer
|
||||||
|
applications, use of the outline data to create derivative fonts and/or
|
||||||
|
faces, etc. However, the AMS does require that the AMS copyright notice be
|
||||||
|
removed from any derivative versions of the fonts which have been altered in
|
||||||
|
any way. In addition, to ensure the fidelity of TeX documents using Computer
|
||||||
|
Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces,
|
||||||
|
has requested that any alterations which yield different font metrics be
|
||||||
|
given a different name.
|
||||||
|
|
||||||
|
$Id$
|
3
fonts/license-weathericons-regular-webfont.txt
Normal file
3
fonts/license-weathericons-regular-webfont.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Weather Icons licensed under SIL OFL 1.1: http://scripts.sil.org/OFL.
|
||||||
|
|
||||||
|
You can get the fonts at https://github.com/erikflowers/weather-icons
|
BIN
fonts/weathericons-regular-webfont.ttf
Normal file
BIN
fonts/weathericons-regular-webfont.ttf
Normal file
Binary file not shown.
BIN
img/night_image.jpg
Normal file
BIN
img/night_image.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
152
mycalendar.py
Executable file
152
mycalendar.py
Executable file
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# Inspiration from https://github.com/zli117/EInk-Calendar/tree/master/resources
|
||||||
|
|
||||||
|
import configparser
|
||||||
|
import os
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from PIL import Image, ImageFont, ImageDraw
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from drivers.caldavprovider import CalDavProvider
|
||||||
|
from widgets.calendarwidget import CalendarWidget
|
||||||
|
from widgets.timewidget import TimeWidget
|
||||||
|
from widgets.weatherwidget import WeatherWidget
|
||||||
|
|
||||||
|
def today_calendar():
|
||||||
|
fullimg = Image.new('1', (640, 384), color=255)
|
||||||
|
fullimg2 = Image.new('1', (640, 384), color=255)
|
||||||
|
weather = WeatherWidget(owm_api_key)
|
||||||
|
fullimg.paste(weather.get_weather(owm_location), box=(0, 0))
|
||||||
|
timewidget = TimeWidget()
|
||||||
|
fullimg.paste(timewidget.get_time(), box=(128, 0))
|
||||||
|
calendar = CalendarWidget(family_names)
|
||||||
|
event_getter = CalDavProvider(calendar_username, calendar_password)
|
||||||
|
test_event_list = []
|
||||||
|
|
||||||
|
today_start = datetime.now().replace(hour=8, minute=0, second=0, microsecond=0)
|
||||||
|
today_end = today_start.replace(hour=21, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
|
for url in calendar_list:
|
||||||
|
test_event_list.append(event_getter.get_calendar(url, today_start, today_end))
|
||||||
|
img1, img2 = calendar.get_calendar(test_event_list)
|
||||||
|
fullimg.paste(img1, box=(0, 80))
|
||||||
|
fullimg2.paste(img2, box=(0, 80))
|
||||||
|
return fullimg, fullimg2
|
||||||
|
|
||||||
|
def week_calendar(calid, name):
|
||||||
|
fullimg = Image.new('1', (640, 384), color=255)
|
||||||
|
fullimg2 = Image.new('1', (640, 384), color=255)
|
||||||
|
weather = WeatherWidget(owm_api_key)
|
||||||
|
fullimg.paste(weather.get_weather(owm_location), box=(0, 0))
|
||||||
|
timewidget = TimeWidget()
|
||||||
|
fullimg.paste(timewidget.get_time(), box=(128, 0))
|
||||||
|
event_getter = CalDavProvider(calendar_username, calendar_password)
|
||||||
|
test_event_list = []
|
||||||
|
|
||||||
|
today = datetime.today()
|
||||||
|
curdate = today + timedelta(days=-today.weekday(), weeks=0)
|
||||||
|
curdate = curdate.replace(hour=8, minute=0, second=0, microsecond=0)
|
||||||
|
|
||||||
|
for day in range(7, 12):
|
||||||
|
test_event_list.append(event_getter.get_calendar(calendar_list[calid], curdate, curdate.replace(hour=21)))
|
||||||
|
curdate = curdate + timedelta(days=1)
|
||||||
|
|
||||||
|
calendar = CalendarWidget(['L', 'M', 'X', 'J', 'V'])
|
||||||
|
|
||||||
|
img1, img2 = calendar.get_calendar(test_event_list, title=name)
|
||||||
|
fullimg.paste(img1, box=(0, 80))
|
||||||
|
fullimg2.paste(img2, box=(0, 80))
|
||||||
|
return fullimg, fullimg2
|
||||||
|
|
||||||
|
def special_image():
|
||||||
|
today = datetime.now()
|
||||||
|
filename = '%02d-%02d-%4d' % (today.day, today.month, today.year)
|
||||||
|
filename_jpg = filename + '.jpg'
|
||||||
|
filename_png = filename + '.png'
|
||||||
|
img = None
|
||||||
|
img2 = Image.new('1', (640, 384), color=255)
|
||||||
|
if os.path.exists(os.path.join('./img', filename_jpg)):
|
||||||
|
img = Image.open(os.path.join('./img', filename_jpg)).convert('1').resize((640,384))
|
||||||
|
elif os.path.exists(os.path.join('./img', filename_png)):
|
||||||
|
img = Image.open(os.path.join('./img', filename_png)).convert('1').resize((640,384))
|
||||||
|
if img:
|
||||||
|
return img, img2, True
|
||||||
|
return img2, img2, False
|
||||||
|
|
||||||
|
def night_image():
|
||||||
|
img = Image.open(os.path.join('./img', 'night_image.jpg')).convert('1').resize((640,384))
|
||||||
|
img2 = Image.new('1', (640, 384), color=255)
|
||||||
|
return img, img2
|
||||||
|
|
||||||
|
cp = configparser.RawConfigParser()
|
||||||
|
cp.read('config.ini')
|
||||||
|
video_driver = cp.get('DEFAULT', 'video')
|
||||||
|
kbd_driver = cp.get('DEFAULT', 'keyboard')
|
||||||
|
owm_api_key = cp.get('weather', 'owm_api_key')
|
||||||
|
owm_location = cp.getint('weather', 'owm_location')
|
||||||
|
calendar_username = cp.get('calendar', 'username')
|
||||||
|
calendar_password = cp.get('calendar', 'password')
|
||||||
|
calendar_list=[]
|
||||||
|
for item in cp.get('calendar', 'urls').split(','):
|
||||||
|
calendar_list.append(item.strip())
|
||||||
|
family_names = []
|
||||||
|
for item in cp.get('calendar', 'names').split(','):
|
||||||
|
family_names.append(item.strip())
|
||||||
|
|
||||||
|
if video_driver == 'pygame':
|
||||||
|
from drivers.pygamedriver import PygameDriver
|
||||||
|
video = PygameDriver(640, 384)
|
||||||
|
elif video_driver == 'eink':
|
||||||
|
from drivers.einkdriver import EinkDriver
|
||||||
|
video = EinkDriver(640, 384)
|
||||||
|
|
||||||
|
if kbd_driver == 'pygame':
|
||||||
|
from drivers.pygamedriver import PygameKBDriver
|
||||||
|
keyboard = PygameKBDriver()
|
||||||
|
elif kbd_driver == 'gpio':
|
||||||
|
from drivers.gpiodriver import GPIODriver
|
||||||
|
keyboard = GPIODriver()
|
||||||
|
|
||||||
|
current_screen = 0
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
print("Reading calendar data", flush=True)
|
||||||
|
start_time = datetime.now()
|
||||||
|
images = []
|
||||||
|
images.append(today_calendar())
|
||||||
|
images.append(week_calendar(0, family_names[0]))
|
||||||
|
images.append(week_calendar(1, family_names[1]))
|
||||||
|
images.append(week_calendar(2, family_names[2]))
|
||||||
|
special_img1, special_img2, is_today_special = special_image()
|
||||||
|
if not is_today_special:
|
||||||
|
special_img1, special_img2 = night_image()
|
||||||
|
images.append([special_img1, special_img2])
|
||||||
|
|
||||||
|
done = False
|
||||||
|
refresh = True
|
||||||
|
while not done:
|
||||||
|
if refresh:
|
||||||
|
print("Display", flush=True)
|
||||||
|
video.display(image1=images[current_screen][0], image2=images[current_screen][1])
|
||||||
|
refresh = False
|
||||||
|
print("Wait for keypress", flush=True)
|
||||||
|
value = keyboard.wait_for_keypress(timeout=60)
|
||||||
|
print("Received keypress: %s" % value, flush=True)
|
||||||
|
if value:
|
||||||
|
current_screen = value - 1
|
||||||
|
refresh = True
|
||||||
|
else:
|
||||||
|
current_time = datetime.now()
|
||||||
|
if current_time.hour > 20 or current_time.hour < 7:
|
||||||
|
current_screen = 4
|
||||||
|
else:
|
||||||
|
current_screen = 0
|
||||||
|
current_time = datetime.now()
|
||||||
|
delta_time = current_time - start_time
|
||||||
|
if delta_time.seconds > 600:
|
||||||
|
# After 10 minutes, re-read calendar data
|
||||||
|
done = True
|
||||||
|
finally:
|
||||||
|
print("Quit", flush=True)
|
||||||
|
video.end()
|
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
caldav
|
||||||
|
icalendar
|
||||||
|
pygame
|
||||||
|
pillow
|
||||||
|
pyowm
|
||||||
|
pytz
|
5
reset_eink.py
Executable file
5
reset_eink.py
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from drivers.einkdriver import EinkDriver
|
||||||
|
video = EinkDriver(640, 384)
|
||||||
|
video.end()
|
14
scripts/calendar.service
Normal file
14
scripts/calendar.service
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=My cool e-ink family calendar
|
||||||
|
After=syslog.target network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=pi
|
||||||
|
ExecStart=/home/pi/launcher.sh
|
||||||
|
PrivateTmp=false
|
||||||
|
KillMode=control-group
|
||||||
|
Restart=always
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
5
scripts/launcher.sh
Executable file
5
scripts/launcher.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
cd ~/calendario
|
||||||
|
source .venv/bin/activate
|
||||||
|
./mycalendar.py
|
81
widgets/calendarwidget.py
Normal file
81
widgets/calendarwidget.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from pyowm import OWM
|
||||||
|
from PIL import Image, ImageFont, ImageDraw
|
||||||
|
|
||||||
|
def get_local_ip():
|
||||||
|
import socket
|
||||||
|
"""Try to determine the local IP address of the machine."""
|
||||||
|
try:
|
||||||
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
|
|
||||||
|
# Use Google Public DNS server to determine own IP
|
||||||
|
sock.connect(('192.168.1.1', 80))
|
||||||
|
|
||||||
|
return sock.getsockname()[0]
|
||||||
|
except socket.error:
|
||||||
|
try:
|
||||||
|
return socket.gethostbyname(socket.gethostname())
|
||||||
|
except socket.gaierror:
|
||||||
|
return '127.0.0.1'
|
||||||
|
finally:
|
||||||
|
sock.close()
|
||||||
|
|
||||||
|
class CalendarWidget():
|
||||||
|
def __init__(self, column_names):
|
||||||
|
self.column_names = column_names
|
||||||
|
self.font14 = ImageFont.truetype(os.path.join('./fonts/', 'DejaVuSansMono-Bold.ttf'), 14)
|
||||||
|
|
||||||
|
def create_entry_box(self, inittime, finishtime, text, width):
|
||||||
|
"""
|
||||||
|
both inittime and finishtime are objects of type datetime.datetime
|
||||||
|
"""
|
||||||
|
timediff = finishtime - inittime
|
||||||
|
hours = timediff.seconds / 3600
|
||||||
|
|
||||||
|
img = Image.new('1', (width, int(hours * 20)), color=0)
|
||||||
|
imgdraw = ImageDraw.Draw(img)
|
||||||
|
imgdraw.text((8, 0), text, font=self.font14, fill=255)
|
||||||
|
return img
|
||||||
|
|
||||||
|
def get_calendar(self, column_events, title=None):
|
||||||
|
"""
|
||||||
|
column_events contains a list of columns, and each of them is a list of hashes containing:
|
||||||
|
- event_start: start time
|
||||||
|
- event_end: ending time
|
||||||
|
- event_title: event title
|
||||||
|
"""
|
||||||
|
img = Image.new('1', (640, 304), color=255)
|
||||||
|
img2 = Image.new('1', (640, 304), color=255)
|
||||||
|
imgdraw = ImageDraw.Draw(img)
|
||||||
|
imgdraw2 = ImageDraw.Draw(img2)
|
||||||
|
index = 0
|
||||||
|
distance = 540 // len(self.column_names)
|
||||||
|
boxwidth = 570 // len(self.column_names)
|
||||||
|
|
||||||
|
for column in self.column_names:
|
||||||
|
imgdraw.text((128 + distance*index, 0), column, font=self.font14, fill=0)
|
||||||
|
index += 1
|
||||||
|
if title:
|
||||||
|
imgdraw.text((8, 0), title, font=self.font14, fill=0)
|
||||||
|
imgdraw.text((540, 294), get_local_ip())
|
||||||
|
|
||||||
|
# Draw Calendar lines
|
||||||
|
for i in range(0, 14):
|
||||||
|
imgdraw2.line([(0,30 + 20*i), (639, 30 + 20*i)], fill=0, width=1)
|
||||||
|
for i in range(0, 13):
|
||||||
|
imgdraw.text((0, 30 + 20*i), '%2d:00' % (i+8), font=self.font14, fill=0)
|
||||||
|
|
||||||
|
column_number = 0
|
||||||
|
for column_list in column_events:
|
||||||
|
for event in column_list:
|
||||||
|
if event['event_start'].hour >= 8 or event['event_start'].hour <= 19:
|
||||||
|
eventimg = self.create_entry_box(event['event_start'],
|
||||||
|
event['event_end'],
|
||||||
|
event['event_title'],
|
||||||
|
boxwidth)
|
||||||
|
x = column_number + 64 + (boxwidth * column_number)
|
||||||
|
y = 30 + int(20 * (event['event_start'].hour + (event['event_start'].minute / 60) - 8.0))
|
||||||
|
img2.paste(eventimg, box = (x, y))
|
||||||
|
column_number += 1
|
||||||
|
return img, img2
|
17
widgets/timewidget.py
Normal file
17
widgets/timewidget.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from PIL import Image, ImageFont, ImageDraw
|
||||||
|
|
||||||
|
class TimeWidget():
|
||||||
|
def __init__(self):
|
||||||
|
self.font = ImageFont.truetype(os.path.join('./fonts/', 'DejaVuSansMono-Bold.ttf'), 24)
|
||||||
|
|
||||||
|
def get_time(self):
|
||||||
|
now = datetime.now()
|
||||||
|
img = Image.new('1', (512, 64), color=255)
|
||||||
|
imgdraw = ImageDraw.Draw(img)
|
||||||
|
imgdraw.text((32, 24), '%s, %2d/%2d/%4d' % (self.weekdays[now.isoweekday()], now.day, now.month, now.year), font=self.font, fill=0)
|
||||||
|
imgdraw.text((400, 24), '%02d:%02d' % (now.hour, now.minute), font=self.font, fill=0)
|
||||||
|
return img
|
||||||
|
|
||||||
|
weekdays = ['None', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo']
|
87
widgets/weatherwidget.py
Normal file
87
widgets/weatherwidget.py
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import os
|
||||||
|
from pyowm import OWM
|
||||||
|
from PIL import Image, ImageFont, ImageDraw
|
||||||
|
|
||||||
|
class WeatherWidget():
|
||||||
|
def __init__(self, api_key):
|
||||||
|
self.api_key = api_key
|
||||||
|
self.weatherfont = ImageFont.truetype(os.path.join('./fonts/', 'weathericons-regular-webfont.ttf'), 48)
|
||||||
|
self.font16 = ImageFont.truetype(os.path.join('./fonts/', 'DejaVuSansMono-Bold.ttf'), 16)
|
||||||
|
self.owm = OWM(api_key)
|
||||||
|
|
||||||
|
def get_weather(self, location):
|
||||||
|
mgr = self.owm.weather_manager()
|
||||||
|
current = mgr.weather_at_id(location)
|
||||||
|
w = current.weather
|
||||||
|
temp = w.temperature('celsius')['temp']
|
||||||
|
wcode = w.weather_code
|
||||||
|
img = Image.new('1', (128, 64), color=255)
|
||||||
|
imgdraw = ImageDraw.Draw(img)
|
||||||
|
imgdraw.text((0, 0), self.wcode_to_unicode[wcode], font=self.weatherfont, fill=0)
|
||||||
|
imgdraw.text((64, 32), '%.1fº' % temp, font=self.font16, fill=0)
|
||||||
|
return img
|
||||||
|
|
||||||
|
wcode_to_unicode = {
|
||||||
|
200: u'\uf01e',
|
||||||
|
201: u'\uf01e',
|
||||||
|
202: u'\uf01e',
|
||||||
|
210: u'\uf016',
|
||||||
|
211: u'\uf016',
|
||||||
|
212: u'\uf016',
|
||||||
|
221: u'\uf016',
|
||||||
|
230: u'\uf01e',
|
||||||
|
231: u'\uf01e',
|
||||||
|
232: u'\uf01e',
|
||||||
|
300: u'\uf01c',
|
||||||
|
301: u'\uf01c',
|
||||||
|
302: u'\uf019',
|
||||||
|
310: u'\uf017',
|
||||||
|
311: u'\uf019',
|
||||||
|
312: u'\uf019',
|
||||||
|
313: u'\uf01a',
|
||||||
|
314: u'\uf019',
|
||||||
|
321: u'\uf01c',
|
||||||
|
500: u'\uf01c',
|
||||||
|
501: u'\uf019',
|
||||||
|
502: u'\uf019',
|
||||||
|
503: u'\uf019',
|
||||||
|
504: u'\uf019',
|
||||||
|
511: u'\uf017',
|
||||||
|
520: u'\uf01a',
|
||||||
|
521: u'\uf01a',
|
||||||
|
522: u'\uf01a',
|
||||||
|
531: u'\uf01d',
|
||||||
|
600: u'\uf01b',
|
||||||
|
601: u'\uf01b',
|
||||||
|
602: u'\uf0b5',
|
||||||
|
611: u'\uf017',
|
||||||
|
612: u'\uf017',
|
||||||
|
615: u'\uf017',
|
||||||
|
616: u'\uf017',
|
||||||
|
620: u'\uf017',
|
||||||
|
621: u'\uf01b',
|
||||||
|
622: u'\uf01b',
|
||||||
|
701: u'\uf014',
|
||||||
|
711: u'\uf062',
|
||||||
|
721: u'\uf0b6',
|
||||||
|
731: u'\uf063',
|
||||||
|
741: u'\uf014',
|
||||||
|
761: u'\uf063',
|
||||||
|
762: u'\uf063',
|
||||||
|
771: u'\uf011',
|
||||||
|
781: u'\uf056',
|
||||||
|
800: u'\uf00d',
|
||||||
|
801: u'\uf011',
|
||||||
|
802: u'\uf011',
|
||||||
|
803: u'\uf012',
|
||||||
|
804: u'\uf013',
|
||||||
|
900: u'\uf056',
|
||||||
|
901: u'\uf01d',
|
||||||
|
902: u'\uf073',
|
||||||
|
903: u'\uf076',
|
||||||
|
904: u'\uf072',
|
||||||
|
905: u'\uf021',
|
||||||
|
906: u'\uf015',
|
||||||
|
957: u'\uf050',
|
||||||
|
}
|
||||||
|
|
Reference in New Issue
Block a user