diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f8b73e7 --- /dev/null +++ b/.gitignore @@ -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/ + diff --git a/LICENSE b/LICENSE index 915f9b4..0741db7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,25 +1,26 @@ -BSD 2-Clause License +Copyright (c) . All rights reserved. -Copyright (c) 2021, Javier Peña -All rights reserved. +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, 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, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. +this list of conditions and the following disclaimer in the documentation +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" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 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 -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a4b4ba8 --- /dev/null +++ b/README.md @@ -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 = +owm_location = + +[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). diff --git a/config.ini b/config.ini new file mode 100644 index 0000000..cedf011 --- /dev/null +++ b/config.ini @@ -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 = +# 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 = + +[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 diff --git a/drivers/caldavprovider.py b/drivers/caldavprovider.py new file mode 100644 index 0000000..6fe2efb --- /dev/null +++ b/drivers/caldavprovider.py @@ -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 diff --git a/drivers/einkdriver.py b/drivers/einkdriver.py new file mode 100644 index 0000000..aa68070 --- /dev/null +++ b/drivers/einkdriver.py @@ -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") + diff --git a/drivers/epd7in5bc.py b/drivers/epd7in5bc.py new file mode 100644 index 0000000..a656e6d --- /dev/null +++ b/drivers/epd7in5bc.py @@ -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 ### diff --git a/drivers/epdconfig.py b/drivers/epdconfig.py new file mode 100644 index 0000000..3470b66 --- /dev/null +++ b/drivers/epdconfig.py @@ -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 ### + diff --git a/drivers/gpiodriver.py b/drivers/gpiodriver.py new file mode 100644 index 0000000..a09e23c --- /dev/null +++ b/drivers/gpiodriver.py @@ -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 diff --git a/drivers/pygamedriver.py b/drivers/pygamedriver.py new file mode 100644 index 0000000..342ac40 --- /dev/null +++ b/drivers/pygamedriver.py @@ -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) diff --git a/fonts/DejaVuSansMono-Bold.ttf b/fonts/DejaVuSansMono-Bold.ttf new file mode 100644 index 0000000..a570861 Binary files /dev/null and b/fonts/DejaVuSansMono-Bold.ttf differ diff --git a/fonts/LICENSE-DejaVuSansMono.txt b/fonts/LICENSE-DejaVuSansMono.txt new file mode 100644 index 0000000..df52c17 --- /dev/null +++ b/fonts/LICENSE-DejaVuSansMono.txt @@ -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$ diff --git a/fonts/license-weathericons-regular-webfont.txt b/fonts/license-weathericons-regular-webfont.txt new file mode 100644 index 0000000..53d7169 --- /dev/null +++ b/fonts/license-weathericons-regular-webfont.txt @@ -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 diff --git a/fonts/weathericons-regular-webfont.ttf b/fonts/weathericons-regular-webfont.ttf new file mode 100644 index 0000000..948f0a5 Binary files /dev/null and b/fonts/weathericons-regular-webfont.ttf differ diff --git a/img/night_image.jpg b/img/night_image.jpg new file mode 100644 index 0000000..21d5c73 Binary files /dev/null and b/img/night_image.jpg differ diff --git a/mycalendar.py b/mycalendar.py new file mode 100755 index 0000000..40bbb74 --- /dev/null +++ b/mycalendar.py @@ -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() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..073a5d7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +caldav +icalendar +pygame +pillow +pyowm +pytz diff --git a/reset_eink.py b/reset_eink.py new file mode 100755 index 0000000..2dcd2f9 --- /dev/null +++ b/reset_eink.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +from drivers.einkdriver import EinkDriver +video = EinkDriver(640, 384) +video.end() diff --git a/scripts/calendar.service b/scripts/calendar.service new file mode 100644 index 0000000..0c3c89a --- /dev/null +++ b/scripts/calendar.service @@ -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 diff --git a/scripts/launcher.sh b/scripts/launcher.sh new file mode 100755 index 0000000..20ff364 --- /dev/null +++ b/scripts/launcher.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +cd ~/calendario +source .venv/bin/activate +./mycalendar.py diff --git a/widgets/calendarwidget.py b/widgets/calendarwidget.py new file mode 100644 index 0000000..117c853 --- /dev/null +++ b/widgets/calendarwidget.py @@ -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 diff --git a/widgets/timewidget.py b/widgets/timewidget.py new file mode 100644 index 0000000..32ed72d --- /dev/null +++ b/widgets/timewidget.py @@ -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'] diff --git a/widgets/weatherwidget.py b/widgets/weatherwidget.py new file mode 100644 index 0000000..ab2db59 --- /dev/null +++ b/widgets/weatherwidget.py @@ -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', + } +