diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/README.md b/README.md index 9d15504..e6f6154 100644 --- a/README.md +++ b/README.md @@ -1,100 +1,10 @@ -![alt text](infowindow.jpg) +I've personalised the project for my own use. +I'm using the HD version of the screen, the existing code did not work for this. +I've therefore updated to use with the latest drivers -# Infowindow -Rapsberry pi powered e-ink display for displaying information in an always on state. There are several other iterations -of this project online, but they didnt do quite what I wanted them to. This is my version. Also keeping up my python -skills as they dont get used as much as they used to! +I've removed the weather functions as these were not useful to me. -The functionality is not meant to be an "end all solution for calendaring and Todo lists" The intent is to provide an -*always on* display to show me what is coming up next. I can then check in browser, phone, etc for details and updates -to the data. In your face reminder. -
- Features | - Installation | - Configuration | - Running | -
- -## Features -* **Calendar** - * Google Calendar is the only calendar currently supported -* **Todo List** - * Todoist - * Teamwork.com -* **Weather** - * Open Weather Map current data only. Future plan for forecast data. - -## Installation -### Raspberry Pi setup -Activate SPI on your Raspberry Pi by using the `raspi-config` tool under Interface Options and reboot. - -### Get software -Clone this repo onto your raspberry pi. Does not really matter where it is, but good option is in the `pi` users home -directory: `/home/pi/InfoWindow` - -### Setup python modules -Run `pip install -r requirements.txt`. This should install all required modules. I stuck to basic standard modules for -ease of installation. - -## Configuration -You will need to configure a few things such as API Keys and location. Copy config.json-sample to config.json. Edit -config.json to add your api keys and other information. - -## Optional: Increase lifetime of your SD-Card -If you want to increase the lifetime of the SD-Card, add the following line to `/etc/fstab` and reboot: - -`tmpfs /tmp tmpfs defaults,noatime,nosuid,size=100m 0 0` - -With this line, the `/tmp` folder will be held in RAM and will not be written to the SD-Card. - -## Optional: Screen saver -Always displaying the same colors at the same spots might have some negative effect on your E-Ink screen. To remedy -this, there is a simple additional script, which displays all three colors on the whole screen: I recommend to let -this run once every night, i.e. at 1 minute past 5 with: -* Run `crontab -e` -* insert `1 5 * * * /usr/bin/python /home/pi/InfoWindow/screensaver.py > /dev/null 2>&1` - -### General -* rotation: 0 - This is the rotation of the display in degrees. Leave at zero if you use it as a desktop display. Change -to 180 if you have it mounted and hanging from a shelf. -* timeformat: 12h / 24h -* charset: utf-8 (or something else). I.e. to get äöü working, use latin1 - -### Todo (Module) -Todoist is the current active module in this code. It only requires `api_key`. Teamwork also requires a 'site' key. If -using google tasks, leave this as null `todo: null` -* api_key: Enter your todoist API key. - -### Weather (Module) -Open Weather Map is where the data is coming from in the default module. This requires a few keys. -* api_key: Get your api key from OWM website. -* city: Look at OWM docs to figure what your city name is. Mine is "Sacramento,US" -* units: This can either be `imperial` or `metric` - -### Google calendar and ToDo list (Modules) -To use the google APIs, you first have to login to the [google cloud console](https://console.cloud.google.com/apis/). -In the google cloud console, do the following things: -1) Create a project and give it a name, i.e. `infowindow` and switch to the context of this project if not already - active. -2) Create a [new oauth consent screen](https://console.cloud.google.com/apis/credentials/consent) (just enter a name - should be enough). -3) Create a [new oauth 2.0 client id](https://console.cloud.google.com/apis/credentials). Choosing type `other` should - work just fine. Finally, download the json file provided by the google cloud console and store it in the repo - directory (i.e. `/home/pi/InfoWindow/google_secret.json`) on the Raspberry Pi. - -#### Calendar -There are are additional sections in the config for this module: -* additional: A list of additional calendar names (summary) to fetch. To use i.e. birthdays, add "Contacts" (also if - you use google in german. -* ignored: A list of events to be removed from the calendar display. - -## Running -### First Run -You should run the script manually the first time so that Googles auth modules can run interactivly. Once that has -completed you will want to add this to CRON so it runs every few minutes automatically. - -### Cron Run (Normal use) -* Run `crontab -e` -* insert `*/6 * * * * /usr/bin/python /home/pi/InfoWindow/infowindow.py --cron` +I have added in a todo module to work with "Grocy" +I've left 3 columns each to be used with any todo or calendar app diff --git a/clearScreen.py b/clearScreen.py index af59ca7..67d9628 100644 --- a/clearScreen.py +++ b/clearScreen.py @@ -1,3 +1,24 @@ -from mod_infowindow import infowindow -iw = infowindow.InfoWindow() -iw.display() +#!/usr/bin/python +# -*- coding:utf-8 -*- +import sys +import os +picdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'pic') +libdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'lib') +if os.path.exists(libdir): + sys.path.append(libdir) + +import logging +from driver import epd7in5b +import time +from PIL import Image,ImageDraw,ImageFont +import traceback + +logging.basicConfig(level=logging.DEBUG) + +logging.info("epd7in5b_HD Demo") + +epd = epd7in5b.EPD() + +logging.info("init and Clear") +epd.init() +epd.Clear() diff --git a/config.json-sample b/config.json-sample index 9362e48..65ce7fb 100644 --- a/config.json-sample +++ b/config.json-sample @@ -1,22 +1,22 @@ { "general": { - "rotation": 180, + "rotation": 0, "timeformat": "12h", - "charset": "latin1", + "charset": "utf-8", "cell_spacing": 2 }, "todo": { - "api_key": "1234" + "api_key": "" }, "calendar": { - "additional": ["Contacts", "Birthdays"], - "ignored": ["Buy ticket!"], - "today_text_color": "red", + "include": [""], + "ignored": [""], + "today_text_color": "black", "today_background_color": "white" }, - "weather": { - "api_key": "1234", - "city": "Sacramento,US", - "units": "imperial" + "grocy": { + "api_key": "", + "server": "", + "port": "" } } diff --git a/driver/epd7in5b-old.py b/driver/epd7in5b-old.py new file mode 100755 index 0000000..2791cf9 --- /dev/null +++ b/driver/epd7in5b-old.py @@ -0,0 +1,212 @@ +## + # @filename : epd7in5.py + # @brief : Implements for Dual-color e-paper library + # @author : Yehui from Waveshare + # + # Copyright (C) Waveshare July 10 2017 + # + # 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 epdif +from PIL import Image +import RPi.GPIO as GPIO +import logging + +# Display resolution +EPD_WIDTH = 640 +EPD_HEIGHT = 384 + +# EPD7IN5 commands +PANEL_SETTING = 0x00 +POWER_SETTING = 0x01 +POWER_OFF = 0x02 +POWER_OFF_SEQUENCE_SETTING = 0x03 +POWER_ON = 0x04 +POWER_ON_MEASURE = 0x05 +BOOSTER_SOFT_START = 0x06 +DEEP_SLEEP = 0x07 +DATA_START_TRANSMISSION_1 = 0x10 +DATA_STOP = 0x11 +DISPLAY_REFRESH = 0x12 +IMAGE_PROCESS = 0x13 +LUT_FOR_VCOM = 0x20 +LUT_BLUE = 0x21 +LUT_WHITE = 0x22 +LUT_GRAY_1 = 0x23 +LUT_GRAY_2 = 0x24 +LUT_RED_0 = 0x25 +LUT_RED_1 = 0x26 +LUT_RED_2 = 0x27 +LUT_RED_3 = 0x28 +LUT_XON = 0x29 +PLL_CONTROL = 0x30 +TEMPERATURE_SENSOR_COMMAND = 0x40 +TEMPERATURE_CALIBRATION = 0x41 +TEMPERATURE_SENSOR_WRITE = 0x42 +TEMPERATURE_SENSOR_READ = 0x43 +VCOM_AND_DATA_INTERVAL_SETTING = 0x50 +LOW_POWER_DETECTION = 0x51 +TCON_SETTING = 0x60 +TCON_RESOLUTION = 0x61 +SPI_FLASH_CONTROL = 0x65 +REVISION = 0x70 +GET_STATUS = 0x71 +AUTO_MEASUREMENT_VCOM = 0x80 +READ_VCOM_VALUE = 0x81 +VCM_DC_SETTING = 0x82 + +class EPD: + def __init__(self): + self.reset_pin = epdif.RST_PIN + self.dc_pin = epdif.DC_PIN + self.busy_pin = epdif.BUSY_PIN + self.width = EPD_WIDTH + self.height = EPD_HEIGHT + + def digital_write(self, pin, value): + epdif.epd_digital_write(pin, value) + + def digital_read(self, pin): + return epdif.epd_digital_read(pin) + + def delay_ms(self, delaytime): + epdif.epd_delay_ms(delaytime) + + def send_command(self, command): + self.digital_write(self.dc_pin, GPIO.LOW) + # the parameter type is list but not int + # so use [command] instead of command + epdif.spi_transfer([command]) + + def send_data(self, data): + self.digital_write(self.dc_pin, GPIO.HIGH) + # the parameter type is list but not int + # so use [data] instead of data + epdif.spi_transfer([data]) + + def init(self): + if (epdif.epd_init() != 0): + return -1 + self.reset() + self.send_command(POWER_SETTING) + self.send_data(0x37) + self.send_data(0x00) + self.send_command(PANEL_SETTING) + self.send_data(0xCF) + self.send_data(0x08) + self.send_command(BOOSTER_SOFT_START) + self.send_data(0xc7) + self.send_data(0xcc) + self.send_data(0x28) + self.send_command(POWER_ON) + self.wait_until_idle() + self.send_command(PLL_CONTROL) + self.send_data(0x3c) + self.send_command(TEMPERATURE_CALIBRATION) + self.send_data(0x00) + self.send_command(VCOM_AND_DATA_INTERVAL_SETTING) + self.send_data(0x77) + self.send_command(TCON_SETTING) + self.send_data(0x22) + self.send_command(TCON_RESOLUTION) + self.send_data(0x02) #source 640 + self.send_data(0x80) + self.send_data(0x01) #gate 384 + self.send_data(0x80) + self.send_command(VCM_DC_SETTING) + self.send_data(0x1E) #decide by LUT file + self.send_command(0xe5) #FLASH MODE + self.send_data(0x03) + + def wait_until_idle(self): + while(self.digital_read(self.busy_pin) == 0): # 0: busy, 1: idle + #logging.debug("DRIVER: (wait_until_idle)") + #self.delay_ms(100) + self.delay_ms(50) + + def reset(self): + self.digital_write(self.reset_pin, GPIO.LOW) # module reset + self.delay_ms(200) + self.digital_write(self.reset_pin, GPIO.HIGH) + self.delay_ms(200) + + def get_frame_buffer(self, image): + buf = [0x00] * int(self.width * self.height / 4) + # Set buffer to value of Python Imaging Library image. + # Image must be in mode L. + image_grayscale = image.convert('L') + imwidth, imheight = image_grayscale.size + if imwidth != self.width or imheight != self.height: + raise ValueError('Image must be same dimensions as display \ + ({0}x{1}).' .format(self.width, self.height)) + + pixels = image_grayscale.load() + for y in range(self.height): + for x in range(self.width): + # Set the bits for the column of pixels at the current position. + if pixels[x, y] < 64: # black + buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2)) + elif pixels[x, y] < 192: # convert gray to red + buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2)) + buf[int((x + y * self.width) / 4)] |= 0x40 >> (x % 4 * 2) + else: # white + buf[int((x + y * self.width) / 4)] |= 0xC0 >> (x % 4 * 2) + return buf + + def display_frame(self, frame_buffer): + self.send_command(DATA_START_TRANSMISSION_1) + logging.debug("DRIVER: ENTERING FOR LOOP") + for i in range(0, int(self.width / 4 * self.height)): + temp1 = frame_buffer[i] + j = 0 + while (j < 4): + if ((temp1 & 0xC0) == 0xC0): + temp2 = 0x03 + elif ((temp1 & 0xC0) == 0x00): + temp2 = 0x00 + else: + temp2 = 0x04 + temp2 = (temp2 << 4) & 0xFF + temp1 = (temp1 << 2) & 0xFF + j += 1 + if((temp1 & 0xC0) == 0xC0): + temp2 |= 0x03 + elif ((temp1 & 0xC0) == 0x00): + temp2 |= 0x00 + else: + temp2 |= 0x04 + temp1 = (temp1 << 2) & 0xFF + self.send_data(temp2) + j += 1 + logging.debug("SENDING DISPLAY_REFRESH COMMAND") + self.send_command(DISPLAY_REFRESH) + logging.debug("DELAY 100 MS") + self.delay_ms(100) + logging.debug("WAIT UNTIL IDLE") + self.wait_until_idle() + + def sleep(self): + self.send_command(POWER_OFF) + self.wait_until_idle() + self.send_command(DEEP_SLEEP) + self.send_data(0xa5) + +### END OF FILE ### + diff --git a/driver/epd7in5b.py b/driver/epd7in5b.py index 2791cf9..5261770 100644 --- a/driver/epd7in5b.py +++ b/driver/epd7in5b.py @@ -1,212 +1,206 @@ -## - # @filename : epd7in5.py - # @brief : Implements for Dual-color e-paper library - # @author : Yehui from Waveshare - # - # Copyright (C) Waveshare July 10 2017 - # - # 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 epdif -from PIL import Image -import RPi.GPIO as GPIO -import logging - -# Display resolution -EPD_WIDTH = 640 -EPD_HEIGHT = 384 - -# EPD7IN5 commands -PANEL_SETTING = 0x00 -POWER_SETTING = 0x01 -POWER_OFF = 0x02 -POWER_OFF_SEQUENCE_SETTING = 0x03 -POWER_ON = 0x04 -POWER_ON_MEASURE = 0x05 -BOOSTER_SOFT_START = 0x06 -DEEP_SLEEP = 0x07 -DATA_START_TRANSMISSION_1 = 0x10 -DATA_STOP = 0x11 -DISPLAY_REFRESH = 0x12 -IMAGE_PROCESS = 0x13 -LUT_FOR_VCOM = 0x20 -LUT_BLUE = 0x21 -LUT_WHITE = 0x22 -LUT_GRAY_1 = 0x23 -LUT_GRAY_2 = 0x24 -LUT_RED_0 = 0x25 -LUT_RED_1 = 0x26 -LUT_RED_2 = 0x27 -LUT_RED_3 = 0x28 -LUT_XON = 0x29 -PLL_CONTROL = 0x30 -TEMPERATURE_SENSOR_COMMAND = 0x40 -TEMPERATURE_CALIBRATION = 0x41 -TEMPERATURE_SENSOR_WRITE = 0x42 -TEMPERATURE_SENSOR_READ = 0x43 -VCOM_AND_DATA_INTERVAL_SETTING = 0x50 -LOW_POWER_DETECTION = 0x51 -TCON_SETTING = 0x60 -TCON_RESOLUTION = 0x61 -SPI_FLASH_CONTROL = 0x65 -REVISION = 0x70 -GET_STATUS = 0x71 -AUTO_MEASUREMENT_VCOM = 0x80 -READ_VCOM_VALUE = 0x81 -VCM_DC_SETTING = 0x82 - -class EPD: - def __init__(self): - self.reset_pin = epdif.RST_PIN - self.dc_pin = epdif.DC_PIN - self.busy_pin = epdif.BUSY_PIN - self.width = EPD_WIDTH - self.height = EPD_HEIGHT - - def digital_write(self, pin, value): - epdif.epd_digital_write(pin, value) - - def digital_read(self, pin): - return epdif.epd_digital_read(pin) - - def delay_ms(self, delaytime): - epdif.epd_delay_ms(delaytime) - - def send_command(self, command): - self.digital_write(self.dc_pin, GPIO.LOW) - # the parameter type is list but not int - # so use [command] instead of command - epdif.spi_transfer([command]) - - def send_data(self, data): - self.digital_write(self.dc_pin, GPIO.HIGH) - # the parameter type is list but not int - # so use [data] instead of data - epdif.spi_transfer([data]) - - def init(self): - if (epdif.epd_init() != 0): - return -1 - self.reset() - self.send_command(POWER_SETTING) - self.send_data(0x37) - self.send_data(0x00) - self.send_command(PANEL_SETTING) - self.send_data(0xCF) - self.send_data(0x08) - self.send_command(BOOSTER_SOFT_START) - self.send_data(0xc7) - self.send_data(0xcc) - self.send_data(0x28) - self.send_command(POWER_ON) - self.wait_until_idle() - self.send_command(PLL_CONTROL) - self.send_data(0x3c) - self.send_command(TEMPERATURE_CALIBRATION) - self.send_data(0x00) - self.send_command(VCOM_AND_DATA_INTERVAL_SETTING) - self.send_data(0x77) - self.send_command(TCON_SETTING) - self.send_data(0x22) - self.send_command(TCON_RESOLUTION) - self.send_data(0x02) #source 640 - self.send_data(0x80) - self.send_data(0x01) #gate 384 - self.send_data(0x80) - self.send_command(VCM_DC_SETTING) - self.send_data(0x1E) #decide by LUT file - self.send_command(0xe5) #FLASH MODE - self.send_data(0x03) - - def wait_until_idle(self): - while(self.digital_read(self.busy_pin) == 0): # 0: busy, 1: idle - #logging.debug("DRIVER: (wait_until_idle)") - #self.delay_ms(100) - self.delay_ms(50) - - def reset(self): - self.digital_write(self.reset_pin, GPIO.LOW) # module reset - self.delay_ms(200) - self.digital_write(self.reset_pin, GPIO.HIGH) - self.delay_ms(200) - - def get_frame_buffer(self, image): - buf = [0x00] * int(self.width * self.height / 4) - # Set buffer to value of Python Imaging Library image. - # Image must be in mode L. - image_grayscale = image.convert('L') - imwidth, imheight = image_grayscale.size - if imwidth != self.width or imheight != self.height: - raise ValueError('Image must be same dimensions as display \ - ({0}x{1}).' .format(self.width, self.height)) - - pixels = image_grayscale.load() - for y in range(self.height): - for x in range(self.width): - # Set the bits for the column of pixels at the current position. - if pixels[x, y] < 64: # black - buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2)) - elif pixels[x, y] < 192: # convert gray to red - buf[int((x + y * self.width) / 4)] &= ~(0xC0 >> (x % 4 * 2)) - buf[int((x + y * self.width) / 4)] |= 0x40 >> (x % 4 * 2) - else: # white - buf[int((x + y * self.width) / 4)] |= 0xC0 >> (x % 4 * 2) - return buf - - def display_frame(self, frame_buffer): - self.send_command(DATA_START_TRANSMISSION_1) - logging.debug("DRIVER: ENTERING FOR LOOP") - for i in range(0, int(self.width / 4 * self.height)): - temp1 = frame_buffer[i] - j = 0 - while (j < 4): - if ((temp1 & 0xC0) == 0xC0): - temp2 = 0x03 - elif ((temp1 & 0xC0) == 0x00): - temp2 = 0x00 - else: - temp2 = 0x04 - temp2 = (temp2 << 4) & 0xFF - temp1 = (temp1 << 2) & 0xFF - j += 1 - if((temp1 & 0xC0) == 0xC0): - temp2 |= 0x03 - elif ((temp1 & 0xC0) == 0x00): - temp2 |= 0x00 - else: - temp2 |= 0x04 - temp1 = (temp1 << 2) & 0xFF - self.send_data(temp2) - j += 1 - logging.debug("SENDING DISPLAY_REFRESH COMMAND") - self.send_command(DISPLAY_REFRESH) - logging.debug("DELAY 100 MS") - self.delay_ms(100) - logging.debug("WAIT UNTIL IDLE") - self.wait_until_idle() - - def sleep(self): - self.send_command(POWER_OFF) - self.wait_until_idle() - self.send_command(DEEP_SLEEP) - self.send_data(0xa5) - -### END OF FILE ### - +# ***************************************************************************** +# * | File : epd7in5bc_HD.py +# * | Author : Waveshare team +# * | Function : Electronic paper driver +# * | Info : +# *---------------- +# * | This version: V1.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 = 880 +EPD_HEIGHT = 528 + +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(4) + 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 ReadBusy(self): + logging.debug("e-Paper busy") + busy = epdconfig.digital_read(self.busy_pin) + while(busy == 1): + busy = epdconfig.digital_read(self.busy_pin) + epdconfig.delay_ms(200) + + def init(self): + if (epdconfig.module_init() != 0): + return -1 + + self.reset() + + self.send_command(0x12); #SWRESET + self.ReadBusy(); #waiting for the electronic paper IC to release the idle signal + + self.send_command(0x46); # Auto Write RAM + self.send_data(0xF7); + self.ReadBusy(); #waiting for the electronic paper IC to release the idle signal + + self.send_command(0x47); # Auto Write RAM + self.send_data(0xF7); + self.ReadBusy(); #waiting for the electronic paper IC to release the idle signal + + self.send_command(0x0C); # Soft start setting + self.send_data(0xAE); + self.send_data(0xC7); + self.send_data(0xC3); + self.send_data(0xC0); + self.send_data(0x40); + + self.send_command(0x01); # Set MUX as 527 + self.send_data(0xAF); + self.send_data(0x02); + self.send_data(0x01); + + self.send_command(0x11); # Data entry mode + self.send_data(0x01); + + self.send_command(0x44); + self.send_data(0x00); # RAM x address start at 0 + self.send_data(0x00); + self.send_data(0x6F); # RAM x address end at 36Fh -> 879 + self.send_data(0x03); + self.send_command(0x45); + self.send_data(0xAF); # RAM y address start at 20Fh; + self.send_data(0x02); + self.send_data(0x00); # RAM y address end at 00h; + self.send_data(0x00); + + self.send_command(0x3C); # VBD + self.send_data(0x01); # LUT1, for white + + self.send_command(0x18); + self.send_data(0X80); + self.send_command(0x22); + self.send_data(0XB1); #Load Temperature and waveform setting. + self.send_command(0x20); + self.ReadBusy(); #waiting for the electronic paper IC to release the idle signal + + self.send_command(0x4E); + self.send_data(0x00); + self.send_data(0x00); + self.send_command(0x4F); + self.send_data(0xAF); + self.send_data(0x02); + + return 0 + + def getbuffer(self, image): + # logging.debug("bufsiz = ",int(self.width/8) * self.height) + buf = [0xFF] * (int(self.width/8) * self.height) + image_monocolor = image.convert('1') + imwidth, imheight = image_monocolor.size + pixels = image_monocolor.load() + logging.debug('imwidth = %d imheight = %d ',imwidth, imheight) + if(imwidth == self.width and imheight == self.height): + logging.debug("Horizontal") + for y in range(imheight): + for x in range(imwidth): + # Set the bits for the column of pixels at the current position. + if pixels[x, y] == 0: + buf[int((x + y * self.width) / 8)] &= ~(0x80 >> (x % 8)) + elif(imwidth == self.height and imheight == self.width): + logging.debug("Vertical") + for y in range(imheight): + for x in range(imwidth): + newx = y + newy = self.height - x - 1 + if pixels[x, y] == 0: + buf[int((newx + newy*self.width) / 8)] &= ~(0x80 >> (y % 8)) + return buf + + def display(self, imageblack, imagered): + self.send_command(0x4F); + self.send_data(0xAf); + + self.send_command(0x24) + for i in range(0, int(self.width * self.height / 8)): + self.send_data(imageblack[i]); + + + self.send_command(0x26) + for i in range(0, int(self.width * self.height / 8)): + self.send_data(~imagered[i]); + + self.send_command(0x22); + self.send_data(0xC7); #Load LUT from MCU(0x32) + self.send_command(0x20); + epdconfig.delay_ms(200); #!!!The delay here is necessary, 200uS at least!!! + self.ReadBusy(); + + def Clear(self): + self.send_command(0x4F); + self.send_data(0xAf); + + self.send_command(0x24) + for i in range(0, int(self.width * self.height / 8)): + self.send_data(0xff); + + + self.send_command(0x26) + for i in range(0, int(self.width * self.height / 8)): + self.send_data(0x00); + + self.send_command(0x22); + self.send_data(0xC7); #Load LUT from MCU(0x32) + self.send_command(0x20); + epdconfig.delay_ms(200); #!!!The delay here is necessary, 200uS at least!!! + self.ReadBusy(); + + def sleep(self): + self.send_command(0x10); #deep sleep + self.send_data(0x01); + + def Dev_exit(self): + epdconfig.module_exit() +### END OF FILE ### + diff --git a/driver/epdconfig.py b/driver/epdconfig.py new file mode 100755 index 0000000..861f43d --- /dev/null +++ b/driver/epdconfig.py @@ -0,0 +1,154 @@ +# /***************************************************************************** +# * | 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.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/infowindow.jpg b/infowindow.jpg deleted file mode 100644 index 6ea1040..0000000 Binary files a/infowindow.jpg and /dev/null differ diff --git a/infowindow.py b/infowindow.py index 89098bd..91d8294 100755 --- a/infowindow.py +++ b/infowindow.py @@ -5,17 +5,21 @@ import json import logging import string +import datetime +from PIL import Image +from PIL import ImageDraw +from PIL import ImageFont +from PIL import ImageChops from mod_infowindow import infowindow # Select pluggable module for todo list, calendar and weather. # Replace the mod_ with one of: # TODO: mod_todoist, mod_teamwork # CALENDAR: mod_google, mod_ical -# WEATHER: mod_owm, mod_wunderground from mod_utils import iw_utils -from mod_todo import mod_google as modTodo # TODO +from mod_todo import mod_todoist as modTodo # TODO from mod_calendar import mod_google as modCalendar # CALENDAR -from mod_weather import mod_owm as modWeather # WEATHER +from mod_todo import mod_grocy as modGrocy # TODO: Create dictionaries for API args. so that they can be custom. @@ -28,12 +32,11 @@ rotation = config_data["general"]["rotation"] charset = config_data["general"]["charset"] todo_opts = config_data["todo"] +grocy_opts = config_data["grocy"] calendar_opts = config_data["calendar"] -weather_opts = config_data["weather"] infowindow_opts = {} # give the timeformat to all the modules needing it calendar_opts["timeformat"] = config_data["general"]["timeformat"] -weather_opts["timeformat"] = config_data["general"]["timeformat"] infowindow_opts["timeformat"] = config_data["general"]["timeformat"] infowindow_opts["cell_spacing"] = config_data["general"]["cell_spacing"] @@ -49,80 +52,78 @@ # display since this will run headless most of the time. This gives the user # enough info to know that they need to troubleshoot. def HandleException(et, val, tb): - iw = infowindow.InfoWindow() - iw.text(0, 10, "EXCEPTION IN PROGRAM", 'robotoBlack18', 'black') - iw.text(0, 30, val.encode(charset).strip(), 'robotoBlack18', 'black') - iw.text(0, 60, "Please run program from command line interactivly to resolve", 'robotoBlack18', 'black') + red = infowindow.InfoWindow() + red.text(0, 10, "EXCEPTION IN PROGRAM", 'robotoBlack18', 'black') + red.text(0, 30, val.encode(charset).strip(), 'robotoBlack18', 'black') + red.text(0, 60, "Please run program from command line interactivly to resolve", 'robotoBlack18', 'black') print("EXCEPTION IN PROGRAM ==================================") print("error message: %s" % val) print("type: %s" % et) print("traceback: %s" % tb) print("line: %s" % tb.lineno) print("END EXCEPTION =========================================") - iw.display(rotation) + red.display(rotation) sys.excepthook = HandleException - # helper to calculate max char width and height -def get_max_char_size(iw, chars, font): +def get_max_char_size(black, chars, font): max_x = 0 max_y = 0 for char in chars: - (x, y) = iw.getFont(font).getsize(char) + (x, y) = black.getFont(font).getsize(char) if x > max_x: max_x = x if y > max_y: max_y = y return max_x, max_y - # Main Program ################################################################ def main(): # Instantiate API modules + logging.info(str(todo_opts)) + logging.info(str(calendar_opts)) todo = modTodo.ToDo(todo_opts) cal = modCalendar.Cal(calendar_opts) - weather = modWeather.Weather(weather_opts) + grocy = modGrocy.test(grocy_opts) # Setup e-ink initial drawings - iw = infowindow.InfoWindow(infowindow_opts) + red = infowindow.InfoWindow(infowindow_opts, "red") + black = infowindow.InfoWindow(infowindow_opts, "black") + + # Title Line + black.line(0, 0, 880, 0) # Top Line + red.rectangle(1, 1, 880, 24) # Red Rectangle + black.line(0, 25, 880, 25) # Bottom Black Line + + # Titles + text_width = red.textwidth("CALENDAR", 'robotoBlack24') + red.text(143 - text_width, 0, "CALENDAR", 'robotoBlack24', 'white') + text_width = red.textwidth("FRIDGE", 'robotoBlack24') + red.text(440 - text_width, 0, "FRIDGE", 'robotoBlack24', 'white') + text_width = red.textwidth("TASKS", 'robotoBlack24') + red.text(737 - text_width, 0, "TASKS", 'robotoBlack24', 'white') + + #Date header + text_width = black.textwidth(datetime.today().strftime('%a'), 'robotoBlack18') + black.text(297 - text_width, 0, datetime.today().strftime('%a'), 'robotoBlack18', 'black') + text_width = black.textwidth(datetime.today().strftime('%-d %b'), 'robotoBlack18') + black.text(594 - text_width, 0, datetime.today().strftime('%a'), 'robotoBlack18', 'black') # Set some things calendar_date_font = "robotoRegular14" - calendar_entry_font = "robotoRegular18" - tasks_font = "robotoRegular18" - - # Weather Grid - temp_rect_width = 102 - temp_rect_left = (iw.width / 2) - (temp_rect_width / 2) - temp_rect_right = (iw.width / 2) + (temp_rect_width / 2) - - iw.line(268, 0, 268, 64, 'black') # First Vertical Line - iw.rectangle(temp_rect_left, 0, temp_rect_right, 64, 'red') - iw.line(372, 0, 372, 64, 'black') # Second Vertical Line - - iw.bitmap(375, 0, "windSmall.bmp") # Wind Icon - iw.line(461, 0, 461, 64, 'black') # Third Vertical Line - - iw.bitmap(464, 0, "rainSmall.bmp") # Rain Icon - iw.line(550, 0, 550, 64, 'black') # Fourth Vertical Line - - iw.bitmap(554, 0, "snowSmall.bmp") # Snow Icon - - # Center cal/todo divider line - iw.line(314, 90, 314, 384, 'black') # Left Black line - iw.rectangle(315, 64, 325, 384, 'red') # Red Rectangle - iw.line(326, 90, 326, 384, 'black') # Right Black line + calendar_entry_font = "robotoBlack18" + tasks_font = "robotoBlack18" - # Calendar / Todo Title Line - iw.line(0, 64, 640, 64, 'black') # Top Line - iw.rectangle(0, 65, 640, 90, 'red') # Red Rectangle - iw.line(0, 91, 640, 91, 'black') # Bottom Black Line + # Dividing lines + black.line(286, 24, 286, 528) # Left Black line + red.rectangle(287, 24, 296, 528) # Red Rectangle + black.line(297, 24, 297, 528) # Right Black line - # Todo / Weather Titles - iw.text(440, 64, "TODO", 'robotoBlack24', 'white') - iw.text(95, 64, "CALENDAR", 'robotoBlack24', 'white') + black.line(583, 24, 583, 528) # Left Black line + red.rectangle(584, 24, 593, 528) # Red Rectangle + black.line(594, 24, 594, 528) # Right Black line # DISPLAY TODO INFO # ========================================================================= @@ -130,19 +131,65 @@ def main(): logging.debug("Todo Items") logging.debug("-----------------------------------------------------------------------") - #(t_x, t_y) = iw.getFont(tasks_font).getsize('JgGj') - (t_x, t_y) = get_max_char_size(iw, string.printable, tasks_font) + #(t_x, t_y) = red.getFont(tasks_font).getsize('JgGj') + (t_x, t_y) = get_max_char_size(red, string.printable, tasks_font) line_height = t_y + (2 * infowindow_opts["cell_spacing"]) - current_task_y = 92 + current_task_y = 25 for todo_item in todo_items: - iw.text(333, (current_task_y + infowindow_opts["cell_spacing"]), todo_item['content'].encode(charset).strip(), - tasks_font, 'black') - iw.line(327, (current_task_y + line_height + 1), 640, (current_task_y + line_height + 1), 'black') + if todo_item['due']: + date = datetime.datetime.strptime(todo_item['due'][0:10], '%Y-%m-%d').date() + else: + date = datetime.datetime.now().date() + if datetime.datetime.now().date() >= date: + + if 2156103501 in todo_item['labels']: + red.text(595, (current_task_y + infowindow_opts["cell_spacing"]), red.truncate(todo_item['content'].encode(charset).strip(), tasks_font, 286), tasks_font) + red.line(595, (current_task_y + line_height + 1), 880, (current_task_y + line_height + 1)) + # set next loop height + current_task_y = (current_task_y + line_height + 2) + logging.debug("ITEM: %s" % todo_item['content'].encode(charset).strip()) + if todo_item['priority'] > 2: + black.text(595, (current_task_y + infowindow_opts["cell_spacing"]), black.truncate(todo_item['content'].encode(charset).strip(), tasks_font, 286), tasks_font) + red.line(595, (current_task_y + line_height + 1), 880, (current_task_y + line_height + 1)) + # set next loop height + current_task_y = (current_task_y + line_height + 2) + logging.debug("ITEM: %s" % todo_item['content'].encode(charset).strip()) + + + # DISPLAY GROCY INFO + # ========================================================================= + try: + grocy_items = grocy.list() + logging.debug("Grocy Items") + logging.debug("-----------------------------------------------------------------------") + + #(t_x, t_y) = red.getFont(tasks_font).getsize('JgGj') + (t_x, t_y) = get_max_char_size(red, string.printable, tasks_font) + line_height = t_y + (2 * infowindow_opts["cell_spacing"]) + + current_task_y = 25 + for grocy_item in grocy_items: + (np_x, np_y) = red.getFont(tasks_font).getsize(str(grocy_item['days']) + " ") + if int(grocy_item['days']) < 3: + text = red + else: + text = black + + text.text(298, (current_task_y + infowindow_opts["cell_spacing"]), str(grocy_item['days'] + " "), tasks_font) + (op_x, op_y) = text.getFont(tasks_font).getsize(black.truncate(grocy_item['content'].encode(charset).strip(), tasks_font, 286 - np_x)) + text.text(583 - op_x, (current_task_y + infowindow_opts["cell_spacing"]), text.truncate(grocy_item['content'].encode(charset).strip(), tasks_font, 286 - np_x), tasks_font) + red.line(298, (current_task_y + line_height + 1), 582, (current_task_y + line_height + 1)) + + + # set next loop height + current_task_y = (current_task_y + line_height + 2) + logging.debug("ITEM: %s" % grocy_item['content'].encode(charset).strip()) + except: + logging.debug("Grocy Failed") + logging.debug("-----------------------------------------------------------------------") + red.text(298, (25 + infowindow_opts["cell_spacing"]), str("Grocy failed to load"), tasks_font) - # set next loop height - current_task_y = (current_task_y + line_height + 2) - logging.debug("ITEM: %s" % todo_item['content'].encode(charset).strip()) # DISPLAY CALENDAR INFO # ========================================================================= @@ -151,53 +198,54 @@ def main(): logging.debug("-----------------------------------------------------------------------") if calendar_opts['timeformat'] == "12h": - (t_x, t_y) = get_max_char_size(iw, string.digits, calendar_date_font) - (dt_x, dt_y) = iw.getFont(calendar_date_font).getsize(': pm') + (t_x, t_y) = get_max_char_size(red, string.digits, calendar_date_font) + (dt_x, dt_y) = black.getFont(calendar_date_font).getsize(': pm') dt_x = dt_x + (4 * t_x) if t_y > dt_y: dt_y = t_y else: - (t_x, t_y) = get_max_char_size(iw, string.digits, calendar_date_font) - (dt_x, dt_y) = iw.getFont(calendar_date_font).getsize('.') + (t_x, t_y) = get_max_char_size(red, string.digits, calendar_date_font) + (dt_x, dt_y) = black.getFont(calendar_date_font).getsize('.') dt_x = dt_x + (4 * t_x) - (it_x, it_y) = get_max_char_size(iw, string.printable, calendar_entry_font) + (it_x, it_y) = get_max_char_size(red, string.printable, calendar_entry_font) line_height = (2 * dt_y) + (2 * infowindow_opts["cell_spacing"]) - current_calendar_y = 92 + current_calendar_y = 26 for cal_item in cal_items: font_color = 'black' + text = black if cal_item['today']: - font_color = calendar_opts['today_text_color'] - iw.rectangle(0, current_calendar_y, - 313, (current_calendar_y + line_height), + text = red + black.rectangle(0, current_calendar_y, + 285, (current_calendar_y + line_height), calendar_opts['today_background_color']) # draw horizontal line - iw.line(0, (current_calendar_y + line_height + 1), - 313, (current_calendar_y + line_height + 1), + red.line(0, (current_calendar_y + line_height + 1), + 285, (current_calendar_y + line_height + 1), 'black') # draw vertical line - iw.line((dt_x + (2 * infowindow_opts["cell_spacing"]) + 1), current_calendar_y, + red.line((dt_x + (2 * infowindow_opts["cell_spacing"]) + 1), current_calendar_y, (dt_x + (2 * infowindow_opts["cell_spacing"]) + 1), (current_calendar_y + line_height), 'black') # draw event date - iw.text((infowindow_opts["cell_spacing"]), + text.text((infowindow_opts["cell_spacing"]), (current_calendar_y + infowindow_opts["cell_spacing"]), cal_item['date'].encode(charset).strip(), calendar_date_font, font_color) # draw event time - iw.text((infowindow_opts["cell_spacing"]), + text.text((infowindow_opts["cell_spacing"]), (current_calendar_y + ((line_height - 2 * infowindow_opts["cell_spacing"]) / 2)), cal_item['time'].encode(charset).strip(), calendar_date_font, font_color) # draw event text calendar_event_text_start = dt_x + (3 * infowindow_opts["cell_spacing"]) + 1 - max_event_text_length = 313 - calendar_event_text_start - infowindow_opts["cell_spacing"] - iw.text(calendar_event_text_start, + max_event_text_length = 285 - calendar_event_text_start - infowindow_opts["cell_spacing"] + text.text(calendar_event_text_start, (current_calendar_y + ((line_height - it_y) / 2)), - iw.truncate(cal_item['content'].encode(charset).strip(), calendar_entry_font, max_event_text_length), + black.truncate(cal_item['content'].encode(charset).strip(), calendar_entry_font, max_event_text_length), calendar_entry_font, font_color) # set new line height for next round @@ -205,51 +253,32 @@ def main(): # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) logging.debug("ITEM: %s" % cal_item['content'].encode(charset).strip()) - # DISPLAY WEATHER INFO - # ========================================================================= - weather = weather.list() - logging.debug("Weather Info") - logging.debug("-----------------------------------------------------------------------") - # Set unit descriptors - if weather_opts['units'] == 'imperial': - u_speed = u"mph" - u_temp = u"F" - elif weather_opts['units'] == 'metric': - u_speed = u"m/sec" - u_temp = u"C" - else: - u_speed = u"m/sec" - u_temp = u"K" - - deg_symbol = u"\u00b0" - iw.bitmap(2, 2, weather['icon']) - iw.text(70, 2, weather['description'].title().encode(charset).strip(), 'robotoBlack24', 'black') - iw.text(70, 35, weather['sunrise'], 'robotoRegular18', 'black') - iw.text(154, 35, weather['sunset'], 'robotoRegular18', 'black') - - # Temp ( adjust for str length ) - (t_x, t_y) = iw.getFont('robotoBlack48').getsize(str(weather['temp_cur']) + deg_symbol) - temp_left = (iw.width / 2) - (t_x / 2) - iw.text(temp_left, 2, str(weather['temp_cur']) + deg_symbol, 'robotoBlack48', 'white') - t_desc_posx = (temp_left + t_x) - 15 - iw.text(t_desc_posx, 25, u_temp, 'robotoBlack18', 'white') - - # Wind - iw.text(405, 5, weather['wind']['dir'], 'robotoBlack18', 'black') - iw.text(380, 35, str(weather['wind']['speed']) + u_speed, 'robotoRegular18', 'black') - - # Rain - iw.text(481, 29, "1hr: " + str(weather['rain']['1h']), 'robotoRegular18', 'black') - iw.text(481, 44, "3hr: " + str(weather['rain']['3h']), 'robotoRegular18', 'black') - - # Snow - iw.text(573, 29, "1hr: " + str(weather['snow']['1h']), 'robotoRegular18', 'black') - iw.text(573, 44, "3hr: " + str(weather['snow']['3h']), 'robotoRegular18', 'black') - # Write to screen # ========================================================================= - iw.display(rotation) - + red.image = red.image.rotate(rotation) + black.image = black.image.rotate(rotation) + + new_image_found = 0 + if os.path.exists(red.tmpImagePath): + old_image = Image.open(red.tmpImagePath) + diff = ImageChops.difference(red.image, old_image) + if not diff.getbbox(): + new_image_found += 1 + + if os.path.exists(black.tmpImagePath): + old_image = Image.open(black.tmpImagePath) + diff = ImageChops.difference(black.image, old_image) + if not diff.getbbox(): + new_image_found += 1 + + if new_image_found < 2: + logging.info("New information in the image detected. Updating the screen.") + red.image.save(red.tmpImagePath) + black.image.save(black.tmpImagePath) + red.epd.display(black.epd.getbuffer(black.image),red.epd.getbuffer(red.image)) + red.epd.sleep() + else: + logging.info("No new information found. Not updating the screen.") if __name__ == '__main__': main() diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 563963a..3b8b753 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -13,7 +13,7 @@ def __init__(self, options): ga = mod_google_auth.GoogleAuth() self.creds = ga.login() self.timeformat = options["timeformat"] - self.additional = options["additional"] + self.include = options["include"] self.ignored = options["ignored"] def list(self): @@ -28,10 +28,11 @@ def list(self): while True: calendar_list = service.calendarList().list(pageToken=page_token).execute() for calendar_list_entry in calendar_list['items']: - if "primary" in calendar_list_entry.keys(): + if "primary" in calendar_list_entry.keys() and "Primary" in self.include: if calendar_list_entry['primary']: calendar_ids.append(calendar_list_entry['id']) - elif calendar_list_entry['summary'] in self.additional: + print(str(calendar_list_entry['primary'])) + elif calendar_list_entry['summary'] in self.include: calendar_ids.append(calendar_list_entry['id']) page_token = calendar_list.get('nextPageToken') if not page_token: @@ -66,7 +67,7 @@ def list(self): # Sunrise and Sunset. if self.timeformat == "12h": - st_date = dt.strftime(dtparse(start), format='%m-%d') + st_date = dt.strftime(dtparse(start), format='%a %d-%m') st_time = dt.strftime(dtparse(start), format='%I:%M %p') else: st_date = dt.strftime(dtparse(start), format='%d.%m') diff --git a/mod_infowindow/.gitignore b/mod_infowindow/.gitignore new file mode 100755 index 0000000..fc842ce --- /dev/null +++ b/mod_infowindow/.gitignore @@ -0,0 +1,9 @@ +google_secret.json +*.pickle +*.code-workspace +*.pyc +icons/*.png +test.py +config.json +infowindow.jpg +venv/ diff --git a/mod_infowindow/clearScreen.py b/mod_infowindow/clearScreen.py new file mode 100755 index 0000000..67d9628 --- /dev/null +++ b/mod_infowindow/clearScreen.py @@ -0,0 +1,24 @@ +#!/usr/bin/python +# -*- coding:utf-8 -*- +import sys +import os +picdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'pic') +libdir = os.path.join(os.path.dirname(os.path.dirname(os.path.realpath(__file__))), 'lib') +if os.path.exists(libdir): + sys.path.append(libdir) + +import logging +from driver import epd7in5b +import time +from PIL import Image,ImageDraw,ImageFont +import traceback + +logging.basicConfig(level=logging.DEBUG) + +logging.info("epd7in5b_HD Demo") + +epd = epd7in5b.EPD() + +logging.info("init and Clear") +epd.init() +epd.Clear() diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index dab37e8..cc7dcd8 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -1,101 +1,97 @@ -from driver import epd7in5b -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont -from PIL import ImageChops -import os, sys -import logging -import tempfile - - -class InfoWindow: - def __init__(self, options): - self.epd = epd7in5b.EPD() - self.epd.init() - self.width = 640 - self.height = 384 - self.image = Image.new('L', (640, 384), 255) - self.draw = ImageDraw.Draw(self.image) - self.fonts = {} - self.initFonts() - self.tmpImagePath = os.path.join(tempfile.gettempdir(), "InfoWindow.png") - self.timeformat = options['timeformat'] - - def getCWD(self): - path = os.path.dirname(os.path.realpath(sys.argv[0])) - return path - - def getImage(self): - return self.image - - def getDraw(self): - return self.draw - - def getEpd(self): - return self.epd - - def line(self, left_1, top_1, left_2, top_2, fill, width=1): - self.draw.line((left_1, top_1, left_2, top_2), fill=fill) - - def rectangle(self, tl, tr, bl, br, fill): - self.draw.rectangle(((tl, tr), (bl, br)), fill=fill) - - def text(self, left, top, text, font, fill): - font = self.fonts[font] - self.draw.text((left, top), text, font=font, fill=fill) - return self.draw.textsize(text, font=font) - - def rotate(self, angle): - self.image.rotate(angle) - - # def chord(self, x, y, xx, yy, xxx, yyy, fill): - # self.draw.chord((x, y, xx, yy), xxx, yyy, fill) - - def bitmap(self, x, y, image_path): - bitmap = Image.open(self.getCWD()+"/icons/"+image_path) - # self.image.paste((0, 0), (x, y), 'black', bitmap) - self.draw.bitmap((x, y), bitmap) - - def getFont(self, font_name): - return self.fonts[font_name] - - def initFonts(self): - roboto = self.getCWD()+"/fonts/roboto/Roboto-" - self.fonts = { - - 'robotoBlack24': ImageFont.truetype(roboto+"Black.ttf", 24), - 'robotoBlack18': ImageFont.truetype(roboto+"Black.ttf", 18), - 'robotoRegular18': ImageFont.truetype(roboto+"Regular.ttf", 18), - 'robotoRegular14': ImageFont.truetype(roboto+"Regular.ttf", 14), - 'robotoBlack48': ImageFont.truetype(roboto+"Black.ttf", 48) - } - - def truncate(self, string, font, max_size): - num_chars = len(string) - for char in string: - (np_x, np_y) = self.getFont(font).getsize(string) - if np_x >= max_size: - string = string[:-1] - - if np_x <= max_size: - return string - - return string - - def display(self, angle): - self.image = self.image.rotate(angle) - - new_image_found = True - if os.path.exists(self.tmpImagePath): - old_image = Image.open(self.tmpImagePath) - diff = ImageChops.difference(self.image, old_image) - if not diff.getbbox(): - new_image_found = False - - if new_image_found: - logging.info("New information in the image detected. Updating the screen.") - self.image.save(self.tmpImagePath) - self.epd.display_frame(self.epd.get_frame_buffer(self.image)) - self.epd.sleep() - else: - logging.info("No new information found. Not updating the screen.") +from driver import epd7in5b +from PIL import Image +from PIL import ImageDraw +from PIL import ImageFont +from PIL import ImageChops +import os, sys +import logging +import tempfile + +class InfoWindow: + def __init__(self, options, colour): + self.epd = epd7in5b.EPD() + self.epd.init() + self.width = 880 + self.height = 528 + self.image = Image.new('L', (880, 528), 255) + self.draw = ImageDraw.Draw(self.image) + self.fonts = {} + self.colour = colour + self.initFonts() + self.tmpImagePath = os.path.join(tempfile.gettempdir(), colour+".png") + self.timeformat = options['timeformat'] + + def getCWD(self): + path = os.path.dirname(os.path.realpath(sys.argv[0])) + return path + + def getImage(self): + return self.image + + def getDraw(self): + return self.draw + + def getEpd(self): + return self.epd + + def line(self, left_1, top_1, left_2, top_2, fill=0, width=1): + self.draw.line((left_1, top_1, left_2, top_2), fill=fill) + + def rectangle(self, tl, tr, bl, br, fill=0): + self.draw.rectangle(((tl, tr), (bl, br)), fill=fill) + + def text(self, left, top, text, font, fill=0): + font = self.fonts[font] + self.draw.text((left, top), text, font=font, fill=fill) + return self.draw.textsize(text, font=font) + + def rtext(self, right, top, text, font, fill=0): + font = self.fonts[font] + self.draw.text((right, top), text, font=font, fill=fill) + return self.draw.textsize(text, font=font) + + def textwidth(self, text, font): + font = self.fonts[font] + ascent, descent = font.getmetrics() + text_width = font.getmask(text).getbbox()[2] + text_height = font.getmask(text).getbbox()[3] + descent + text_width = (text_width/2) + return text_width + + def rotate(self, angle): + self.image.rotate(angle) + + # def chord(self, x, y, xx, yy, xxx, yyy, fill): + # self.draw.chord((x, y, xx, yy), xxx, yyy, fill) + + def bitmap(self, x, y, image_path): + bitmap = Image.open(self.getCWD()+"/icons/"+image_path) + # self.image.paste((0, 0), (x, y), 'black', bitmap) + self.draw.bitmap((x, y), bitmap) + + def getFont(self, font_name): + return self.fonts[font_name] + + def initFonts(self): + roboto = self.getCWD()+"/fonts/roboto/Roboto-" + self.fonts = { + 'robotoBlack24': ImageFont.truetype(roboto+"Black.ttf", 24), + 'robotoBlack18': ImageFont.truetype(roboto+"Black.ttf", 18), + 'robotoRegular18': ImageFont.truetype(roboto+"Regular.ttf", 18), + 'robotoRegular14': ImageFont.truetype(roboto+"Regular.ttf", 14), + 'robotoBlack48': ImageFont.truetype(roboto+"Black.ttf", 48), + 'robotoBlack36': ImageFont.truetype(roboto+"Black.ttf", 36) + } + + def truncate(self, string, font, max_size): + num_chars = len(string) + for char in string: + (np_x, np_y) = self.getFont(font).getsize(string) + if np_x >= max_size: + string = string[:-1] + if np_x <= max_size: + return string + return string + + def spare(): + return diff --git a/mod_todo/mod_grocy.py b/mod_todo/mod_grocy.py new file mode 100755 index 0000000..7233dc4 --- /dev/null +++ b/mod_todo/mod_grocy.py @@ -0,0 +1,39 @@ +import requests +import logging +import json +import datetime + + +class test: + def __init__(self, opts): + logging.debug("Grocy API: GROCY") + self.api = False + headers = { + 'accept': 'application/json', + 'GROCY-API-KEY': opts['api_key'], + } + if not opts['api_key']: + logging.warning("Not loading Todo API, since no api key is configured") + else: + self.api = requests.get("https://"+opts['server']+":"+opts['port']+"/api/stock", headers=headers) + + def list(self): + today = datetime.date.today() + items = [] + # Loop through original array from Grocy and pull out items by soonest best before date + if self.api: + data = json.loads(self.api.content) + for item in data: + best_before = item['best_before_date'] + best_before_date = datetime.date(int(best_before[0:4]),int(best_before[5:7]),int(best_before[8:10])) + days = best_before_date - today + items.append({ + "content": item['product']['name'], + "best_before": item['best_before_date'], + "days": str(days.days) + }) + + items = sorted(items, key = lambda i: i['best_before']) + + + return items diff --git a/mod_todo/mod_todoist.py b/mod_todo/mod_todoist.py index c0b74ff..2199faf 100644 --- a/mod_todo/mod_todoist.py +++ b/mod_todo/mod_todoist.py @@ -14,19 +14,30 @@ def __init__(self, opts): def list(self): items = [] - # Loop through original array from Todoist and pull out + # Loop through original array from Todoist and pull out # items of interest if self.api: for item in self.api.state['items']: if item['checked'] == 0: - items.append({ - "content": item['content'], - "priority": item['priority'], - }) + if item['due']: + items.append({ + "content": item['content'], + "priority": item['priority'], + "labels":item['labels'], + "due":item['due']['date'] + }) + else: + items.append({ + "content": item['content'], + "priority": item['priority'], + "labels":item['labels'], + "due":0 + }) + # Sort the array by priority items = sorted(items, key = lambda i: i['priority']) - + unicode(items).encode('utf8') # Reverse list, since Todoist sets priority in reverse. # On web interface HIGH=Priority1, but stored in API as 4. who knows?! items.reverse() diff --git a/screensaver.py b/screensaver.py index c1aad2b..3708846 100755 --- a/screensaver.py +++ b/screensaver.py @@ -2,6 +2,8 @@ import logging import os +import sys +import tempfile from driver import epd7in5b from PIL import Image @@ -14,15 +16,20 @@ def main(): epd = epd7in5b.EPD() epd.init() - images = ["red.png", "black.png", "white.png"] - for image in images: - logging.info("Display %s" % image) - image_data = Image.open(os.path.join("resources", image)) - epd.display_frame(epd.get_frame_buffer(image_data)) - + black = Image.open(os.path.join(os.path.dirname(sys.argv[0]), "resources", "black.png")) + red = Image.open(os.path.join(os.path.dirname(sys.argv[0]),"resources", "red.png")) + white = Image.open(os.path.join(os.path.dirname(sys.argv[0]),"resources", "white.png")) + epd.display(epd.getbuffer(black),epd.getbuffer(white)) + logging.info("Display black") + epd.display(epd.getbuffer(white),epd.getbuffer(black)) + logging.info("Display red") + epd.display(epd.getbuffer(white),epd.getbuffer(white)) + logging.info("Display white") + epd.sleep() logging.info("Screen saver finished") + os.remove(os.path.join(tempfile.gettempdir(), "black.png")) if __name__ == '__main__': main()