From f7c5d7c407a83065c57431b22917444212e3eedb Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 17:40:36 +0100 Subject: [PATCH 001/113] fixing unsupported operand type --- mod_utils/mod_google_auth.py | 142 +++++++++++++++++------------------ 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/mod_utils/mod_google_auth.py b/mod_utils/mod_google_auth.py index 658ae9b..90ed3ba 100644 --- a/mod_utils/mod_google_auth.py +++ b/mod_utils/mod_google_auth.py @@ -1,72 +1,72 @@ -from apiclient.discovery import build -from google_auth_oauthlib.flow import InstalledAppFlow -from google.auth.transport.requests import Request -from mod_utils import iw_utils -import pickle -import os.path -import sys -import logging - -logger = logging.getLogger(__name__) - - -class GoogleAuth: - def __init__(self): - logger.info("Initializing Module: GoogleAuth") - self.scopes = [ - 'https://www.googleapis.com/auth/calendar', - 'https://www.googleapis.com/auth/tasks' - ] - - self.creds = None - - def getCWD(self): - path = os.path.dirname(os.path.realpath(sys.argv[0])) - return path - - def login(self): - - # Check for pickle. - # if os.path.exists('token.pickle'): - if os.path.exists(self.getCWD()+'/token.pickle'): - logger.info("token.pickle Exists. Attempting read") - with open(self.getCWD()+'/token.pickle', 'rb') as token: - self.creds = pickle.load(token) - else: - logger.info(self.getCWD+"/token.pickle NOT FOUND") - - # If there are no valid creds, let user login. - # If we get to this point there is a user interaction that needs - # to happen. Must generate some sort of display on e-ink to let the - # user know that they need to run interactivly. - if not self.creds or not self.creds.valid: - logger.info("Credentials do not exist, or are not valid.") - - # Requires input from user. Write error to e-ink if is run from cron. - # if iw_utils.isCron(): - # iw_utils.HandleError("Google Credentials do not exist, or are not valid") - - if self.creds and self.creds.expired and self.creds.refresh_token: - logging.info("Refreshing Google Auth Credentials") - self.creds.refresh(Request()) - else: - # Check to see if google_secret.json exists. Throw error if not - if not os.path.exists(self.getCWD+'/google_secret.json'): - logger.info(self.getCWD+"/google_secret.json does not exist") - - # Requires input from user. Write error to e-ink if is run from cron. - if iw_utils.isCron(): - iw_utils.HandleError('Message') - - flow = InstalledAppFlow.from_client_secrets_file( - self.getCWD()+'/google_secret.json', self.scopes - ) - - self.creds = flow.run_console() - - # Write pickle file - logger.info("Writing "+self.getCWD()+"/token.pickle file") - with open(self.getCWD()+'/token.pickle', 'wb') as token: - pickle.dump(self.creds, token) - +from apiclient.discovery import build +from google_auth_oauthlib.flow import InstalledAppFlow +from google.auth.transport.requests import Request +from mod_utils import iw_utils +import pickle +import os.path +import sys +import logging + +logger = logging.getLogger(__name__) + + +class GoogleAuth: + def __init__(self): + logger.info("Initializing Module: GoogleAuth") + self.scopes = [ + 'https://www.googleapis.com/auth/calendar', + 'https://www.googleapis.com/auth/tasks' + ] + + self.creds = None + + def getCWD(self): + path = os.path.dirname(os.path.realpath(sys.argv[0])) + return path + + def login(self): + + # Check for pickle. + # if os.path.exists('token.pickle'): + if os.path.exists(os.path.join(self.getCWD(),'/token.pickle')): + logger.info("token.pickle Exists. Attempting read") + with open(self.getCWD()+'/token.pickle', 'rb') as token: + self.creds = pickle.load(token) + else: + logger.info(self.getCWD+"/token.pickle NOT FOUND") + + # If there are no valid creds, let user login. + # If we get to this point there is a user interaction that needs + # to happen. Must generate some sort of display on e-ink to let the + # user know that they need to run interactivly. + if not self.creds or not self.creds.valid: + logger.info("Credentials do not exist, or are not valid.") + + # Requires input from user. Write error to e-ink if is run from cron. + # if iw_utils.isCron(): + # iw_utils.HandleError("Google Credentials do not exist, or are not valid") + + if self.creds and self.creds.expired and self.creds.refresh_token: + logging.info("Refreshing Google Auth Credentials") + self.creds.refresh(Request()) + else: + # Check to see if google_secret.json exists. Throw error if not + if not os.path.exists(self.getCWD+'/google_secret.json'): + logger.info(self.getCWD+"/google_secret.json does not exist") + + # Requires input from user. Write error to e-ink if is run from cron. + if iw_utils.isCron(): + iw_utils.HandleError('Message') + + flow = InstalledAppFlow.from_client_secrets_file( + self.getCWD()+'/google_secret.json', self.scopes + ) + + self.creds = flow.run_console() + + # Write pickle file + logger.info("Writing "+self.getCWD()+"/token.pickle file") + with open(self.getCWD()+'/token.pickle', 'wb') as token: + pickle.dump(self.creds, token) + return self.creds \ No newline at end of file From fd4abd720904ef42ba1d944e724887f1763de7d5 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 17:42:20 +0100 Subject: [PATCH 002/113] fixing unsupported operand type --- mod_utils/mod_google_auth.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mod_utils/mod_google_auth.py b/mod_utils/mod_google_auth.py index 90ed3ba..8f76732 100644 --- a/mod_utils/mod_google_auth.py +++ b/mod_utils/mod_google_auth.py @@ -28,12 +28,13 @@ def login(self): # Check for pickle. # if os.path.exists('token.pickle'): - if os.path.exists(os.path.join(self.getCWD(),'/token.pickle')): + file_path = os.path.join(self.getCWD(), '/token.pickle') + if os.path.exists(file_path): logger.info("token.pickle Exists. Attempting read") - with open(self.getCWD()+'/token.pickle', 'rb') as token: + with open(file_path, 'rb') as token: self.creds = pickle.load(token) else: - logger.info(self.getCWD+"/token.pickle NOT FOUND") + logger.info("%s NOT FOUND" % file_path) # If there are no valid creds, let user login. # If we get to this point there is a user interaction that needs From 3cb2cadb2abda36ff050394de2eb9815aba12f43 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 17:44:48 +0100 Subject: [PATCH 003/113] fixing unsupported operand type --- mod_utils/mod_google_auth.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/mod_utils/mod_google_auth.py b/mod_utils/mod_google_auth.py index 8f76732..0115969 100644 --- a/mod_utils/mod_google_auth.py +++ b/mod_utils/mod_google_auth.py @@ -28,13 +28,13 @@ def login(self): # Check for pickle. # if os.path.exists('token.pickle'): - file_path = os.path.join(self.getCWD(), '/token.pickle') - if os.path.exists(file_path): + pickle_token_file_path = os.path.join(self.getCWD(), '/token.pickle') + if os.path.exists(pickle_token_file_path): logger.info("token.pickle Exists. Attempting read") - with open(file_path, 'rb') as token: + with open(pickle_token_file_path, 'rb') as token: self.creds = pickle.load(token) else: - logger.info("%s NOT FOUND" % file_path) + logger.info("%s NOT FOUND" % pickle_token_file_path) # If there are no valid creds, let user login. # If we get to this point there is a user interaction that needs @@ -52,22 +52,23 @@ def login(self): self.creds.refresh(Request()) else: # Check to see if google_secret.json exists. Throw error if not - if not os.path.exists(self.getCWD+'/google_secret.json'): - logger.info(self.getCWD+"/google_secret.json does not exist") + google_secrets_file_path = os.path.join(self.getCWD, '/google_secret.json') + if not os.path.exists(google_secrets_file_path): + logger.info("%s does not exist" % google_secrets_file_path) # Requires input from user. Write error to e-ink if is run from cron. if iw_utils.isCron(): iw_utils.HandleError('Message') flow = InstalledAppFlow.from_client_secrets_file( - self.getCWD()+'/google_secret.json', self.scopes + google_secrets_file_path, self.scopes ) self.creds = flow.run_console() # Write pickle file - logger.info("Writing "+self.getCWD()+"/token.pickle file") - with open(self.getCWD()+'/token.pickle', 'wb') as token: + logger.info("Writing %s file", pickle_token_file_path) + with open(pickle_token_file_path, 'wb') as token: pickle.dump(self.creds, token) return self.creds \ No newline at end of file From df43c9227dca606c3198121112629f8709242fff Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 17:46:22 +0100 Subject: [PATCH 004/113] fixing unsupported operand type --- mod_utils/mod_google_auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mod_utils/mod_google_auth.py b/mod_utils/mod_google_auth.py index 0115969..49bc447 100644 --- a/mod_utils/mod_google_auth.py +++ b/mod_utils/mod_google_auth.py @@ -28,7 +28,7 @@ def login(self): # Check for pickle. # if os.path.exists('token.pickle'): - pickle_token_file_path = os.path.join(self.getCWD(), '/token.pickle') + pickle_token_file_path = os.path.join(self.getCWD(), 'token.pickle') if os.path.exists(pickle_token_file_path): logger.info("token.pickle Exists. Attempting read") with open(pickle_token_file_path, 'rb') as token: @@ -52,7 +52,7 @@ def login(self): self.creds.refresh(Request()) else: # Check to see if google_secret.json exists. Throw error if not - google_secrets_file_path = os.path.join(self.getCWD, '/google_secret.json') + google_secrets_file_path = os.path.join(self.getCWD, 'google_secret.json') if not os.path.exists(google_secrets_file_path): logger.info("%s does not exist" % google_secrets_file_path) From 08152b86b0d84c1f29cd0b8ce2a3190da28ddca1 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 17:52:24 +0100 Subject: [PATCH 005/113] removing one function call --- mod_utils/mod_google_auth.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/mod_utils/mod_google_auth.py b/mod_utils/mod_google_auth.py index 49bc447..a0e072e 100644 --- a/mod_utils/mod_google_auth.py +++ b/mod_utils/mod_google_auth.py @@ -19,16 +19,13 @@ def __init__(self): ] self.creds = None + self.path = os.path.dirname(os.path.realpath(sys.argv[0])) - def getCWD(self): - path = os.path.dirname(os.path.realpath(sys.argv[0])) - return path - - def login(self): + def login(self): # Check for pickle. # if os.path.exists('token.pickle'): - pickle_token_file_path = os.path.join(self.getCWD(), 'token.pickle') + pickle_token_file_path = os.path.join(self.path, 'token.pickle') if os.path.exists(pickle_token_file_path): logger.info("token.pickle Exists. Attempting read") with open(pickle_token_file_path, 'rb') as token: @@ -52,7 +49,7 @@ def login(self): self.creds.refresh(Request()) else: # Check to see if google_secret.json exists. Throw error if not - google_secrets_file_path = os.path.join(self.getCWD, 'google_secret.json') + google_secrets_file_path = os.path.join(self.path, 'google_secret.json') if not os.path.exists(google_secrets_file_path): logger.info("%s does not exist" % google_secrets_file_path) From e281bff16be006b6d45f463869dd15ea01689a56 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 19:31:15 +0100 Subject: [PATCH 006/113] fixing oauth flow --- mod_utils/mod_google_auth.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/mod_utils/mod_google_auth.py b/mod_utils/mod_google_auth.py index a0e072e..7d10bc7 100644 --- a/mod_utils/mod_google_auth.py +++ b/mod_utils/mod_google_auth.py @@ -1,5 +1,5 @@ from apiclient.discovery import build -from google_auth_oauthlib.flow import InstalledAppFlow +from google_auth_oauthlib import flow from google.auth.transport.requests import Request from mod_utils import iw_utils import pickle @@ -57,11 +57,16 @@ def login(self): if iw_utils.isCron(): iw_utils.HandleError('Message') - flow = InstalledAppFlow.from_client_secrets_file( + appflow = flow.InstalledAppFlow.from_client_secrets_file( google_secrets_file_path, self.scopes ) - self.creds = flow.run_console() + if iw_utils.isCron(): + appflow.run_local_server() + else: + appflow.run_console() + + self.creds = appflow.credentials # Write pickle file logger.info("Writing %s file", pickle_token_file_path) From e7e322af1c5b5c8f84e4f40bc5ad1ea10afde230 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:08:15 +0100 Subject: [PATCH 007/113] ignoring rendered screens --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 2248d0b..91f98f7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ google_secret.json icons/*.png test.py config.json - +infowindow.jpg From 50acde6d71315e474066db392b8683cfc0e4f79e Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:14:18 +0100 Subject: [PATCH 008/113] if no api key is configure, don't fail --- mod_todo/mod_todoist.py | 63 ++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/mod_todo/mod_todoist.py b/mod_todo/mod_todoist.py index 1cf5235..c0b74ff 100644 --- a/mod_todo/mod_todoist.py +++ b/mod_todo/mod_todoist.py @@ -1,29 +1,34 @@ -import todoist -import logging - - -class ToDo: - def __init__(self, opts): - logging.debug("Todo API: TODOIST") - self.api = todoist.TodoistAPI(opts['api_key']) - self.api.sync() - - def list(self): - items = [] - # Loop through original array from Todoist and pull out - # items of interest - for item in self.api.state['items']: - if item['checked'] == 0: - items.append({ - "content": item['content'], - "priority": item['priority'], - }) - - # Sort the array by priority - items = sorted(items, key = lambda i: i['priority']) - - # Reverse list, since Todoist sets priority in reverse. - # On web interface HIGH=Priority1, but stored in API as 4. who knows?! - items.reverse() - - return items \ No newline at end of file +import todoist +import logging + + +class ToDo: + def __init__(self, opts): + logging.debug("Todo API: TODOIST") + self.api = False + if not opts['api_key']: + logging.warning("Not loading Todo API, since no api key is configured") + else: + self.api = todoist.TodoistAPI(opts['api_key']) + self.api.sync() + + def list(self): + items = [] + # 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'], + }) + + # Sort the array by priority + items = sorted(items, key = lambda i: i['priority']) + + # Reverse list, since Todoist sets priority in reverse. + # On web interface HIGH=Priority1, but stored in API as 4. who knows?! + items.reverse() + + return items From e5c2b98e97c3e823c21c0b2171f73deb1d4f4239 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:15:41 +0100 Subject: [PATCH 009/113] remove unused import --- mod_utils/mod_google_auth.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mod_utils/mod_google_auth.py b/mod_utils/mod_google_auth.py index 7d10bc7..5e5aa80 100644 --- a/mod_utils/mod_google_auth.py +++ b/mod_utils/mod_google_auth.py @@ -1,4 +1,3 @@ -from apiclient.discovery import build from google_auth_oauthlib import flow from google.auth.transport.requests import Request from mod_utils import iw_utils @@ -73,4 +72,4 @@ def login(self): with open(pickle_token_file_path, 'wb') as token: pickle.dump(self.creds, token) - return self.creds \ No newline at end of file + return self.creds From 10a8d5dbae54d52aa81885fd2999da9be93fce33 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:16:49 +0100 Subject: [PATCH 010/113] force python2 since the project is not python3 ready (yet) --- infowindow.py | 352 +++++++++++++++++++++++++------------------------- 1 file changed, 176 insertions(+), 176 deletions(-) diff --git a/infowindow.py b/infowindow.py index 156163b..bd827d9 100755 --- a/infowindow.py +++ b/infowindow.py @@ -1,176 +1,176 @@ -#!/usr/bin/env python - -import sys -import json -import logging -import traceback -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_todoist as modTodo # TODO -from mod_calendar import mod_google as modCalendar # CALENDAR -from mod_weather import mod_owm as modWeather # WEATHER - -# TODO: Create dictionaries for API args. so that they can be custom. - -# Configuration ############################################################### -with open(iw_utils.getCWD()+"/config.json") as config_file: - config_data = json.load(config_file) - -## Rotation. 0 for desktop, 180 for hanging upside down -rotation = config_data["general"]["rotation"] -todo_opts = config_data["todo"] -calendar_opts = config_data["calendar"] -weather_opts = config_data["weather"] - -# END CONFIGURATION ########################################################### -############################################################################### - -# Setup Logging - change to logging.DEBUG if you are having issues. -logging.basicConfig(level=logging.DEBUG) -logging.info("Configuration Complete") - -# Custom exception handler. Need to handle exceptions and send them to the -# 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, str(val), 'robotoBlack18', 'black') - iw.text(0, 60, "Please run program from command line interactivly to resolve", 'robotoBlack18', 'black') - print "EXCEPTION IN PROGRAM ==================================" - print val - print et - print tb - print "END EXCEPTION =========================================" - iw.display(rotation) - -sys.excepthook = HandleException - -# Main Program ################################################################ -def main(): - # Instantiate API modules - todo = modTodo.ToDo(todo_opts) - cal = modCalendar.Cal(calendar_opts) - weather = modWeather.Weather(weather_opts) - - ## Setup e-ink initial drawings - iw = infowindow.InfoWindow() - - ### 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 / 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 - - # Todo / Weather Titles - iw.text(440, 64, "TODO", 'robotoBlack24', 'white') - iw.text(95, 64, "CALENDAR", 'robotoBlack24', 'white') - - - # DISPLAY TODO INFO - # ========================================================================= - todo_items = todo.list() - logging.debug("Todo Items") - logging.debug("-----------------------------------------------------------------------") - t_y = 94 - for todo_item in todo_items: - iw.text(333, t_y, str(todo_item['content']), 'robotoRegular18', 'black') - t_y = (t_y + 24) - iw.line(325, (t_y - 2), 640, (t_y - 2), 'black') - logging.debug("ITEM: "+todo_item['content']) - - # DISPLAY CALENDAR INFO - # ========================================================================= - cal_items = cal.list() - logging.debug("Calendar Items") - logging.debug("-----------------------------------------------------------------------") - c_y = 94 - - # Time and date divider line - (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('12-99-2000') - - for cal_item in cal_items: - (x, y) = iw.text(3, c_y, str(cal_item['date']), 'robotoRegular14', 'black') - iw.line((dt_x + 5), c_y, (dt_x + 5), (c_y +32), 'black') - iw.text(3, (c_y + 15), str(cal_item['time']), 'robotoRegular14', 'black') - iw.text((dt_x + 7), (c_y + 5), iw.truncate(str(cal_item['content']), 'robotoRegular18'), 'robotoRegular18', 'black') - c_y = (c_y + 32) - iw.line(0, (c_y - 2), 313, (c_y - 2), 'black') - # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) - logging.debug("ITEM: "+str(cal_item['content'])) - - # DISPLAY WEATHER INFO - # ========================================================================= - weather = weather.list() - logging.debug("Weather Info") - logging.debug("-----------------------------------------------------------------------") - # Set unit descriptors - if weather_opts['units'] == 'imperial': - u_speed = "mph" - u_temp = "F" - elif weather_opts['units'] == 'metric': - u_speed = "m/sec" - u_temp = "C" - else: - u_speed = "m/sec" - u_temp = "K" - - deg_symbol = u"\u00b0" - iw.bitmap(2, 2, weather['icon']) - iw.text(70, 2, weather['description'].title(), '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) - -if __name__ == '__main__': - main() \ No newline at end of file +#!/usr/bin/env python2 + +import sys +import json +import logging +import traceback +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_todoist as modTodo # TODO +from mod_calendar import mod_google as modCalendar # CALENDAR +from mod_weather import mod_owm as modWeather # WEATHER + +# TODO: Create dictionaries for API args. so that they can be custom. + +# Configuration ############################################################### +with open(iw_utils.getCWD()+"/config.json") as config_file: + config_data = json.load(config_file) + +## Rotation. 0 for desktop, 180 for hanging upside down +rotation = config_data["general"]["rotation"] +todo_opts = config_data["todo"] +calendar_opts = config_data["calendar"] +weather_opts = config_data["weather"] + +# END CONFIGURATION ########################################################### +############################################################################### + +# Setup Logging - change to logging.DEBUG if you are having issues. +logging.basicConfig(level=logging.DEBUG) +logging.info("Configuration Complete") + +# Custom exception handler. Need to handle exceptions and send them to the +# 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, str(val), 'robotoBlack18', 'black') + iw.text(0, 60, "Please run program from command line interactivly to resolve", 'robotoBlack18', 'black') + print "EXCEPTION IN PROGRAM ==================================" + print val + print et + print tb + print "END EXCEPTION =========================================" + iw.display(rotation) + +sys.excepthook = HandleException + +# Main Program ################################################################ +def main(): + # Instantiate API modules + todo = modTodo.ToDo(todo_opts) + cal = modCalendar.Cal(calendar_opts) + weather = modWeather.Weather(weather_opts) + + ## Setup e-ink initial drawings + iw = infowindow.InfoWindow() + + ### 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 / 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 + + # Todo / Weather Titles + iw.text(440, 64, "TODO", 'robotoBlack24', 'white') + iw.text(95, 64, "CALENDAR", 'robotoBlack24', 'white') + + + # DISPLAY TODO INFO + # ========================================================================= + todo_items = todo.list() + logging.debug("Todo Items") + logging.debug("-----------------------------------------------------------------------") + t_y = 94 + for todo_item in todo_items: + iw.text(333, t_y, str(todo_item['content']), 'robotoRegular18', 'black') + t_y = (t_y + 24) + iw.line(325, (t_y - 2), 640, (t_y - 2), 'black') + logging.debug("ITEM: "+todo_item['content']) + + # DISPLAY CALENDAR INFO + # ========================================================================= + cal_items = cal.list() + logging.debug("Calendar Items") + logging.debug("-----------------------------------------------------------------------") + c_y = 94 + + # Time and date divider line + (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('12-99-2000') + + for cal_item in cal_items: + (x, y) = iw.text(3, c_y, str(cal_item['date']), 'robotoRegular14', 'black') + iw.line((dt_x + 5), c_y, (dt_x + 5), (c_y +32), 'black') + iw.text(3, (c_y + 15), str(cal_item['time']), 'robotoRegular14', 'black') + iw.text((dt_x + 7), (c_y + 5), iw.truncate(str(cal_item['content']), 'robotoRegular18'), 'robotoRegular18', 'black') + c_y = (c_y + 32) + iw.line(0, (c_y - 2), 313, (c_y - 2), 'black') + # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) + logging.debug("ITEM: "+str(cal_item['content'])) + + # DISPLAY WEATHER INFO + # ========================================================================= + weather = weather.list() + logging.debug("Weather Info") + logging.debug("-----------------------------------------------------------------------") + # Set unit descriptors + if weather_opts['units'] == 'imperial': + u_speed = "mph" + u_temp = "F" + elif weather_opts['units'] == 'metric': + u_speed = "m/sec" + u_temp = "C" + else: + u_speed = "m/sec" + u_temp = "K" + + deg_symbol = u"\u00b0" + iw.bitmap(2, 2, weather['icon']) + iw.text(70, 2, weather['description'].title(), '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) + +if __name__ == '__main__': + main() From 7a7fad09f214317712962265dbb180e0a3fbfedc Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:17:24 +0100 Subject: [PATCH 011/113] remove unused import --- infowindow.py | 1 - 1 file changed, 1 deletion(-) diff --git a/infowindow.py b/infowindow.py index bd827d9..725be4d 100755 --- a/infowindow.py +++ b/infowindow.py @@ -3,7 +3,6 @@ import sys import json import logging -import traceback from mod_infowindow import infowindow # Select pluggable module for todo list, calendar and weather. From c98d04af39522fdceb76fcade53f0a86326b6098 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:18:26 +0100 Subject: [PATCH 012/113] documenting how to add google secrets (to use the google APIs) --- README.md | 40 ++++++++++++++++++++++++++++++---------- 1 file changed, 30 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a468091..3ba9059 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,13 @@ # 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! +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! -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. +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 | @@ -23,31 +27,47 @@ The functionality is not meant to be an "end all solution for calendaring and To ## Installation ### 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` +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. +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. +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. ### 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. +* 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. -### Todo -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` +### 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 +### 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. ## 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. +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` From c0f361042a63888682c2172399b2bbc818e40a5c Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:19:39 +0100 Subject: [PATCH 013/113] fixing indentation --- mod_calendar/mod_google.py | 79 +++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index b8439f0..162643c 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -1,40 +1,39 @@ -from mod_utils import mod_google_auth -from googleapiclient.discovery import build -from dateutil.parser import parse as dtparse -from datetime import datetime as dt -import logging - -# Silence goofy google deprecated errors -logging.getLogger('googleapiclient.discovery_cache').setLevel(logging.ERROR) - - -class Cal: - def __init__(self, api_key): - ga = mod_google_auth.GoogleAuth() - self.creds = ga.login() - - def list(self): - service = build('calendar', 'v3', credentials=self.creds) - - now = dt.utcnow().isoformat() + 'Z' - result = service.events().list(calendarId='primary', timeMin=now, - maxResults=20, - singleEvents=True, - orderBy='startTime').execute() - - events = result.get('items', []) - - # 2019-11-05T10:00:00-08:00 - items = [] - for event in events: - start = event['start'].get('dateTime', event['start'].get('date')) - st_date = dt.strftime(dtparse(start), format='%m-%d-%Y') - st_time = dt.strftime(dtparse(start), format='%I:%M%p') - items.append({ - "date": st_date, - "time": st_time, - "content": event['summary'] - }) - - return items - +from mod_utils import mod_google_auth +from googleapiclient.discovery import build +from dateutil.parser import parse as dtparse +from datetime import datetime as dt +import logging + +# Silence goofy google deprecated errors +logging.getLogger('googleapiclient.discovery_cache').setLevel(logging.ERROR) + + +class Cal: + def __init__(self, api_key): + ga = mod_google_auth.GoogleAuth() + self.creds = ga.login() + + def list(self): + service = build('calendar', 'v3', credentials=self.creds) + + now = dt.utcnow().isoformat() + 'Z' + result = service.events().list(calendarId='primary', timeMin=now, + maxResults=20, + singleEvents=True, + orderBy='startTime').execute() + + events = result.get('items', []) + + # 2019-11-05T10:00:00-08:00 + items = [] + for event in events: + start = event['start'].get('dateTime', event['start'].get('date')) + st_date = dt.strftime(dtparse(start), format='%m-%d-%Y') + st_time = dt.strftime(dtparse(start), format='%I:%M%p') + items.append({ + "date": st_date, + "time": st_time, + "content": event['summary'] + }) + + return items From cd474beaedccf0e0f6d4bc2b3a7693456030c39e Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:25:29 +0100 Subject: [PATCH 014/113] making PEP8 linter happy --- infowindow.py | 81 +++++++++--------- mod_infowindow/infowindow.py | 161 ++++++++++++++++++----------------- mod_todo/mod_google.py | 67 ++++++++------- mod_todo/mod_teamwork.py | 81 +++++++++--------- mod_utils/iw_utils.py | 42 ++++----- 5 files changed, 216 insertions(+), 216 deletions(-) diff --git a/infowindow.py b/infowindow.py index 725be4d..1e9e777 100755 --- a/infowindow.py +++ b/infowindow.py @@ -11,17 +11,17 @@ # CALENDAR: mod_google, mod_ical # WEATHER: mod_owm, mod_wunderground from mod_utils import iw_utils -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_todoist as modTodo # TODO +from mod_calendar import mod_google as modCalendar # CALENDAR +from mod_weather import mod_owm as modWeather # WEATHER # TODO: Create dictionaries for API args. so that they can be custom. # Configuration ############################################################### -with open(iw_utils.getCWD()+"/config.json") as config_file: +with open(iw_utils.getCWD() + "/config.json") as config_file: config_data = json.load(config_file) -## Rotation. 0 for desktop, 180 for hanging upside down +# Rotation. 0 for desktop, 180 for hanging upside down rotation = config_data["general"]["rotation"] todo_opts = config_data["todo"] calendar_opts = config_data["calendar"] @@ -34,6 +34,7 @@ logging.basicConfig(level=logging.DEBUG) logging.info("Configuration Complete") + # Custom exception handler. Need to handle exceptions and send them to the # display since this will run headless most of the time. This gives the user # enough info to know that they need to troubleshoot. @@ -42,15 +43,17 @@ def HandleException(et, val, tb): iw.text(0, 10, "EXCEPTION IN PROGRAM", 'robotoBlack18', 'black') iw.text(0, 30, str(val), 'robotoBlack18', 'black') iw.text(0, 60, "Please run program from command line interactivly to resolve", 'robotoBlack18', 'black') - print "EXCEPTION IN PROGRAM ==================================" - print val - print et - print tb - print "END EXCEPTION =========================================" + print("EXCEPTION IN PROGRAM ==================================") + print(val) + print(et) + print(tb) + print("END EXCEPTION =========================================") iw.display(rotation) + sys.excepthook = HandleException + # Main Program ################################################################ def main(): # Instantiate API modules @@ -58,42 +61,40 @@ def main(): cal = modCalendar.Cal(calendar_opts) weather = modWeather.Weather(weather_opts) - ## Setup e-ink initial drawings + # Setup e-ink initial drawings iw = infowindow.InfoWindow() - ### Weather Grid + # 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.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.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(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(464, 0, "rainSmall.bmp") # Rain Icon - iw.line(550, 0, 550, 64, 'black') # Fourth Vertical Line + iw.bitmap(554, 0, "snowSmall.bmp") # Snow Icon - iw.bitmap(554, 0, "snowSmall.bmp") # Snow Icon - # Center cal/todo divider line - iw.line(314, 90, 314, 384, 'black') # Left Black 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 - + iw.line(326, 90, 326, 384, 'black') # Right Black line # 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 + 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 # Todo / Weather Titles iw.text(440, 64, "TODO", 'robotoBlack24', 'white') iw.text(95, 64, "CALENDAR", 'robotoBlack24', 'white') - # DISPLAY TODO INFO # ========================================================================= todo_items = todo.list() @@ -104,7 +105,7 @@ def main(): iw.text(333, t_y, str(todo_item['content']), 'robotoRegular18', 'black') t_y = (t_y + 24) iw.line(325, (t_y - 2), 640, (t_y - 2), 'black') - logging.debug("ITEM: "+todo_item['content']) + logging.debug("ITEM: " + todo_item['content']) # DISPLAY CALENDAR INFO # ========================================================================= @@ -118,13 +119,14 @@ def main(): for cal_item in cal_items: (x, y) = iw.text(3, c_y, str(cal_item['date']), 'robotoRegular14', 'black') - iw.line((dt_x + 5), c_y, (dt_x + 5), (c_y +32), 'black') + iw.line((dt_x + 5), c_y, (dt_x + 5), (c_y + 32), 'black') iw.text(3, (c_y + 15), str(cal_item['time']), 'robotoRegular14', 'black') - iw.text((dt_x + 7), (c_y + 5), iw.truncate(str(cal_item['content']), 'robotoRegular18'), 'robotoRegular18', 'black') + iw.text((dt_x + 7), (c_y + 5), iw.truncate(str(cal_item['content']), 'robotoRegular18'), 'robotoRegular18', + 'black') c_y = (c_y + 32) iw.line(0, (c_y - 2), 313, (c_y - 2), 'black') # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) - logging.debug("ITEM: "+str(cal_item['content'])) + logging.debug("ITEM: " + str(cal_item['content'])) # DISPLAY WEATHER INFO # ========================================================================= @@ -147,29 +149,30 @@ def main(): iw.text(70, 2, weather['description'].title(), '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) + (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') + 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') + 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') + 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') + 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) + if __name__ == '__main__': main() diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index 26ff8cf..d3c865b 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -1,80 +1,81 @@ -from driver import epd7in5b -from PIL import Image -from PIL import ImageDraw -from PIL import ImageFont -import os, sys - -class InfoWindow(): - def __init__(self): - 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.initFonts() - - 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, str, font): - num_chars = len(str) - for char in str: - (np_x, np_y) = self.getFont(font).getsize(str) - if np_x >= 235: - str = str[:-1] - - if np_x <= 235: - return str - - return str - - def display(self, angle): - self.image = self.image.rotate(angle) - self.epd.display_frame(self.epd.get_frame_buffer(self.image)) +from driver import epd7in5b +from PIL import Image +from PIL import ImageDraw +from PIL import ImageFont +import os, sys + + +class InfoWindow: + def __init__(self): + 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.initFonts() + + 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, str, font): + num_chars = len(str) + for char in str: + (np_x, np_y) = self.getFont(font).getsize(str) + if np_x >= 235: + str = str[:-1] + + if np_x <= 235: + return str + + return str + + def display(self, angle): + self.image = self.image.rotate(angle) + self.epd.display_frame(self.epd.get_frame_buffer(self.image)) diff --git a/mod_todo/mod_google.py b/mod_todo/mod_google.py index 5390436..48f26a0 100644 --- a/mod_todo/mod_google.py +++ b/mod_todo/mod_google.py @@ -1,33 +1,34 @@ -from mod_utils import mod_google_auth -from googleapiclient.discovery import build -import logging - -logger = logging.getLogger(__name__) - - -class ToDo: - def __init__(self, api_key): - # This module authenticates from Google Auth API. We pull in the auth module - # wrapper to keep it clean. - logger.info("Initializing Module: ToDo: GOOGLE") - ga = mod_google_auth.GoogleAuth() - self.creds = ga.login() - - def list(self): - logging.info("Entering ToDo.list()") - service = build('tasks', 'v1', credentials=self.creds) - - # Fetch Results - results = service.tasks().list(tasklist='YVJWSXk4cXVhZk1aSGlmag').execute() - - items = [] - - # Loop through results and format them for ingest - for task in results['items']: - items.append({ - "content": task['title'], - "priority": task['position'] - }) - - # Return results to main program - return items \ No newline at end of file +from mod_utils import mod_google_auth +from googleapiclient.discovery import build +import logging + +logger = logging.getLogger(__name__) + + +class ToDo: + def __init__(self, api_key): + # This module authenticates from Google Auth API. We pull in the auth module + # wrapper to keep it clean. + logger.info("Initializing Module: ToDo: GOOGLE") + ga = mod_google_auth.GoogleAuth() + self.creds = ga.login() + + def list(self): + logging.info("Entering ToDo.list()") + service = build('tasks', 'v1', credentials=self.creds) + + # Fetch Results + # ToDo: is this a hard-coded tasklist ID? + results = service.tasks().list(tasklist='YVJWSXk4cXVhZk1aSGlmag').execute() + + items = [] + + # Loop through results and format them for ingest + for task in results['items']: + items.append({ + "content": task['title'], + "priority": task['position'] + }) + + # Return results to main program + return items diff --git a/mod_todo/mod_teamwork.py b/mod_todo/mod_teamwork.py index d2fadf2..dc41a92 100644 --- a/mod_todo/mod_teamwork.py +++ b/mod_todo/mod_teamwork.py @@ -1,43 +1,38 @@ -import urllib2, base64 -import json -import logging - -class ToDo: - def __init__(self, opts): - logging.debug("Todo API: TEAMWORK") - self.company = opts['site'] - self.key = opts['api_key'] - - def list(self): - action = "tasks.json?sort=priority" - request = urllib2.Request("https://{0}/{1}".format(self.company, action)) - request.add_header("Authorization", "BASIC " + base64.b64encode(self.key + ":xxx")) - - response = urllib2.urlopen(request) - data = json.loads(response.read()) - items = [] - - for task in data['todo-items']: - if task['priority'] == 'high': - priority = 1 - elif task['priority'] == 'medium': - priority = 2 - elif task['priority'] == 'low': - priority = 3 - elif task['priority'] == 'None': - priority = 4 - else: - priority = 8 - - items.append({ - "content": task['content'], - "priority": priority - }) - - return items - - - - - - +import urllib2, base64 +import json +import logging + + +class ToDo: + def __init__(self, opts): + logging.debug("Todo API: TEAMWORK") + self.company = opts['site'] + self.key = opts['api_key'] + + def list(self): + action = "tasks.json?sort=priority" + request = urllib2.Request("https://{0}/{1}".format(self.company, action)) + request.add_header("Authorization", "BASIC " + base64.b64encode(self.key + ":xxx")) + + response = urllib2.urlopen(request) + data = json.loads(response.read()) + items = [] + + for task in data['todo-items']: + if task['priority'] == 'high': + priority = 1 + elif task['priority'] == 'medium': + priority = 2 + elif task['priority'] == 'low': + priority = 3 + elif task['priority'] == 'None': + priority = 4 + else: + priority = 8 + + items.append({ + "content": task['content'], + "priority": priority + }) + + return items diff --git a/mod_utils/iw_utils.py b/mod_utils/iw_utils.py index f16ec94..6551943 100644 --- a/mod_utils/iw_utils.py +++ b/mod_utils/iw_utils.py @@ -1,21 +1,21 @@ -import os -import sys - -def isCron(): - if len(sys.argv) == 2: - if(sys.argv[1] == '--cron'): - return True - return False - -def getCWD(): - path = os.path.dirname(os.path.realpath(sys.argv[0])) - return path - -# Custom Error handler. This function will display the error message -# on the e-ink display and exit. -def HandleError(msg): - print "ERROR IN PROGRAM ======================================" - print "Program requires user input. Please run from console" - print "ERR: " + msg - print "END ERROR =============================================" - quit() \ No newline at end of file +import os +import sys + +def isCron(): + if len(sys.argv) == 2: + if(sys.argv[1] == '--cron'): + return True + return False + +def getCWD(): + path = os.path.dirname(os.path.realpath(sys.argv[0])) + return path + +# Custom Error handler. This function will display the error message +# on the e-ink display and exit. +def HandleError(msg): + print("ERROR IN PROGRAM ======================================") + print("Program requires user input. Please run from console") + print("ERR: " + msg) + print("END ERROR =============================================") + quit() From 7b030cd7aa3ee25e9a1d0a9415a46cfd6b7525e4 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:29:52 +0100 Subject: [PATCH 015/113] more text on error handling --- infowindow.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/infowindow.py b/infowindow.py index 1e9e777..fb07fbc 100755 --- a/infowindow.py +++ b/infowindow.py @@ -43,10 +43,12 @@ def HandleException(et, val, tb): iw.text(0, 10, "EXCEPTION IN PROGRAM", 'robotoBlack18', 'black') iw.text(0, 30, str(val), 'robotoBlack18', 'black') iw.text(0, 60, "Please run program from command line interactivly to resolve", 'robotoBlack18', 'black') - print("EXCEPTION IN PROGRAM ==================================") - print(val) - print(et) - print(tb) + print("EXCEPT3ON IN PROGRAM ==================================") + print("error message: ", val) + print("type: ", et) + print("traceback: ", tb) + print("filename: ", tb.filename) + print("line: ", tb.lineno) print("END EXCEPTION =========================================") iw.display(rotation) From b5166a0fac2889deb64879077883b235637aa3f9 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:32:59 +0100 Subject: [PATCH 016/113] format error output --- infowindow.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/infowindow.py b/infowindow.py index fb07fbc..1192aaa 100755 --- a/infowindow.py +++ b/infowindow.py @@ -43,12 +43,11 @@ def HandleException(et, val, tb): iw.text(0, 10, "EXCEPTION IN PROGRAM", 'robotoBlack18', 'black') iw.text(0, 30, str(val), 'robotoBlack18', 'black') iw.text(0, 60, "Please run program from command line interactivly to resolve", 'robotoBlack18', 'black') - print("EXCEPT3ON IN PROGRAM ==================================") - print("error message: ", val) - print("type: ", et) - print("traceback: ", tb) - print("filename: ", tb.filename) - print("line: ", tb.lineno) + 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) From c516cb4d08a585d82e19a1ad2199d1c8539f155e Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:35:43 +0100 Subject: [PATCH 017/113] fixing unicode errors --- infowindow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infowindow.py b/infowindow.py index 1192aaa..24c83ee 100755 --- a/infowindow.py +++ b/infowindow.py @@ -122,8 +122,8 @@ def main(): (x, y) = iw.text(3, c_y, str(cal_item['date']), 'robotoRegular14', 'black') iw.line((dt_x + 5), c_y, (dt_x + 5), (c_y + 32), 'black') iw.text(3, (c_y + 15), str(cal_item['time']), 'robotoRegular14', 'black') - iw.text((dt_x + 7), (c_y + 5), iw.truncate(str(cal_item['content']), 'robotoRegular18'), 'robotoRegular18', - 'black') + iw.text((dt_x + 7), (c_y + 5), iw.truncate(cal_item['content'].encode('utf-8').strip(), 'robotoRegular18'), + 'robotoRegular18', 'black') c_y = (c_y + 32) iw.line(0, (c_y - 2), 313, (c_y - 2), 'black') # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) From f7d4903c3a510608e05443817aac0f600c6d3dda Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 20:36:48 +0100 Subject: [PATCH 018/113] fixing unicode error in debug message --- infowindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infowindow.py b/infowindow.py index 24c83ee..d731275 100755 --- a/infowindow.py +++ b/infowindow.py @@ -127,7 +127,7 @@ def main(): c_y = (c_y + 32) iw.line(0, (c_y - 2), 313, (c_y - 2), 'black') # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) - logging.debug("ITEM: " + str(cal_item['content'])) + logging.debug("ITEM: %s" % str(cal_item['content'].encode('utf-8').strip())) # DISPLAY WEATHER INFO # ========================================================================= From b539482c89077b8ccb79107c21b24194f90214ab Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 21:59:02 +0100 Subject: [PATCH 019/113] implement "metric" time --- mod_weather/mod_owm.py | 215 +++++++++++++++++++++-------------------- 1 file changed, 109 insertions(+), 106 deletions(-) diff --git a/mod_weather/mod_owm.py b/mod_weather/mod_owm.py index 16f0ee3..9d25bf5 100644 --- a/mod_weather/mod_owm.py +++ b/mod_weather/mod_owm.py @@ -1,106 +1,109 @@ -import requests -from datetime import datetime as dt -import os -import json -import math -from PIL import Image -import logging - - -class Weather: - def __init__(self, options): - logging.debug("Weather API: Open Weather Map") - self.api_key = options['api_key'] - self.icon_path = "icons/" - self.city = options['city'] - self.units = options['units'] - - def pngToBmp(self, icon): - img = Image.open(self.icon_path+str(icon)) - r,g,b,a = img.split() - # img.merge("RGB", (r, g, b)) - basename = os.path.splitext(icon)[0] - img = img.convert('1') - img.save(self.icon_path+basename+".bmp") - return basename+".bmp" - - def getIcon(self, iconUrl): - # check for icon - bn = os.path.basename(iconUrl) - for root, dirs, files in os.walk(self.icon_path): - if not bn in files: - with open(self.icon_path+bn, "wb") as file: - response = requests.get(iconUrl) - file.write(response.content) - file.close() - - return self.pngToBmp(bn) - - def degreesToTextDesc(self, deg): - if deg > 337.5: return "N" - if deg > 292.5: return "NW" - if deg > 247.5: return "W" - if deg > 202.5: return "SW" - if deg > 157.5: return "S" - if deg > 122.5: return "SE" - if deg > 67.5: return "E" - if deg > 22.5: return "NE" - return "N" - - def list(self): - url = 'http://api.openweathermap.org/data/2.5/weather' - r = requests.get('{}?q={}&units={}&appid={}'.format(url, self.city, self.units, self.api_key)) - - data = r.json() - - # Sunrise and Sunset. - sunrise = dt.fromtimestamp(data['sys'].get('sunrise')).strftime('%I:%M%p') - sunset = dt.fromtimestamp(data['sys'].get('sunset')).strftime('%I:%M%p') - - # Rain and Snow - wTypes = ['rain', 'snow'] - for wType in wTypes: - # Check to see if dictionary has values for rain or snow. - # if it does NOT, set zero values for consistancy. - if data.has_key(wType): - setattr(self, wType, { - "1h": data[wType].get('1h'), - "3h": data[wType].get('3h') - }) - else: - setattr(self, wType, { - "1h": 0, - "3h": 0 - }) - - # Fetch Wind Data - wind = { - "dir": self.degreesToTextDesc(data['wind'].get('deg')), - "speed": int(round(data['wind'].get('speed'))) - #"speed": 33 - } - - #icon = self.getIcon("http://openweathermap.org/img/wn/"+data['weather'][0].get('icon')+".png") - icon = os.path.basename(data['weather'][0].get('icon'))+".bmp" - - return { - "description": data['weather'][0].get('description'), - "humidity": data['main'].get('humidity'), - "temp_cur": int(math.ceil(data['main'].get('temp'))), - #"temp_cur": int(9), - "temp_min": int(math.ceil(data['main'].get('temp_min'))), - "temp_max": int(math.ceil(data['main'].get('temp_max'))), - #"temp_min": int(100), - #"temp_max": int(112), - "sunrise": sunrise, - "sunset": sunset, - "rain": self.rain, - "snow": self.snow, - "wind": wind, - "icon": icon - } - - - - - \ No newline at end of file +import requests +from datetime import datetime as dt +import os +import json +import math +from PIL import Image +import logging + + +class Weather: + def __init__(self, options): + logging.debug("Weather API: Open Weather Map") + self.api_key = options['api_key'] + self.icon_path = "icons/" + self.city = options['city'] + self.units = options['units'] + + def pngToBmp(self, icon): + img = Image.open(self.icon_path+str(icon)) + r,g,b,a = img.split() + # img.merge("RGB", (r, g, b)) + basename = os.path.splitext(icon)[0] + img = img.convert('1') + img.save(self.icon_path+basename+".bmp") + return basename+".bmp" + + def getIcon(self, iconUrl): + # check for icon + bn = os.path.basename(iconUrl) + for root, dirs, files in os.walk(self.icon_path): + if not bn in files: + with open(self.icon_path+bn, "wb") as file: + response = requests.get(iconUrl) + file.write(response.content) + file.close() + + return self.pngToBmp(bn) + + def degreesToTextDesc(self, deg): + if deg > 337.5: return "N" + if deg > 292.5: return "NW" + if deg > 247.5: return "W" + if deg > 202.5: return "SW" + if deg > 157.5: return "S" + if deg > 122.5: return "SE" + if deg > 67.5: return "E" + if deg > 22.5: return "NE" + return "N" + + def list(self): + url = 'http://api.openweathermap.org/data/2.5/weather' + r = requests.get('{}?q={}&units={}&appid={}'.format(url, self.city, self.units, self.api_key)) + + data = r.json() + + # Sunrise and Sunset. + if self.units == "imperial": + sunrise = dt.fromtimestamp(data['sys'].get('sunrise')).strftime('%I:%M %p') + sunset = dt.fromtimestamp(data['sys'].get('sunset')).strftime('%I:%M %p') + else: + sunrise = dt.fromtimestamp(data['sys'].get('sunrise')).strftime('%H:%M') + sunset = dt.fromtimestamp(data['sys'].get('sunset')).strftime('%H:%M') + + # Rain and Snow + wTypes = ['rain', 'snow'] + for wType in wTypes: + # Check to see if dictionary has values for rain or snow. + # if it does NOT, set zero values for consistancy. + if data.has_key(wType): + setattr(self, wType, { + "1h": data[wType].get('1h'), + "3h": data[wType].get('3h') + }) + else: + setattr(self, wType, { + "1h": 0, + "3h": 0 + }) + + # Fetch Wind Data + wind = { + "dir": self.degreesToTextDesc(data['wind'].get('deg')), + "speed": int(round(data['wind'].get('speed'))) + #"speed": 33 + } + + #icon = self.getIcon("http://openweathermap.org/img/wn/"+data['weather'][0].get('icon')+".png") + icon = os.path.basename(data['weather'][0].get('icon'))+".bmp" + + return { + "description": data['weather'][0].get('description'), + "humidity": data['main'].get('humidity'), + "temp_cur": int(math.ceil(data['main'].get('temp'))), + #"temp_cur": int(9), + "temp_min": int(math.ceil(data['main'].get('temp_min'))), + "temp_max": int(math.ceil(data['main'].get('temp_max'))), + #"temp_min": int(100), + #"temp_max": int(112), + "sunrise": sunrise, + "sunset": sunset, + "rain": self.rain, + "snow": self.snow, + "wind": wind, + "icon": icon + } + + + + From 167d637e2f5bc475dff5451fb63a850e5d76a2c9 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 22:28:28 +0100 Subject: [PATCH 020/113] testing if tasklist is hardcoded --- mod_todo/mod_google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_todo/mod_google.py b/mod_todo/mod_google.py index 48f26a0..6c166d0 100644 --- a/mod_todo/mod_google.py +++ b/mod_todo/mod_google.py @@ -19,7 +19,7 @@ def list(self): # Fetch Results # ToDo: is this a hard-coded tasklist ID? - results = service.tasks().list(tasklist='YVJWSXk4cXVhZk1aSGlmag').execute() + results = service.tasks().list(tasklist='MDAzODYzMzE5OTA3MDc0NDM0MDI6MDow').execute() items = [] From 1b01cf4d51ad3faabcdc18f16af7e2c5feee63d3 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 22:38:15 +0100 Subject: [PATCH 021/113] removed hardcoded task list and display all tasks where "todo" is in the name --- mod_todo/mod_google.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/mod_todo/mod_google.py b/mod_todo/mod_google.py index 6c166d0..c50c300 100644 --- a/mod_todo/mod_google.py +++ b/mod_todo/mod_google.py @@ -17,18 +17,20 @@ def list(self): logging.info("Entering ToDo.list()") service = build('tasks', 'v1', credentials=self.creds) - # Fetch Results - # ToDo: is this a hard-coded tasklist ID? - results = service.tasks().list(tasklist='MDAzODYzMzE5OTA3MDc0NDM0MDI6MDow').execute() - items = [] - # Loop through results and format them for ingest - for task in results['items']: - items.append({ - "content": task['title'], - "priority": task['position'] - }) + # Fetch Results from all lists where todo is in the name + tasklists = service.tasklists().list().execute() + for tasklist in tasklists['items']: + if "todo" in tasklist['title'].lower(): + results = service.tasks().list(tasklist=tasklist['id']).execute() + + # Loop through results and format them for ingest + for task in results['items']: + items.append({ + "content": task['title'], + "priority": task['position'] + }) # Return results to main program return items From 734468a8d99a8542720d7f0256d3d8d3a404ef5c Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 22:50:15 +0100 Subject: [PATCH 022/113] use google todo module --- infowindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infowindow.py b/infowindow.py index d731275..318553d 100755 --- a/infowindow.py +++ b/infowindow.py @@ -11,7 +11,7 @@ # CALENDAR: mod_google, mod_ical # WEATHER: mod_owm, mod_wunderground from mod_utils import iw_utils -from mod_todo import mod_todoist as modTodo # TODO +from mod_todo import mod_google as modTodo # TODO from mod_calendar import mod_google as modCalendar # CALENDAR from mod_weather import mod_owm as modWeather # WEATHER From 23f9a6cf3a8c7dee0fbe5b9370af7addae072026 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 15 Dec 2019 23:12:52 +0100 Subject: [PATCH 023/113] only work if there are tasks in list --- mod_todo/mod_google.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mod_todo/mod_google.py b/mod_todo/mod_google.py index c50c300..88bef1d 100644 --- a/mod_todo/mod_google.py +++ b/mod_todo/mod_google.py @@ -26,11 +26,12 @@ def list(self): results = service.tasks().list(tasklist=tasklist['id']).execute() # Loop through results and format them for ingest - for task in results['items']: - items.append({ - "content": task['title'], - "priority": task['position'] - }) + if 'items' in results.keys(): + for task in results['items']: + items.append({ + "content": task['title'], + "priority": task['position'] + }) # Return results to main program return items From 3e0aea26a92990347ac27671b8851951d804c8d3 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 16 Dec 2019 20:37:25 +0100 Subject: [PATCH 024/113] working on utf-8 problems --- infowindow.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/infowindow.py b/infowindow.py index 318553d..4ca5eef 100755 --- a/infowindow.py +++ b/infowindow.py @@ -41,7 +41,7 @@ def HandleException(et, val, tb): iw = infowindow.InfoWindow() iw.text(0, 10, "EXCEPTION IN PROGRAM", 'robotoBlack18', 'black') - iw.text(0, 30, str(val), 'robotoBlack18', 'black') + iw.text(0, 30, val.encode('utf-8').strip(), 'robotoBlack18', 'black') iw.text(0, 60, "Please run program from command line interactivly to resolve", 'robotoBlack18', 'black') print("EXCEPTION IN PROGRAM ==================================") print("error message: %s" % val) @@ -103,10 +103,10 @@ def main(): logging.debug("-----------------------------------------------------------------------") t_y = 94 for todo_item in todo_items: - iw.text(333, t_y, str(todo_item['content']), 'robotoRegular18', 'black') + iw.text(333, t_y, todo_item['content'].encode('utf-8').strip(), 'robotoRegular18', 'black') t_y = (t_y + 24) iw.line(325, (t_y - 2), 640, (t_y - 2), 'black') - logging.debug("ITEM: " + todo_item['content']) + logging.debug("ITEM: %s" % todo_item['content'].encode('utf-8').strip()) # DISPLAY CALENDAR INFO # ========================================================================= @@ -119,15 +119,15 @@ def main(): (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('12-99-2000') for cal_item in cal_items: - (x, y) = iw.text(3, c_y, str(cal_item['date']), 'robotoRegular14', 'black') + (x, y) = iw.text(3, c_y, cal_item['date'].encode('utf-8').strip(), 'robotoRegular14', 'black') iw.line((dt_x + 5), c_y, (dt_x + 5), (c_y + 32), 'black') - iw.text(3, (c_y + 15), str(cal_item['time']), 'robotoRegular14', 'black') + iw.text(3, (c_y + 15), cal_item['time'].encode('utf-8').strip(), 'robotoRegular14', 'black') iw.text((dt_x + 7), (c_y + 5), iw.truncate(cal_item['content'].encode('utf-8').strip(), 'robotoRegular18'), 'robotoRegular18', 'black') c_y = (c_y + 32) iw.line(0, (c_y - 2), 313, (c_y - 2), 'black') # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) - logging.debug("ITEM: %s" % str(cal_item['content'].encode('utf-8').strip())) + logging.debug("ITEM: %s" % cal_item['content'].encode('utf-8').strip()) # DISPLAY WEATHER INFO # ========================================================================= @@ -136,39 +136,39 @@ def main(): logging.debug("-----------------------------------------------------------------------") # Set unit descriptors if weather_opts['units'] == 'imperial': - u_speed = "mph" - u_temp = "F" + u_speed = u"mph" + u_temp = u"F" elif weather_opts['units'] == 'metric': - u_speed = "m/sec" - u_temp = "C" + u_speed = u"m/sec" + u_temp = u"C" else: - u_speed = "m/sec" - u_temp = "K" + 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(), 'robotoBlack24', 'black') + iw.text(70, 2, weather['description'].title().encode('utf-8').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) + (t_x, t_y) = iw.getFont('robotoBlack48').getsize(weather['temp_cur'].encode('utf-8').strip() + deg_symbol) temp_left = (iw.width / 2) - (t_x / 2) - iw.text(temp_left, 2, str(weather['temp_cur']) + deg_symbol, 'robotoBlack48', 'white') + iw.text(temp_left, 2, weather['temp_cur'].encode('utf-8').strip() + 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') + iw.text(380, 35, weather['wind']['speed'].encode('utf-8').strip() + 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') + iw.text(481, 29, "1hr: " + weather['rain']['1h'].encode('utf-8').strip(), 'robotoRegular18', 'black') + iw.text(481, 44, "3hr: " + weather['rain']['3h'].encode('utf-8').strip(), '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') + iw.text(573, 29, "1hr: " + weather['snow']['1h'].encode('utf-8').strip(), 'robotoRegular18', 'black') + iw.text(573, 44, "3hr: " + weather['snow']['3h'].encode('utf-8').strip(), 'robotoRegular18', 'black') # Write to screen # ========================================================================= From 2062622e0c4996a632c3f781247bc5b024cd32e0 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 16 Dec 2019 20:39:56 +0100 Subject: [PATCH 025/113] working on utf-8 problems --- infowindow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infowindow.py b/infowindow.py index 4ca5eef..6301dc8 100755 --- a/infowindow.py +++ b/infowindow.py @@ -152,9 +152,9 @@ def main(): iw.text(154, 35, weather['sunset'], 'robotoRegular18', 'black') # Temp ( adjust for str length ) - (t_x, t_y) = iw.getFont('robotoBlack48').getsize(weather['temp_cur'].encode('utf-8').strip() + deg_symbol) + (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, weather['temp_cur'].encode('utf-8').strip() + deg_symbol, 'robotoBlack48', 'white') + 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') From b5718242906bb738b28c1ea7c50b7440ebaeff89 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 16 Dec 2019 20:41:28 +0100 Subject: [PATCH 026/113] unicoding --- infowindow.py | 2 +- mod_weather/mod_owm.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/infowindow.py b/infowindow.py index 6301dc8..c0a04bd 100755 --- a/infowindow.py +++ b/infowindow.py @@ -160,7 +160,7 @@ def main(): # Wind iw.text(405, 5, weather['wind']['dir'], 'robotoBlack18', 'black') - iw.text(380, 35, weather['wind']['speed'].encode('utf-8').strip() + u_speed, 'robotoRegular18', 'black') + iw.text(380, 35, str(weather['wind']['speed']) + u_speed, 'robotoRegular18', 'black') # Rain iw.text(481, 29, "1hr: " + weather['rain']['1h'].encode('utf-8').strip(), 'robotoRegular18', 'black') diff --git a/mod_weather/mod_owm.py b/mod_weather/mod_owm.py index 9d25bf5..0137a07 100644 --- a/mod_weather/mod_owm.py +++ b/mod_weather/mod_owm.py @@ -37,15 +37,15 @@ def getIcon(self, iconUrl): return self.pngToBmp(bn) def degreesToTextDesc(self, deg): - if deg > 337.5: return "N" - if deg > 292.5: return "NW" - if deg > 247.5: return "W" - if deg > 202.5: return "SW" - if deg > 157.5: return "S" - if deg > 122.5: return "SE" - if deg > 67.5: return "E" - if deg > 22.5: return "NE" - return "N" + if deg > 337.5: return u"N" + if deg > 292.5: return u"NW" + if deg > 247.5: return u"W" + if deg > 202.5: return u"SW" + if deg > 157.5: return u"S" + if deg > 122.5: return u"SE" + if deg > 67.5: return u"E" + if deg > 22.5: return u"NE" + return u"N" def list(self): url = 'http://api.openweathermap.org/data/2.5/weather' From c8eb1d9b0441745a68652ca9040b398c59f43910 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 16 Dec 2019 20:42:36 +0100 Subject: [PATCH 027/113] unicoding --- infowindow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/infowindow.py b/infowindow.py index c0a04bd..84ab7e7 100755 --- a/infowindow.py +++ b/infowindow.py @@ -163,12 +163,12 @@ def main(): iw.text(380, 35, str(weather['wind']['speed']) + u_speed, 'robotoRegular18', 'black') # Rain - iw.text(481, 29, "1hr: " + weather['rain']['1h'].encode('utf-8').strip(), 'robotoRegular18', 'black') - iw.text(481, 44, "3hr: " + weather['rain']['3h'].encode('utf-8').strip(), 'robotoRegular18', 'black') + 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: " + weather['snow']['1h'].encode('utf-8').strip(), 'robotoRegular18', 'black') - iw.text(573, 44, "3hr: " + weather['snow']['3h'].encode('utf-8').strip(), 'robotoRegular18', 'black') + 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 # ========================================================================= From d0c7d574edeee62cd3b57b2499918a038ce2ce85 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 16 Dec 2019 21:00:50 +0100 Subject: [PATCH 028/113] more internationalization --- config.json-sample | 30 ++++++++++++++++-------------- infowindow.py | 6 +++++- mod_calendar/mod_google.py | 14 +++++++++++--- mod_weather/mod_owm.py | 4 ++-- 4 files changed, 34 insertions(+), 20 deletions(-) diff --git a/config.json-sample b/config.json-sample index 48ba33c..f2e53d3 100644 --- a/config.json-sample +++ b/config.json-sample @@ -1,14 +1,16 @@ -{ - "general": { - "rotation": 180 - }, - "todo": { - "api_key": "1234" - }, - "calendar": null, - "weather": { - "api_key": "1234", - "city": "Sacramento,US", - "units": "imperial" - } -} \ No newline at end of file +{ + "general": { + "rotation": 180, + "timeformat": "12h" + }, + "todo": { + "api_key": "1234" + }, + "calendar": { + }, + "weather": { + "api_key": "1234", + "city": "Sacramento,US", + "units": "imperial" + } +} diff --git a/infowindow.py b/infowindow.py index 84ab7e7..712a89f 100755 --- a/infowindow.py +++ b/infowindow.py @@ -18,7 +18,8 @@ # TODO: Create dictionaries for API args. so that they can be custom. # Configuration ############################################################### -with open(iw_utils.getCWD() + "/config.json") as config_file: +config_path = os.path.join(iw_utils.getCWD(), "/config.json") +with open(config_path) as config_file: config_data = json.load(config_file) # Rotation. 0 for desktop, 180 for hanging upside down @@ -26,6 +27,9 @@ todo_opts = config_data["todo"] calendar_opts = config_data["calendar"] weather_opts = config_data["weather"] +# give the timeformat to all the modules needing it +calendar_opts["timeformat"] = config_data["general"]["timeformat"] +weather_opts["timeformat"] = config_data["general"]["timeformat"] # END CONFIGURATION ########################################################### ############################################################################### diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 162643c..14f8004 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -9,9 +9,10 @@ class Cal: - def __init__(self, api_key): + def __init__(self, options): ga = mod_google_auth.GoogleAuth() self.creds = ga.login() + self.timeformat = options["timeformat"] def list(self): service = build('calendar', 'v3', credentials=self.creds) @@ -28,8 +29,15 @@ def list(self): items = [] for event in events: start = event['start'].get('dateTime', event['start'].get('date')) - st_date = dt.strftime(dtparse(start), format='%m-%d-%Y') - st_time = dt.strftime(dtparse(start), format='%I:%M%p') + + # Sunrise and Sunset. + if self.timeformat == "12h": + st_date = dt.strftime(dtparse(start), format='%m-%d-%Y') + st_time = dt.strftime(dtparse(start), format='%I:%M%p') + else: + st_date = dt.strftime(dtparse(start), format='%d.%m.%Y') + st_time = dt.strftime(dtparse(start), format='%H:%M') + items.append({ "date": st_date, "time": st_time, diff --git a/mod_weather/mod_owm.py b/mod_weather/mod_owm.py index 0137a07..82352df 100644 --- a/mod_weather/mod_owm.py +++ b/mod_weather/mod_owm.py @@ -1,7 +1,6 @@ import requests from datetime import datetime as dt import os -import json import math from PIL import Image import logging @@ -14,6 +13,7 @@ def __init__(self, options): self.icon_path = "icons/" self.city = options['city'] self.units = options['units'] + self.timeformat = options['timeformat'] def pngToBmp(self, icon): img = Image.open(self.icon_path+str(icon)) @@ -54,7 +54,7 @@ def list(self): data = r.json() # Sunrise and Sunset. - if self.units == "imperial": + if self.timeformat == "12h": sunrise = dt.fromtimestamp(data['sys'].get('sunrise')).strftime('%I:%M %p') sunset = dt.fromtimestamp(data['sys'].get('sunset')).strftime('%I:%M %p') else: From ecbc34353f407f4f47febe0f49e498d79603d27c Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 16 Dec 2019 21:01:33 +0100 Subject: [PATCH 029/113] fixing import --- infowindow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infowindow.py b/infowindow.py index 712a89f..e6e22d1 100755 --- a/infowindow.py +++ b/infowindow.py @@ -1,6 +1,7 @@ #!/usr/bin/env python2 import sys +import os.path import json import logging from mod_infowindow import infowindow From a31b89865ead89f331ea781a8c2d0e0b652a5f28 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 16 Dec 2019 21:02:30 +0100 Subject: [PATCH 030/113] fixing import --- infowindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infowindow.py b/infowindow.py index e6e22d1..f87ee06 100755 --- a/infowindow.py +++ b/infowindow.py @@ -19,7 +19,7 @@ # TODO: Create dictionaries for API args. so that they can be custom. # Configuration ############################################################### -config_path = os.path.join(iw_utils.getCWD(), "/config.json") +config_path = os.path.join(iw_utils.getCWD(), "config.json") with open(config_path) as config_file: config_data = json.load(config_file) From adcfbebbc930adf07e22a90eddb95850d3cfe586 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 16 Dec 2019 22:05:02 +0100 Subject: [PATCH 031/113] working on character encodings/charsets --- config.json-sample | 3 ++- infowindow.py | 17 +++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/config.json-sample b/config.json-sample index f2e53d3..9344322 100644 --- a/config.json-sample +++ b/config.json-sample @@ -1,7 +1,8 @@ { "general": { "rotation": 180, - "timeformat": "12h" + "timeformat": "12h", + "charset": "latin1" }, "todo": { "api_key": "1234" diff --git a/infowindow.py b/infowindow.py index f87ee06..9d4bf90 100755 --- a/infowindow.py +++ b/infowindow.py @@ -25,6 +25,7 @@ # Rotation. 0 for desktop, 180 for hanging upside down rotation = config_data["general"]["rotation"] +charset = config_data["general"]["charset"] todo_opts = config_data["todo"] calendar_opts = config_data["calendar"] weather_opts = config_data["weather"] @@ -46,7 +47,7 @@ def HandleException(et, val, tb): iw = infowindow.InfoWindow() iw.text(0, 10, "EXCEPTION IN PROGRAM", 'robotoBlack18', 'black') - iw.text(0, 30, val.encode('utf-8').strip(), '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') print("EXCEPTION IN PROGRAM ==================================") print("error message: %s" % val) @@ -108,10 +109,10 @@ def main(): logging.debug("-----------------------------------------------------------------------") t_y = 94 for todo_item in todo_items: - iw.text(333, t_y, todo_item['content'].encode('utf-8').strip(), 'robotoRegular18', 'black') + iw.text(333, t_y, todo_item['content'].encode(charset).strip(), 'robotoRegular18', 'black') t_y = (t_y + 24) iw.line(325, (t_y - 2), 640, (t_y - 2), 'black') - logging.debug("ITEM: %s" % todo_item['content'].encode('utf-8').strip()) + logging.debug("ITEM: %s" % todo_item['content'].encode(charset).strip()) # DISPLAY CALENDAR INFO # ========================================================================= @@ -124,15 +125,15 @@ def main(): (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('12-99-2000') for cal_item in cal_items: - (x, y) = iw.text(3, c_y, cal_item['date'].encode('utf-8').strip(), 'robotoRegular14', 'black') + (x, y) = iw.text(3, c_y, cal_item['date'].encode(charset).strip(), 'robotoRegular14', 'black') iw.line((dt_x + 5), c_y, (dt_x + 5), (c_y + 32), 'black') - iw.text(3, (c_y + 15), cal_item['time'].encode('utf-8').strip(), 'robotoRegular14', 'black') - iw.text((dt_x + 7), (c_y + 5), iw.truncate(cal_item['content'].encode('utf-8').strip(), 'robotoRegular18'), + iw.text(3, (c_y + 15), cal_item['time'].encode(charset).strip(), 'robotoRegular14', 'black') + iw.text((dt_x + 7), (c_y + 5), iw.truncate(cal_item['content'].encode(charset).strip(), 'robotoRegular18'), 'robotoRegular18', 'black') c_y = (c_y + 32) iw.line(0, (c_y - 2), 313, (c_y - 2), 'black') # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) - logging.debug("ITEM: %s" % cal_item['content'].encode('utf-8').strip()) + logging.debug("ITEM: %s" % cal_item['content'].encode(charset).strip()) # DISPLAY WEATHER INFO # ========================================================================= @@ -152,7 +153,7 @@ def main(): deg_symbol = u"\u00b0" iw.bitmap(2, 2, weather['icon']) - iw.text(70, 2, weather['description'].title().encode('utf-8').strip(), 'robotoBlack24', 'black') + 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') From 9ffd9a713986b4e72dbab8713f6f05e25ad93ed0 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 16 Dec 2019 23:59:52 +0100 Subject: [PATCH 032/113] implementing multiple google calendar support --- config.json-sample | 1 + mod_calendar/mod_google.py | 43 +++++++++++++++++++++++++++++--------- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/config.json-sample b/config.json-sample index 9344322..940e66d 100644 --- a/config.json-sample +++ b/config.json-sample @@ -8,6 +8,7 @@ "api_key": "1234" }, "calendar": { + "additional": ["Work"] }, "weather": { "api_key": "1234", diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 14f8004..1403e7d 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -13,22 +13,45 @@ def __init__(self, options): ga = mod_google_auth.GoogleAuth() self.creds = ga.login() self.timeformat = options["timeformat"] + self.additional = options["additional"] def list(self): - service = build('calendar', 'v3', credentials=self.creds) + calendar_ids = [] + events = {} + items = [] + service = build('calendar', 'v3', credentials=self.creds) now = dt.utcnow().isoformat() + 'Z' - result = service.events().list(calendarId='primary', timeMin=now, - maxResults=20, - singleEvents=True, - orderBy='startTime').execute() - events = result.get('items', []) + page_token = None + 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 calendar_list_entry['primary']: + calendar_ids.append(calendar_list_entry['id']) + elif calendar_list_entry['summary'] == self.additional: + calendar_ids.append(calendar_list_entry['id']) + page_token = calendar_list.get('nextPageToken') + if not page_token: + break + + for id in calendar_ids: + result = service.events().list(calendarId=id, timeMin=now, + maxResults=10, + singleEvents=True, + orderBy='startTime').execute() + + for event in result.get('items', []): + start = event['start'].get('dateTime', event['start'].get('date')) + if start in events.keys(): + start = "%sa" % start + events[start] = event # 2019-11-05T10:00:00-08:00 - items = [] - for event in events: - start = event['start'].get('dateTime', event['start'].get('date')) + + for event_key in sorted(events.keys()): + start = events[event_key]['start'].get('dateTime', events[event_key]['start'].get('date')) # Sunrise and Sunset. if self.timeformat == "12h": @@ -41,7 +64,7 @@ def list(self): items.append({ "date": st_date, "time": st_time, - "content": event['summary'] + "content": events[event_key]['summary'] }) return items From 35a58ce646e2cbf2e831a74b602eef9c42c09a9b Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Tue, 17 Dec 2019 00:03:43 +0100 Subject: [PATCH 033/113] implementing multiple google calendar support --- mod_calendar/mod_google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 1403e7d..8cd8fd1 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -30,7 +30,7 @@ def list(self): if "primary" in calendar_list_entry.keys(): if calendar_list_entry['primary']: calendar_ids.append(calendar_list_entry['id']) - elif calendar_list_entry['summary'] == self.additional: + elif calendar_list_entry['summary'] in self.additional: calendar_ids.append(calendar_list_entry['id']) page_token = calendar_list.get('nextPageToken') if not page_token: From 19adf264910bd3b58a4c6de07c6566cf2d911ca2 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Tue, 17 Dec 2019 00:06:13 +0100 Subject: [PATCH 034/113] implementing multiple google calendar support --- mod_calendar/mod_google.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 8cd8fd1..327cf85 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -30,8 +30,8 @@ def list(self): if "primary" in calendar_list_entry.keys(): if calendar_list_entry['primary']: calendar_ids.append(calendar_list_entry['id']) - elif calendar_list_entry['summary'] in self.additional: - calendar_ids.append(calendar_list_entry['id']) + elif calendar_list_entry['summary'] in self.additional: + calendar_ids.append(calendar_list_entry['id']) page_token = calendar_list.get('nextPageToken') if not page_token: break From 2437af53c5922a8ad4e008765ec5c2955a781b91 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Tue, 17 Dec 2019 00:26:20 +0100 Subject: [PATCH 035/113] implementing ignored entries --- README.md | 8 ++++++++ config.json-sample | 3 ++- infowindow.py | 2 +- mod_calendar/mod_google.py | 5 ++++- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3ba9059..668fff4 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ config.json to add your api keys and other information. ### 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 @@ -64,6 +66,12 @@ In the google cloud console, do the following things: 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 diff --git a/config.json-sample b/config.json-sample index 940e66d..ac3b733 100644 --- a/config.json-sample +++ b/config.json-sample @@ -8,7 +8,8 @@ "api_key": "1234" }, "calendar": { - "additional": ["Work"] + "additional": ["Contacts"], + "ignored": ["Buy ticket!"] }, "weather": { "api_key": "1234", diff --git a/infowindow.py b/infowindow.py index 9d4bf90..1390e21 100755 --- a/infowindow.py +++ b/infowindow.py @@ -110,7 +110,7 @@ def main(): t_y = 94 for todo_item in todo_items: iw.text(333, t_y, todo_item['content'].encode(charset).strip(), 'robotoRegular18', 'black') - t_y = (t_y + 24) + t_y = (t_y + 34) iw.line(325, (t_y - 2), 640, (t_y - 2), 'black') logging.debug("ITEM: %s" % todo_item['content'].encode(charset).strip()) diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 327cf85..9de6c87 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -14,6 +14,7 @@ def __init__(self, options): self.creds = ga.login() self.timeformat = options["timeformat"] self.additional = options["additional"] + self.ignored = options["ignored"] def list(self): calendar_ids = [] @@ -38,11 +39,13 @@ def list(self): for id in calendar_ids: result = service.events().list(calendarId=id, timeMin=now, - maxResults=10, + maxResults=20, singleEvents=True, orderBy='startTime').execute() for event in result.get('items', []): + if event['summary'] in self.ignored: + continue start = event['start'].get('dateTime', event['start'].get('date')) if start in events.keys(): start = "%sa" % start From 255a81a66e71bc1e401d254612538cd1233273ae Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Tue, 17 Dec 2019 00:33:39 +0100 Subject: [PATCH 036/113] redo usless fix --- infowindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infowindow.py b/infowindow.py index 1390e21..9d4bf90 100755 --- a/infowindow.py +++ b/infowindow.py @@ -110,7 +110,7 @@ def main(): t_y = 94 for todo_item in todo_items: iw.text(333, t_y, todo_item['content'].encode(charset).strip(), 'robotoRegular18', 'black') - t_y = (t_y + 34) + t_y = (t_y + 24) iw.line(325, (t_y - 2), 640, (t_y - 2), 'black') logging.debug("ITEM: %s" % todo_item['content'].encode(charset).strip()) From c15073e1b688a0eb8bcb8b09d1c953216c975cd5 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Tue, 17 Dec 2019 08:05:40 +0100 Subject: [PATCH 037/113] Documenting SPI --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 668fff4..9f0296a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ to the data. In your face reminder. * 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` From 719c3a72c01fd047585d17858f1eac034b1365ec Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 19 Dec 2019 23:15:52 +0100 Subject: [PATCH 038/113] only updating the screen if new information is available --- mod_infowindow/infowindow.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index d3c865b..b4b2f3a 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -2,8 +2,10 @@ 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): @@ -14,6 +16,7 @@ def __init__(self): self.image = Image.new('L', (640, 384), 255) self.draw = ImageDraw.Draw(self.image) self.initFonts() + self.tmpImagePath = os.path.join(tempfile.gettempdir(), "InfoWindow.png") def getCWD(self): path = os.path.dirname(os.path.realpath(sys.argv[0])) @@ -78,4 +81,17 @@ def truncate(self, str, font): def display(self, angle): self.image = self.image.rotate(angle) - self.epd.display_frame(self.epd.get_frame_buffer(self.image)) + + 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)) + else: + logging.info("No new information found. Not updating the screen.") From d87435778d2ce351ab32d89af72b7e04510ff486 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 30 Jan 2020 22:44:18 +0100 Subject: [PATCH 039/113] adding screensaver script --- resources/black.png | Bin 0 -> 662 bytes resources/red.png | Bin 0 -> 1101 bytes resources/white.png | Bin 0 -> 1100 bytes screensaver.py | 25 +++++++++++++++++++++++++ 4 files changed, 25 insertions(+) create mode 100644 resources/black.png create mode 100644 resources/red.png create mode 100644 resources/white.png create mode 100644 screensaver.py diff --git a/resources/black.png b/resources/black.png new file mode 100644 index 0000000000000000000000000000000000000000..41e62e226a9dee366b3e4960696d1850b149d2eb GIT binary patch literal 662 zcmeAS@N?(olHy`uVBq!ia0y~yU}|7sU~J$33NT3c^&|l~Dw)pC0iMpz3I#>^X_+~x z3=A3*=T6w`btFKbb^n@0N>M+XS9oyUW|a=U`J?$@QsUWl30qF7MaK9fd2G_z*Oc&)Mp1{-IAM`SSrgPt-7 zGgd6MF9Qm)mw5WRvOi&z6O}g9TNEG+3|Cf97srr_TW=2c%lRHw5O||%Q~loCIAeDt(yP< literal 0 HcmV?d00001 diff --git a/resources/red.png b/resources/red.png new file mode 100644 index 0000000000000000000000000000000000000000..9298929c318bafaf5e25d5071b48ba6642ff8562 GIT binary patch literal 1101 zcmeAS@N?(olHy`uVBq!ia0y~yU}|7sU~J$33NT3c^&|l~Dw)pC0iMpz3I#>^X_+~x z3=A3*=T6w`btFKbb^n@0N>M+XS9oyUW|a=U`J?$@QsUWl30qF7MaK9fd2G_z*Oc&)Mp1{-IAM`SSrgPt-7 zGgd6MF9Qm)mw5WRvOi&z6P30;SH6=M7|?q>T^vIyZoR$aD9FITaoE7&{-4*5Y&Jri zd&{`q^{_|?FgGz8Dv(UEu`nk&7$}fV9cf5#;Nf5+k?LSP*dQUmLk2~L-=Kj(y5B%y zLb~6efkB4fKw-k{)j&@C0X32rf8_g-^X_+~x z3=A3*=T6w`btFKbb^n@0N>M+XS9oyUW|a=U`J?$@QsUWl30qF7MaK9fd2G_z*Oc&)Mp1{-IAM`SSrgPt-7 zGgd6MF9Qm)mw5WRvOi&z6O~qRuHW(l7|^>tT^vIyZoR$aD9FITaoFI%{^yF!a-A)4 z)~w}jOgtTo2OA^=NT!&Z7!4I91W2cn91IlLSeQwqjx;1V@Nlq^L6PA%Xkd`;H&B?6 z?l)*)kl{CAm~=3z9v~=3> BK^p)7 literal 0 HcmV?d00001 diff --git a/screensaver.py b/screensaver.py new file mode 100644 index 0000000..c4e5d12 --- /dev/null +++ b/screensaver.py @@ -0,0 +1,25 @@ +#!/usr/bin/env python2 + +import logging +import os +from driver import epd7in5b + +# Setup Logging - change to logging.DEBUG if you are having issues. +logging.basicConfig(level=logging.DEBUG) +logging.info("Screensager starting") + + +def main(): + epd = epd7in5b.EPD() + epd.init() + logging.info("Display red") + epd.display_frame(self.epd.get_frame_buffer(os.path.join("resources", "red.png"))) + logging.info("Display black") + epd.display_frame(self.epd.get_frame_buffer(os.path.join("resources", "black.png"))) + logging.info("Display white") + epd.display_frame(self.epd.get_frame_buffer(os.path.join("resources", "white.png"))) + logging.info("Screensaver finished") + + +if __name__ == '__main__': + main() From 2d03afb1a484465d688e51c99651a121feb65f26 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 30 Jan 2020 22:49:23 +0100 Subject: [PATCH 040/113] adding screensaver script --- README.md | 10 +++++++++- screensaver.py | 6 +++--- 2 files changed, 12 insertions(+), 4 deletions(-) mode change 100644 => 100755 screensaver.py diff --git a/README.md b/README.md index 9f0296a..c88bd40 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,15 @@ 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. +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. + ### General * rotation: 0 - This is the rotation of the display in degrees. Leave at zero if you use it as a desktop display. Change diff --git a/screensaver.py b/screensaver.py old mode 100644 new mode 100755 index c4e5d12..cec980b --- a/screensaver.py +++ b/screensaver.py @@ -13,11 +13,11 @@ def main(): epd = epd7in5b.EPD() epd.init() logging.info("Display red") - epd.display_frame(self.epd.get_frame_buffer(os.path.join("resources", "red.png"))) + epd.display_frame(epd.get_frame_buffer(os.path.join("resources", "red.png"))) logging.info("Display black") - epd.display_frame(self.epd.get_frame_buffer(os.path.join("resources", "black.png"))) + epd.display_frame(epd.get_frame_buffer(os.path.join("resources", "black.png"))) logging.info("Display white") - epd.display_frame(self.epd.get_frame_buffer(os.path.join("resources", "white.png"))) + epd.display_frame(epd.get_frame_buffer(os.path.join("resources", "white.png"))) logging.info("Screensaver finished") From 549385cbc2b5f0c624c0bcf1e1645c9c292b7fc6 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 30 Jan 2020 22:53:53 +0100 Subject: [PATCH 041/113] adding screensaver script --- screensaver.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/screensaver.py b/screensaver.py index cec980b..8af228d 100755 --- a/screensaver.py +++ b/screensaver.py @@ -3,22 +3,24 @@ import logging import os from driver import epd7in5b +from PIL import Image # Setup Logging - change to logging.DEBUG if you are having issues. logging.basicConfig(level=logging.DEBUG) -logging.info("Screensager starting") +logging.info("Screen saver starting") def main(): epd = epd7in5b.EPD() epd.init() - logging.info("Display red") - epd.display_frame(epd.get_frame_buffer(os.path.join("resources", "red.png"))) - logging.info("Display black") - epd.display_frame(epd.get_frame_buffer(os.path.join("resources", "black.png"))) - logging.info("Display white") - epd.display_frame(epd.get_frame_buffer(os.path.join("resources", "white.png"))) - logging.info("Screensaver finished") + + 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)) + + logging.info("Screen saver finished") if __name__ == '__main__': From f5eac4be72d5aa85fd4a4b6aca1b6eecf64b17f3 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 30 Jan 2020 22:56:35 +0100 Subject: [PATCH 042/113] disable debug output --- screensaver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/screensaver.py b/screensaver.py index 8af228d..ce58200 100755 --- a/screensaver.py +++ b/screensaver.py @@ -6,7 +6,7 @@ from PIL import Image # Setup Logging - change to logging.DEBUG if you are having issues. -logging.basicConfig(level=logging.DEBUG) +logging.basicConfig(level=logging.INFO) logging.info("Screen saver starting") From 01954026a803d7b26a1f1c73347972c46676f5e1 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 30 Jan 2020 23:02:04 +0100 Subject: [PATCH 043/113] documenting screen saver --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index c88bd40..2aa8e78 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,12 @@ If you want to increase the lifetime of the SD-Card, add the following line to ` 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 From 4eea813acba4f1b1c85a8b14a0ca3588184be592 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 30 Jan 2020 23:04:36 +0100 Subject: [PATCH 044/113] fixing documentation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2aa8e78..9d15504 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Always displaying the same colors at the same spots might have some negative eff 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` +* 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 From 04bc8bfd1f7491e70ac63039b20dab0c56c8a28e Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 30 Jan 2020 23:25:38 +0100 Subject: [PATCH 045/113] adding sleep after update to prevent e-ink damage --- mod_infowindow/infowindow.py | 1 + screensaver.py | 1 + 2 files changed, 2 insertions(+) diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index b4b2f3a..a81cc90 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -93,5 +93,6 @@ def display(self, angle): 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.") diff --git a/screensaver.py b/screensaver.py index ce58200..c1aad2b 100755 --- a/screensaver.py +++ b/screensaver.py @@ -20,6 +20,7 @@ def main(): image_data = Image.open(os.path.join("resources", image)) epd.display_frame(epd.get_frame_buffer(image_data)) + epd.sleep() logging.info("Screen saver finished") From dffc6f1d6febda6ddb97513c58e34b571b719251 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 30 Jan 2020 23:33:58 +0100 Subject: [PATCH 046/113] also adding clear() on screen saver --- screensaver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/screensaver.py b/screensaver.py index c1aad2b..f5d2be3 100755 --- a/screensaver.py +++ b/screensaver.py @@ -13,6 +13,7 @@ def main(): epd = epd7in5b.EPD() epd.init() + epd.clear() images = ["red.png", "black.png", "white.png"] for image in images: From d41e2a5fdd22a57368034790370442cf8009a8d1 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 30 Jan 2020 23:35:06 +0100 Subject: [PATCH 047/113] removing clear, since it is not available in this driver --- screensaver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/screensaver.py b/screensaver.py index f5d2be3..c1aad2b 100755 --- a/screensaver.py +++ b/screensaver.py @@ -13,7 +13,6 @@ def main(): epd = epd7in5b.EPD() epd.init() - epd.clear() images = ["red.png", "black.png", "white.png"] for image in images: From 50bd9e5cf1cfe967efdc6ec179b58fe1e3601743 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 22 May 2020 17:06:38 +0200 Subject: [PATCH 048/113] marking today for calendar events and using more space for text --- .gitignore | 1 + infowindow.py | 2 +- mod_calendar/mod_google.py | 18 +++++++++++++++--- mod_infowindow/infowindow.py | 4 ++-- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 91f98f7..fc842ce 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ icons/*.png test.py config.json infowindow.jpg +venv/ diff --git a/infowindow.py b/infowindow.py index 9d4bf90..0e1d370 100755 --- a/infowindow.py +++ b/infowindow.py @@ -122,7 +122,7 @@ def main(): c_y = 94 # Time and date divider line - (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('12-99-2000') + (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('55.55pm') for cal_item in cal_items: (x, y) = iw.text(3, c_y, cal_item['date'].encode(charset).strip(), 'robotoRegular14', 'black') diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 9de6c87..45f9a1c 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -56,13 +56,25 @@ def list(self): for event_key in sorted(events.keys()): start = events[event_key]['start'].get('dateTime', events[event_key]['start'].get('date')) + if start == dt.strftime(dt.now(), format='%Y-%m-%d'): + today = True + else: + today = False + # Sunrise and Sunset. if self.timeformat == "12h": - st_date = dt.strftime(dtparse(start), format='%m-%d-%Y') + if today: + st_date = dt.strftime(dtparse(start), format='%m-%d <') + else: + st_date = dt.strftime(dtparse(start), format='%m-%d') st_time = dt.strftime(dtparse(start), format='%I:%M%p') else: - st_date = dt.strftime(dtparse(start), format='%d.%m.%Y') - st_time = dt.strftime(dtparse(start), format='%H:%M') + if today: + st_date = dt.strftime(dtparse(start), format='%d.%m <') + st_time = dt.strftime(dtparse(start), format='%H:%M <') + else: + st_date = dt.strftime(dtparse(start), format='%d.%m') + st_time = dt.strftime(dtparse(start), format='%H:%M') items.append({ "date": st_date, diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index a81cc90..a457e25 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -71,10 +71,10 @@ def truncate(self, str, font): num_chars = len(str) for char in str: (np_x, np_y) = self.getFont(font).getsize(str) - if np_x >= 235: + if np_x >= 255: str = str[:-1] - if np_x <= 235: + if np_x <= 255: return str return str From 62ccec2398b41d0025ac2b88e7609c351f862712 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sat, 23 May 2020 12:20:15 +0200 Subject: [PATCH 049/113] * more space for event text due to the use of colors to mark today events* today colors events are now configurable * more pep8 compliant --- config.json-sample | 4 +++- infowindow.py | 20 +++++++++++++++----- mod_calendar/mod_google.py | 19 ++++++------------- mod_infowindow/infowindow.py | 31 +++++++++++++++++++------------ 4 files changed, 43 insertions(+), 31 deletions(-) diff --git a/config.json-sample b/config.json-sample index ac3b733..ef30e9b 100644 --- a/config.json-sample +++ b/config.json-sample @@ -9,7 +9,9 @@ }, "calendar": { "additional": ["Contacts"], - "ignored": ["Buy ticket!"] + "ignored": ["Buy ticket!"], + "today_text_color": "red", + "today_background_color": "white" }, "weather": { "api_key": "1234", diff --git a/infowindow.py b/infowindow.py index 0e1d370..1942ea4 100755 --- a/infowindow.py +++ b/infowindow.py @@ -29,9 +29,11 @@ todo_opts = config_data["todo"] 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"] # END CONFIGURATION ########################################################### ############################################################################### @@ -69,7 +71,7 @@ def main(): weather = modWeather.Weather(weather_opts) # Setup e-ink initial drawings - iw = infowindow.InfoWindow() + iw = infowindow.InfoWindow(infowindow_opts) # Weather Grid temp_rect_width = 102 @@ -122,14 +124,22 @@ def main(): c_y = 94 # Time and date divider line - (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('55.55pm') + if calendar_opts['timeformat'] == "12h": + (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('55.55pm') + else: + (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('55.55') for cal_item in cal_items: - (x, y) = iw.text(3, c_y, cal_item['date'].encode(charset).strip(), 'robotoRegular14', 'black') + font_color = 'black' + if cal_item['today']: + font_color = calendar_opts['today_text_color'] + iw.rectangle(0, (c_y - 2), 313, (c_y + 30), calendar_opts['today_background_color']) + + (x, y) = iw.text(3, c_y, cal_item['date'].encode(charset).strip(), 'robotoRegular14', font_color) iw.line((dt_x + 5), c_y, (dt_x + 5), (c_y + 32), 'black') - iw.text(3, (c_y + 15), cal_item['time'].encode(charset).strip(), 'robotoRegular14', 'black') + iw.text(3, (c_y + 15), cal_item['time'].encode(charset).strip(), 'robotoRegular14', font_color) iw.text((dt_x + 7), (c_y + 5), iw.truncate(cal_item['content'].encode(charset).strip(), 'robotoRegular18'), - 'robotoRegular18', 'black') + 'robotoRegular18', font_color) c_y = (c_y + 32) iw.line(0, (c_y - 2), 313, (c_y - 2), 'black') # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 45f9a1c..87d5ebd 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -55,31 +55,24 @@ def list(self): for event_key in sorted(events.keys()): start = events[event_key]['start'].get('dateTime', events[event_key]['start'].get('date')) - - if start == dt.strftime(dt.now(), format='%Y-%m-%d'): + if dt.strftime(dtparse(start), format='%Y%m%d') == dt.strftime(dt.today(), format='%Y%m%d'): today = True else: today = False # Sunrise and Sunset. if self.timeformat == "12h": - if today: - st_date = dt.strftime(dtparse(start), format='%m-%d <') - else: - st_date = dt.strftime(dtparse(start), format='%m-%d') + st_date = dt.strftime(dtparse(start), format='%m-%d') st_time = dt.strftime(dtparse(start), format='%I:%M%p') else: - if today: - st_date = dt.strftime(dtparse(start), format='%d.%m <') - st_time = dt.strftime(dtparse(start), format='%H:%M <') - else: - st_date = dt.strftime(dtparse(start), format='%d.%m') - st_time = dt.strftime(dtparse(start), format='%H:%M') + st_date = dt.strftime(dtparse(start), format='%d.%m') + st_time = dt.strftime(dtparse(start), format='%H:%M') items.append({ "date": st_date, "time": st_time, - "content": events[event_key]['summary'] + "content": events[event_key]['summary'], + "today": today }) return items diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index a457e25..8ff7a19 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -7,16 +7,23 @@ import logging import tempfile + class InfoWindow: - def __init__(self): + 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'] + if self.timeformat == "12h": + self.calendar_text_length = 253 + else: + self.calendar_text_length = 270 def getCWD(self): path = os.path.dirname(os.path.realpath(sys.argv[0])) @@ -35,11 +42,11 @@ 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) + 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) + self.draw.text((left, top), text, font=font, fill=fill) return self.draw.textsize(text, font=font) def rotate(self, angle): @@ -67,17 +74,17 @@ def initFonts(self): 'robotoBlack48': ImageFont.truetype(roboto+"Black.ttf", 48) } - def truncate(self, str, font): - num_chars = len(str) - for char in str: - (np_x, np_y) = self.getFont(font).getsize(str) - if np_x >= 255: - str = str[:-1] + def truncate(self, string, font): + num_chars = len(string) + for char in string: + (np_x, np_y) = self.getFont(font).getsize(string) + if np_x >= self.calendar_text_length: + string = string[:-1] - if np_x <= 255: - return str + if np_x <= self.calendar_text_length: + return string - return str + return string def display(self, angle): self.image = self.image.rotate(angle) From 249847fcb8d52200ead83a89622ea181ac4100c7 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 25 May 2020 16:07:56 +0200 Subject: [PATCH 050/113] * dynamically calculating calendar event text length * way less hardcoded drawing --- config.json-sample | 3 +- infowindow.py | 74 +++++++++++++++++++++++++++--------- mod_infowindow/infowindow.py | 14 +++---- 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/config.json-sample b/config.json-sample index ef30e9b..cb57612 100644 --- a/config.json-sample +++ b/config.json-sample @@ -2,7 +2,8 @@ "general": { "rotation": 180, "timeformat": "12h", - "charset": "latin1" + "charset": "latin1", + "cell_spacing": 2 }, "todo": { "api_key": "1234" diff --git a/infowindow.py b/infowindow.py index 1942ea4..1f2dba0 100755 --- a/infowindow.py +++ b/infowindow.py @@ -34,6 +34,7 @@ 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"] # END CONFIGURATION ########################################################### ############################################################################### @@ -109,11 +110,19 @@ def main(): todo_items = todo.list() logging.debug("Todo Items") logging.debug("-----------------------------------------------------------------------") - t_y = 94 + + tasks_font = "robotoRegular18" + (t_x, t_y) = iw.getFont(tasks_font).getsize('JgGj') + line_height = t_y + (2 * infowindow_opts["cell_spacing"]) + + current_task_y = 92 for todo_item in todo_items: - iw.text(333, t_y, todo_item['content'].encode(charset).strip(), 'robotoRegular18', 'black') - t_y = (t_y + 24) - iw.line(325, (t_y - 2), 640, (t_y - 2), 'black') + 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') + + # 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 @@ -121,27 +130,56 @@ def main(): cal_items = cal.list() logging.debug("Calendar Items") logging.debug("-----------------------------------------------------------------------") - c_y = 94 - # Time and date divider line + calendar_date_font = "robotoRegular14" + calendar_entry_font = "robotoRegular18" + + # todo: device mechanism to detect max text width/height programmatically (looping over all chars possible) if calendar_opts['timeformat'] == "12h": - (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('55.55pm') + (dt_x, dt_y) = iw.getFont(calendar_date_font).getsize('55.55pm') else: - (dt_x, dt_y) = iw.getFont('robotoRegular14').getsize('55.55') + (dt_x, dt_y) = iw.getFont(calendar_date_font).getsize('55.55') + + (it_x, it_y) = iw.getFont(calendar_entry_font).getsize('JgGj') + + line_height = (2 * dt_y) + (2 * infowindow_opts["cell_spacing"]) + current_calendar_y = 92 for cal_item in cal_items: font_color = 'black' if cal_item['today']: font_color = calendar_opts['today_text_color'] - iw.rectangle(0, (c_y - 2), 313, (c_y + 30), calendar_opts['today_background_color']) - - (x, y) = iw.text(3, c_y, cal_item['date'].encode(charset).strip(), 'robotoRegular14', font_color) - iw.line((dt_x + 5), c_y, (dt_x + 5), (c_y + 32), 'black') - iw.text(3, (c_y + 15), cal_item['time'].encode(charset).strip(), 'robotoRegular14', font_color) - iw.text((dt_x + 7), (c_y + 5), iw.truncate(cal_item['content'].encode(charset).strip(), 'robotoRegular18'), - 'robotoRegular18', font_color) - c_y = (c_y + 32) - iw.line(0, (c_y - 2), 313, (c_y - 2), 'black') + iw.rectangle(0, current_calendar_y, + 313, (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), + 'black') + # draw vertical line + iw.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"]), + (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"]), + (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, + (current_calendar_y + ((line_height - it_y) / 2)), + iw.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 + current_calendar_y = (current_calendar_y + line_height + 2) # 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()) @@ -174,7 +212,7 @@ def main(): t_desc_posx = (temp_left + t_x) - 15 iw.text(t_desc_posx, 25, u_temp, 'robotoBlack18', 'white') - # Wind + # Wind iw.text(405, 5, weather['wind']['dir'], 'robotoBlack18', 'black') iw.text(380, 35, str(weather['wind']['speed']) + u_speed, 'robotoRegular18', 'black') diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index 8ff7a19..dab37e8 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -20,14 +20,10 @@ def __init__(self, options): self.initFonts() self.tmpImagePath = os.path.join(tempfile.gettempdir(), "InfoWindow.png") self.timeformat = options['timeformat'] - if self.timeformat == "12h": - self.calendar_text_length = 253 - else: - self.calendar_text_length = 270 def getCWD(self): path = os.path.dirname(os.path.realpath(sys.argv[0])) - return path + return path def getImage(self): return self.image @@ -43,7 +39,7 @@ def line(self, left_1, top_1, left_2, top_2, fill, width=1): 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) @@ -74,14 +70,14 @@ def initFonts(self): 'robotoBlack48': ImageFont.truetype(roboto+"Black.ttf", 48) } - def truncate(self, string, font): + 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 >= self.calendar_text_length: + if np_x >= max_size: string = string[:-1] - if np_x <= self.calendar_text_length: + if np_x <= max_size: return string return string From 47de99ec944bc4907ba0af8d8190a8bb4d698911 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 25 May 2020 16:26:57 +0200 Subject: [PATCH 051/113] calculating max char width more intelligently --- infowindow.py | 40 +++++++++++++++++++++++++++++--------- mod_calendar/mod_google.py | 2 +- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/infowindow.py b/infowindow.py index 1f2dba0..89098bd 100755 --- a/infowindow.py +++ b/infowindow.py @@ -4,6 +4,7 @@ import os.path import json import logging +import string from mod_infowindow import infowindow # Select pluggable module for todo list, calendar and weather. @@ -64,6 +65,19 @@ def HandleException(et, val, tb): sys.excepthook = HandleException +# helper to calculate max char width and height +def get_max_char_size(iw, chars, font): + max_x = 0 + max_y = 0 + for char in chars: + (x, y) = iw.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 @@ -74,6 +88,11 @@ def main(): # Setup e-ink initial drawings iw = infowindow.InfoWindow(infowindow_opts) + # 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) @@ -111,8 +130,8 @@ def main(): logging.debug("Todo Items") logging.debug("-----------------------------------------------------------------------") - tasks_font = "robotoRegular18" - (t_x, t_y) = iw.getFont(tasks_font).getsize('JgGj') + #(t_x, t_y) = iw.getFont(tasks_font).getsize('JgGj') + (t_x, t_y) = get_max_char_size(iw, string.printable, tasks_font) line_height = t_y + (2 * infowindow_opts["cell_spacing"]) current_task_y = 92 @@ -131,16 +150,19 @@ def main(): logging.debug("Calendar Items") logging.debug("-----------------------------------------------------------------------") - calendar_date_font = "robotoRegular14" - calendar_entry_font = "robotoRegular18" - - # todo: device mechanism to detect max text width/height programmatically (looping over all chars possible) if calendar_opts['timeformat'] == "12h": - (dt_x, dt_y) = iw.getFont(calendar_date_font).getsize('55.55pm') + (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') + dt_x = dt_x + (4 * t_x) + if t_y > dt_y: + dt_y = t_y + else: - (dt_x, dt_y) = iw.getFont(calendar_date_font).getsize('55.55') + (t_x, t_y) = get_max_char_size(iw, string.digits, calendar_date_font) + (dt_x, dt_y) = iw.getFont(calendar_date_font).getsize('.') + dt_x = dt_x + (4 * t_x) - (it_x, it_y) = iw.getFont(calendar_entry_font).getsize('JgGj') + (it_x, it_y) = get_max_char_size(iw, string.printable, calendar_entry_font) line_height = (2 * dt_y) + (2 * infowindow_opts["cell_spacing"]) diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 87d5ebd..b42cdbd 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -63,7 +63,7 @@ def list(self): # Sunrise and Sunset. if self.timeformat == "12h": st_date = dt.strftime(dtparse(start), format='%m-%d') - st_time = dt.strftime(dtparse(start), format='%I:%M%p') + st_time = dt.strftime(dtparse(start), format='%I:%M %p') else: st_date = dt.strftime(dtparse(start), format='%d.%m') st_time = dt.strftime(dtparse(start), format='%H:%M') From d35e9e0c6e5ff643f71cfd8fa44f5c1b8afe188c Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Tue, 2 Jun 2020 18:22:25 +0200 Subject: [PATCH 052/113] * supporting more than two events with the same start date --- mod_calendar/mod_google.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index b42cdbd..37efa2a 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -46,9 +46,13 @@ def list(self): for event in result.get('items', []): if event['summary'] in self.ignored: continue - start = event['start'].get('dateTime', event['start'].get('date')) - if start in events.keys(): - start = "%sa" % start + initial_start = event['start'].get('dateTime', event['start'].get('date')) + start = "%s-0" % initial_start + counter = 0 + while start in events.keys(): + counter += 1 + start = "%s-%s" % (initial_start, counter) + events[start] = event # 2019-11-05T10:00:00-08:00 From 22a651c697d4c4ac03ac817f3470a324edb4f66c Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Tue, 2 Jun 2020 18:43:14 +0200 Subject: [PATCH 053/113] * events started in the past are now also "active" --- mod_calendar/mod_google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 37efa2a..563963a 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -59,7 +59,7 @@ def list(self): for event_key in sorted(events.keys()): start = events[event_key]['start'].get('dateTime', events[event_key]['start'].get('date')) - if dt.strftime(dtparse(start), format='%Y%m%d') == dt.strftime(dt.today(), format='%Y%m%d'): + if int(dt.strftime(dtparse(start), format='%Y%m%d')) <= int(dt.strftime(dt.today(), format='%Y%m%d')): today = True else: today = False From a7189dd8c3218b98378967390e423ca30f827a5a Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sat, 31 Oct 2020 14:47:48 +0100 Subject: [PATCH 054/113] Including birthdays calendar Google moved all birthdays to their own calendar. So you have to add this calendar if you want to show birthdays. I had to use "Geburtstage" in German, since my calendar somehow is called like this. --- config.json-sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json-sample b/config.json-sample index cb57612..9362e48 100644 --- a/config.json-sample +++ b/config.json-sample @@ -9,7 +9,7 @@ "api_key": "1234" }, "calendar": { - "additional": ["Contacts"], + "additional": ["Contacts", "Birthdays"], "ignored": ["Buy ticket!"], "today_text_color": "red", "today_background_color": "white" From 36defd47c6b4dda11b5b293ff466918c3ef0bcd1 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 2 Jul 2021 03:45:36 +0200 Subject: [PATCH 055/113] * events today are now red --- infowindow.py | 7 ++++++- mod_todo/mod_google.py | 12 +++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/infowindow.py b/infowindow.py index 89098bd..351c8d7 100755 --- a/infowindow.py +++ b/infowindow.py @@ -136,8 +136,13 @@ def main(): current_task_y = 92 for todo_item in todo_items: + color = 'black' + if 'today' in todo_item.keys(): + if todo_item['today']: + color = 'red' + iw.text(333, (current_task_y + infowindow_opts["cell_spacing"]), todo_item['content'].encode(charset).strip(), - tasks_font, 'black') + tasks_font, color) iw.line(327, (current_task_y + line_height + 1), 640, (current_task_y + line_height + 1), 'black') # set next loop height diff --git a/mod_todo/mod_google.py b/mod_todo/mod_google.py index 88bef1d..0e79642 100644 --- a/mod_todo/mod_google.py +++ b/mod_todo/mod_google.py @@ -1,7 +1,10 @@ from mod_utils import mod_google_auth from googleapiclient.discovery import build +from datetime import datetime import logging +now = datetime.now() + logger = logging.getLogger(__name__) @@ -28,9 +31,16 @@ def list(self): # Loop through results and format them for ingest if 'items' in results.keys(): for task in results['items']: + + today = False + if 'due' in task.keys(): + if task['due'].startswith(now.strftime("%Y-%m-%d")): + today = True + items.append({ "content": task['title'], - "priority": task['position'] + "priority": task['position'], + "today": today }) # Return results to main program From b22c1587842c7f1b8194a7c2723aafb8eeb6c185 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Dec 2021 03:12:18 +0100 Subject: [PATCH 056/113] converting to python3 --- .idea/.gitignore | 8 +++ README.md | 15 ++++-- config.json-sample | 1 - infowindow.py | 43 ++++++++-------- mod_calendar/mod_google.py | 4 +- mod_todo/mod_google.py | 4 +- mod_todo/mod_teamwork.py | 6 +-- mod_utils/iw_utils.py | 2 +- mod_weather/mod_owm.py | 20 ++++---- requirements.txt | 100 +++---------------------------------- screensaver.py | 5 +- 11 files changed, 68 insertions(+), 140 deletions(-) create mode 100644 .idea/.gitignore diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/README.md b/README.md index 9d15504..7c33db7 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,15 @@ Clone this repo onto your raspberry pi. Does not really matter where it is, but 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 +Run the following commands to install the requirements. I stuck to basic standard modules for ease of installation. +``` +cd /home/pi/InfoWindow +export CFLAGS=-fcommon +sudo apt install python3-dev +python3 -m venv venv +pip install -r requirements.txt +``` ## Configuration You will need to configure a few things such as API Keys and location. Copy config.json-sample to config.json. Edit @@ -53,13 +60,12 @@ Always displaying the same colors at the same spots might have some negative eff 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` +* insert `1 5 * * * /home/pi/InfoWindow/venv/bin/python3 /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 @@ -96,5 +102,4 @@ completed you will want to add this to CRON so it runs every few minutes automat ### Cron Run (Normal use) * Run `crontab -e` -* insert `*/6 * * * * /usr/bin/python /home/pi/InfoWindow/infowindow.py --cron` - +* insert `*/6 * * * * /home/pi/InfoWindow/venv/bin/python3 /home/pi/InfoWindow/infowindow.py --cron > /dev/null 2>&1` diff --git a/config.json-sample b/config.json-sample index 9362e48..7742349 100644 --- a/config.json-sample +++ b/config.json-sample @@ -2,7 +2,6 @@ "general": { "rotation": 180, "timeformat": "12h", - "charset": "latin1", "cell_spacing": 2 }, "todo": { diff --git a/infowindow.py b/infowindow.py index 351c8d7..258081d 100755 --- a/infowindow.py +++ b/infowindow.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import sys import os.path @@ -26,7 +26,6 @@ # Rotation. 0 for desktop, 180 for hanging upside down rotation = config_data["general"]["rotation"] -charset = config_data["general"]["charset"] todo_opts = config_data["todo"] calendar_opts = config_data["calendar"] weather_opts = config_data["weather"] @@ -51,13 +50,13 @@ 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, 30, val.strip(), 'robotoBlack18', 'black') iw.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(("error message: %s" % val)) + print(("type: %s" % et)) + print(("traceback: %s" % tb)) + print(("line: %s" % tb.lineno)) print("END EXCEPTION =========================================") iw.display(rotation) @@ -137,17 +136,17 @@ def main(): current_task_y = 92 for todo_item in todo_items: color = 'black' - if 'today' in todo_item.keys(): + if 'today' in list(todo_item.keys()): if todo_item['today']: color = 'red' - iw.text(333, (current_task_y + infowindow_opts["cell_spacing"]), todo_item['content'].encode(charset).strip(), + iw.text(333, (current_task_y + infowindow_opts["cell_spacing"]), todo_item['content'].strip(), tasks_font, color) iw.line(327, (current_task_y + line_height + 1), 640, (current_task_y + line_height + 1), 'black') # set next loop height current_task_y = (current_task_y + line_height + 2) - logging.debug("ITEM: %s" % todo_item['content'].encode(charset).strip()) + logging.debug("ITEM: %s" % todo_item['content'].strip()) # DISPLAY CALENDAR INFO # ========================================================================= @@ -192,23 +191,23 @@ def main(): # draw event date iw.text((infowindow_opts["cell_spacing"]), (current_calendar_y + infowindow_opts["cell_spacing"]), - cal_item['date'].encode(charset).strip(), calendar_date_font, font_color) + cal_item['date'].strip(), calendar_date_font, font_color) # draw event time iw.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) + cal_item['time'].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, (current_calendar_y + ((line_height - it_y) / 2)), - iw.truncate(cal_item['content'].encode(charset).strip(), calendar_entry_font, max_event_text_length), + iw.truncate(cal_item['content'].strip(), calendar_entry_font, max_event_text_length), calendar_entry_font, font_color) # set new line height for next round current_calendar_y = (current_calendar_y + line_height + 2) # 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()) + logging.debug("ITEM: %s" % cal_item['content'].strip()) # DISPLAY WEATHER INFO # ========================================================================= @@ -217,18 +216,18 @@ def main(): logging.debug("-----------------------------------------------------------------------") # Set unit descriptors if weather_opts['units'] == 'imperial': - u_speed = u"mph" - u_temp = u"F" + u_speed = "mph" + u_temp = "F" elif weather_opts['units'] == 'metric': - u_speed = u"m/sec" - u_temp = u"C" + u_speed = "m/sec" + u_temp = "C" else: - u_speed = u"m/sec" - u_temp = u"K" + u_speed = "m/sec" + u_temp = "K" - deg_symbol = u"\u00b0" + deg_symbol = "°" iw.bitmap(2, 2, weather['icon']) - iw.text(70, 2, weather['description'].title().encode(charset).strip(), 'robotoBlack24', 'black') + iw.text(70, 2, weather['description'].title().strip(), 'robotoBlack24', 'black') iw.text(70, 35, weather['sunrise'], 'robotoRegular18', 'black') iw.text(154, 35, weather['sunset'], 'robotoRegular18', 'black') diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 563963a..c566bd2 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -28,7 +28,7 @@ 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 list(calendar_list_entry.keys()): if calendar_list_entry['primary']: calendar_ids.append(calendar_list_entry['id']) elif calendar_list_entry['summary'] in self.additional: @@ -49,7 +49,7 @@ def list(self): initial_start = event['start'].get('dateTime', event['start'].get('date')) start = "%s-0" % initial_start counter = 0 - while start in events.keys(): + while start in list(events.keys()): counter += 1 start = "%s-%s" % (initial_start, counter) diff --git a/mod_todo/mod_google.py b/mod_todo/mod_google.py index 0e79642..18bbe9d 100644 --- a/mod_todo/mod_google.py +++ b/mod_todo/mod_google.py @@ -29,11 +29,11 @@ def list(self): results = service.tasks().list(tasklist=tasklist['id']).execute() # Loop through results and format them for ingest - if 'items' in results.keys(): + if 'items' in list(results.keys()): for task in results['items']: today = False - if 'due' in task.keys(): + if 'due' in list(task.keys()): if task['due'].startswith(now.strftime("%Y-%m-%d")): today = True diff --git a/mod_todo/mod_teamwork.py b/mod_todo/mod_teamwork.py index dc41a92..21c742f 100644 --- a/mod_todo/mod_teamwork.py +++ b/mod_todo/mod_teamwork.py @@ -1,4 +1,4 @@ -import urllib2, base64 +import urllib.request, urllib.error, urllib.parse, base64 import json import logging @@ -11,10 +11,10 @@ def __init__(self, opts): def list(self): action = "tasks.json?sort=priority" - request = urllib2.Request("https://{0}/{1}".format(self.company, action)) + request = urllib.request.Request("https://{0}/{1}".format(self.company, action)) request.add_header("Authorization", "BASIC " + base64.b64encode(self.key + ":xxx")) - response = urllib2.urlopen(request) + response = urllib.request.urlopen(request) data = json.loads(response.read()) items = [] diff --git a/mod_utils/iw_utils.py b/mod_utils/iw_utils.py index 6551943..ea462dc 100644 --- a/mod_utils/iw_utils.py +++ b/mod_utils/iw_utils.py @@ -16,6 +16,6 @@ def getCWD(): def HandleError(msg): print("ERROR IN PROGRAM ======================================") print("Program requires user input. Please run from console") - print("ERR: " + msg) + print(("ERR: " + msg)) print("END ERROR =============================================") quit() diff --git a/mod_weather/mod_owm.py b/mod_weather/mod_owm.py index 82352df..b51d97f 100644 --- a/mod_weather/mod_owm.py +++ b/mod_weather/mod_owm.py @@ -37,15 +37,15 @@ def getIcon(self, iconUrl): return self.pngToBmp(bn) def degreesToTextDesc(self, deg): - if deg > 337.5: return u"N" - if deg > 292.5: return u"NW" - if deg > 247.5: return u"W" - if deg > 202.5: return u"SW" - if deg > 157.5: return u"S" - if deg > 122.5: return u"SE" - if deg > 67.5: return u"E" - if deg > 22.5: return u"NE" - return u"N" + if deg > 337.5: return "N" + if deg > 292.5: return "NW" + if deg > 247.5: return "W" + if deg > 202.5: return "SW" + if deg > 157.5: return "S" + if deg > 122.5: return "SE" + if deg > 67.5: return "E" + if deg > 22.5: return "NE" + return "N" def list(self): url = 'http://api.openweathermap.org/data/2.5/weather' @@ -66,7 +66,7 @@ def list(self): for wType in wTypes: # Check to see if dictionary has values for rain or snow. # if it does NOT, set zero values for consistancy. - if data.has_key(wType): + if wType in data: setattr(self, wType, { "1h": data[wType].get('1h'), "3h": data[wType].get('3h') diff --git a/requirements.txt b/requirements.txt index 8c11f53..9f877d8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,92 +1,8 @@ -arandr==0.1.9 -arrow==0.11.0 -asn1crypto==0.24.0 -automationhat==0.2.0 -blinker==1.4 -blinkt==0.1.2 -buttonshim==0.0.2 -cachetools==3.1.1 -Cap1xxx==0.1.3 -certifi==2019.9.11 -chardet==3.0.4 -Click==7.0 -colorama==0.3.7 -colorzero==1.1 -configparser==3.5.0b2 -cookies==2.2.1 -cryptography==2.6.1 -drumhat==0.1.0 -entrypoints==0.3 -enum34==1.1.6 -envirophat==1.0.0 -ExplorerHAT==0.4.2 -Flask==1.0.2 -fourletterphat==0.1.0 -funcsigs==1.0.2 -google-api-python-client==1.7.11 -google-auth==1.6.3 -google-auth-httplib2==0.0.3 -google-auth-oauthlib==0.4.1 -gpiozero==1.5.1 -gtasks==0.1.3 -httplib2==0.14.0 -ics==0.5 -idna==2.8 -ipaddress==1.0.17 -itsdangerous==0.24 -Jinja2==2.10 -keyring==17.1.1 -keyrings.alt==3.1.1 -MarkupSafe==1.1.0 -microdotphat==0.2.1 -mock==2.0.0 -mote==0.0.4 -motephat==0.0.2 -numpy==1.16.2 -oauthlib==3.1.0 -olefile==0.46 -pantilthat==0.0.7 -pbr==4.2.0 -phatbeat==0.1.1 -pianohat==0.1.0 -picamera==1.13 -picraft==1.0 -piglow==1.2.4 -pigpio==1.44 -Pillow==5.4.1 -pyasn1==0.4.7 -pyasn1-modules==0.2.7 -pycairo==1.16.2 -pycrypto==2.6.1 -pygame==1.9.4.post1 -PyGObject==3.30.4 -pyinotify==0.9.6 -PyJWT==1.7.0 -pyOpenSSL==19.0.0 -pyserial==3.4 -python-dateutil==2.8.0 -python-teamwork==0.1.3 -pyxdg==0.25 -rainbowhat==0.1.0 -requests==2.22.0 -requests-oauthlib==1.2.0 -responses==0.9.0 -RPi.GPIO==0.7.0 -rsa==4.0 -RTIMULib==7.2.1 -scrollphat==0.0.7 -scrollphathd==1.2.1 -SecretStorage==2.3.1 -sense-hat==2.2.0 -simplejson==3.16.0 -six==1.12.0 -skywriter==0.0.7 -sn3218==1.2.7 -spidev==3.3 -todoist-python==8.1.0 -touchphat==0.0.1 -twython==3.7.0 -unicornhathd==0.0.4 -uritemplate==3.0.0 -urllib3==1.25.6 -Werkzeug==0.14.1 +Pillow +spidev +google-api-python-client +google-auth-oauthlib +python-dateutil +requests +todoist-python +RPi.GPIO \ No newline at end of file diff --git a/screensaver.py b/screensaver.py index c1aad2b..4f3ac48 100755 --- a/screensaver.py +++ b/screensaver.py @@ -1,10 +1,11 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 import logging import os -from driver import epd7in5b from PIL import Image +from driver import epd7in5b + # Setup Logging - change to logging.DEBUG if you are having issues. logging.basicConfig(level=logging.INFO) logging.info("Screen saver starting") From deb311906a9c2098154ff0251befc2125f20e9bc Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Dec 2021 03:15:28 +0100 Subject: [PATCH 057/113] ignoring pycharm --- .gitignore | 1 + .idea/.gitignore | 8 -------- 2 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 .idea/.gitignore diff --git a/.gitignore b/.gitignore index fc842ce..32e2431 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ test.py config.json infowindow.jpg venv/ +.idea/ diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml From fefd2cda54ee888bd69ccd6493357690af01b00b Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sat, 4 Dec 2021 11:43:59 +0100 Subject: [PATCH 058/113] only showing tasks with a due-date in the next two days --- mod_todo/mod_google.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/mod_todo/mod_google.py b/mod_todo/mod_google.py index 18bbe9d..4eec573 100644 --- a/mod_todo/mod_google.py +++ b/mod_todo/mod_google.py @@ -1,9 +1,10 @@ from mod_utils import mod_google_auth from googleapiclient.discovery import build -from datetime import datetime +from datetime import timedelta, date import logging -now = datetime.now() +today = date.today() +tomorrow = date.today() + timedelta(days=1) logger = logging.getLogger(__name__) @@ -32,15 +33,20 @@ def list(self): if 'items' in list(results.keys()): for task in results['items']: - today = False + is_today = False if 'due' in list(task.keys()): - if task['due'].startswith(now.strftime("%Y-%m-%d")): - today = True + if task['due'].startswith(today.strftime("%Y-%m-%d")): + is_today = True + elif task['due'].startswith(tomorrow.strftime("%Y-%m-%d")): + pass + else: + # if this task is 3 days or more in the future, don't show it + continue items.append({ "content": task['title'], "priority": task['position'], - "today": today + "today": is_today }) # Return results to main program From bb3c75f1fd7b8417ba59395fd7b3ac4b8d4d561f Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sat, 4 Dec 2021 16:49:53 +0100 Subject: [PATCH 059/113] sorting of tasks is now way more intelligent --- mod_todo/mod_google.py | 50 ++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/mod_todo/mod_google.py b/mod_todo/mod_google.py index 4eec573..d7abcb4 100644 --- a/mod_todo/mod_google.py +++ b/mod_todo/mod_google.py @@ -1,6 +1,6 @@ from mod_utils import mod_google_auth from googleapiclient.discovery import build -from datetime import timedelta, date +from datetime import datetime, timedelta, date import logging today = date.today() @@ -21,7 +21,9 @@ def list(self): logging.info("Entering ToDo.list()") service = build('tasks', 'v1', credentials=self.creds) - items = [] + tasks_with_due = [] + items_with_due = [] + items_without_due = [] # Fetch Results from all lists where todo is in the name tasklists = service.tasklists().list().execute() @@ -29,25 +31,35 @@ def list(self): if "todo" in tasklist['title'].lower(): results = service.tasks().list(tasklist=tasklist['id']).execute() - # Loop through results and format them for ingest if 'items' in list(results.keys()): for task in results['items']: - - is_today = False if 'due' in list(task.keys()): - if task['due'].startswith(today.strftime("%Y-%m-%d")): - is_today = True - elif task['due'].startswith(tomorrow.strftime("%Y-%m-%d")): - pass - else: - # if this task is 3 days or more in the future, don't show it - continue - - items.append({ - "content": task['title'], - "priority": task['position'], - "today": is_today - }) + tasks_with_due.append(task) + else: + items_without_due.append({ + "content": task['title'], + "priority": task['position'], + "today": False + }) + + for task in sorted(tasks_with_due, key=lambda x: x['due']): + is_today = False + due = datetime.fromisoformat(task['due'].replace("Z", "+00:00")).date() + if due < today: + is_today = True + task['title'] = "Overdue: %s" % task['title'] + elif due == today: + is_today = True + elif due == tomorrow: + pass + else: + continue + + items_with_due.append({ + "content": task['title'], + "priority": task['position'], + "today": is_today + }) # Return results to main program - return items + return items_with_due + items_without_due From bf5785a0d0d05bf9f4dcea5c45f3d2155cab96da Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 10 Mar 2022 22:33:38 +0100 Subject: [PATCH 060/113] adding reference to "bug" for google tasks not showing up --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 7c33db7..eafc3d0 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,13 @@ Todoist is the current active module in this code. It only requires `api_key`. T using google tasks, leave this as null `todo: null` * api_key: Enter your todoist API key. +There is a bug in the Google API which will prevent to show repeated Tasks once one is marked as completed. See (and +upvote): +* https://support.google.com/calendar/thread/3706294 +* https://support.google.com/calendar/thread/4113489 +* https://support.google.com/calendar/thread/111623199 +* https://support.google.com/calendar/thread/113398139 + ### 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. From 091f556268d8d9c4a3d53eeaa8fecc863d1a228c Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sat, 2 Jul 2022 02:48:56 +0200 Subject: [PATCH 061/113] changes to support waveshare 7.5 inch 3 color display v2 with the higher resolution (800x480) --- driver/epd7in5b.py | 212 ----------------------------------- driver/epd7in5b_V2.py | 192 +++++++++++++++++++++++++++++++ infowindow.py | 106 +++++++----------- mod_infowindow/infowindow.py | 52 ++++++--- resources/black.png | Bin 662 -> 0 bytes resources/red.png | Bin 1101 -> 0 bytes resources/white.png | Bin 1100 -> 0 bytes screensaver.py | 37 ++++-- 8 files changed, 303 insertions(+), 296 deletions(-) delete mode 100644 driver/epd7in5b.py create mode 100644 driver/epd7in5b_V2.py delete mode 100644 resources/black.png delete mode 100644 resources/red.png delete mode 100644 resources/white.png diff --git a/driver/epd7in5b.py b/driver/epd7in5b.py deleted file mode 100644 index 2791cf9..0000000 --- a/driver/epd7in5b.py +++ /dev/null @@ -1,212 +0,0 @@ -## - # @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_V2.py b/driver/epd7in5b_V2.py new file mode 100644 index 0000000..df09ae3 --- /dev/null +++ b/driver/epd7in5b_V2.py @@ -0,0 +1,192 @@ +# ***************************************************************************** +# * | File : epd7in5b_V2.py +# * | Author : Waveshare team +# * | Function : Electronic paper driver +# * | Info : +# *---------------- +# * | This version: V4.2 +# * | Date : 2022-01-08 +# # | 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 = 800 +EPD_HEIGHT = 480 + +logger = logging.getLogger(__name__) + +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 send_data2(self, data): #faster + 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): + logger.debug("e-Paper busy") + self.send_command(0x71) + busy = epdconfig.digital_read(self.busy_pin) + while(busy == 0): + self.send_command(0x71) + busy = epdconfig.digital_read(self.busy_pin) + epdconfig.delay_ms(200) + logger.debug("e-Paper busy release") + + def init(self): + if (epdconfig.module_init() != 0): + return -1 + + self.reset() + + # self.send_command(0x06) # btst + # self.send_data(0x17) + # self.send_data(0x17) + # self.send_data(0x38) # If an exception is displayed, try using 0x38 + # self.send_data(0x17) + + self.send_command(0x01); #POWER SETTING + self.send_data(0x07); + self.send_data(0x07); #VGH=20V,VGL=-20V + self.send_data(0x3f); #VDH=15V + self.send_data(0x3f); #VDL=-15V + + self.send_command(0x04); #POWER ON + epdconfig.delay_ms(100); + self.ReadBusy(); + + self.send_command(0X00); #PANNEL SETTING + self.send_data(0x0F); #KW-3f KWR-2F BWROTP 0f BWOTP 1f + + self.send_command(0x61); #tres + self.send_data(0x03); #source 800 + self.send_data(0x20); + self.send_data(0x01); #gate 480 + self.send_data(0xE0); + + self.send_command(0X15); + self.send_data(0x00); + + self.send_command(0X50); #VCOM AND DATA INTERVAL SETTING + self.send_data(0x11); + self.send_data(0x07); + + self.send_command(0X60); #TCON SETTING + self.send_data(0x22); + + self.send_command(0x65); + self.send_data(0x00); + self.send_data(0x00); + self.send_data(0x00); + self.send_data(0x00); + + 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): + # image has correct dimensions, but needs to be rotated + img = img.rotate(90, expand=True).convert('1') + else: + logger.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')) + # The bytes need to be inverted, because in the PIL world 0=black and 1=white, but + # in the e-paper world 0=white and 1=black. + for i in range(len(buf)): + buf[i] ^= 0xFF + return buf + + def display(self, imageblack, imagered): + self.send_command(0x10) + # The black bytes need to be inverted back from what getbuffer did + for i in range(len(imageblack)): + imageblack[i] ^= 0xFF + self.send_data2(imageblack) + + self.send_command(0x13) + self.send_data2(imagered) + + self.send_command(0x12) + epdconfig.delay_ms(100) + self.ReadBusy() + + def Clear(self): + buf = [0x00] * (int(self.width/8) * self.height) + buf2 = [0xff] * (int(self.width/8) * self.height) + self.send_command(0x10) + self.send_data2(buf2) + + self.send_command(0x13) + self.send_data2(buf) + + self.send_command(0x12) + 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) + + epdconfig.delay_ms(2000) + epdconfig.module_exit() +### END OF FILE ### + diff --git a/infowindow.py b/infowindow.py index 258081d..459ce85 100755 --- a/infowindow.py +++ b/infowindow.py @@ -44,26 +44,6 @@ logging.info("Configuration Complete") -# Custom exception handler. Need to handle exceptions and send them to the -# 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.strip(), 'robotoBlack18', 'black') - iw.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) - - -sys.excepthook = HandleException - - # helper to calculate max char width and height def get_max_char_size(iw, chars, font): max_x = 0 @@ -89,47 +69,38 @@ def main(): # Set some things calendar_date_font = "robotoRegular14" - calendar_entry_font = "robotoRegular18" - tasks_font = "robotoRegular18" + calendar_entry_font = "robotoRegular22" + tasks_font = "robotoRegular22" # Weather Grid - temp_rect_width = 102 + temp_rect_width = 128 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.line(335, 0, 335, 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 + iw.line(465, 0, 465, 64, 'black') # Second Vertical Line - # 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 + # Center cal + iw.line(392, 90, 392, 480, 'black') # Left Black line + iw.rectangle(393, 64, 406, 480, 'red') # Red Rectangle + iw.line(407, 90, 407, 480, 'black') # Right Black line - # 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 + # Calendar + iw.line(0, 64, 800, 64, 'black') # Top Line + iw.rectangle(0, 65, 800, 90, 'red') # Red Rectangle + iw.line(0, 91, 800, 91, 'black') # Bottom Black Line - # Todo / Weather Titles - iw.text(440, 64, "TODO", 'robotoBlack24', 'white') - iw.text(95, 64, "CALENDAR", 'robotoBlack24', 'white') + # Weather Titles + iw.text(550, 64, "TODO", 'robotoBlack24', 'white') + iw.text(118, 64, "CALENDAR", 'robotoBlack24', 'white') - # DISPLAY TODO INFO + # DISPLAY TO DO INFO # ========================================================================= todo_items = todo.list() 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) line_height = t_y + (2 * infowindow_opts["cell_spacing"]) @@ -140,9 +111,9 @@ def main(): if todo_item['today']: color = 'red' - iw.text(333, (current_task_y + infowindow_opts["cell_spacing"]), todo_item['content'].strip(), + iw.text(416, (current_task_y + infowindow_opts["cell_spacing"]), todo_item['content'].strip(), tasks_font, color) - iw.line(327, (current_task_y + line_height + 1), 640, (current_task_y + line_height + 1), 'black') + iw.line(408, (current_task_y + line_height + 1), 800, (current_task_y + line_height + 1), 'black') # set next loop height current_task_y = (current_task_y + line_height + 2) @@ -176,12 +147,12 @@ def main(): if cal_item['today']: font_color = calendar_opts['today_text_color'] iw.rectangle(0, current_calendar_y, - 313, (current_calendar_y + line_height), + 391, (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), + 391, (current_calendar_y + line_height + 1), 'black') # draw vertical line iw.line((dt_x + (2 * infowindow_opts["cell_spacing"]) + 1), current_calendar_y, @@ -198,7 +169,7 @@ def main(): cal_item['time'].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"] + max_event_text_length = 391 - calendar_event_text_start - infowindow_opts["cell_spacing"] iw.text(calendar_event_text_start, (current_calendar_y + ((line_height - it_y) / 2)), iw.truncate(cal_item['content'].strip(), calendar_entry_font, max_event_text_length), @@ -225,30 +196,37 @@ def main(): u_speed = "m/sec" u_temp = "K" + # Weather left box deg_symbol = "°" iw.bitmap(2, 2, weather['icon']) - iw.text(70, 2, weather['description'].title().strip(), 'robotoBlack24', 'black') - iw.text(70, 35, weather['sunrise'], 'robotoRegular18', 'black') - iw.text(154, 35, weather['sunset'], 'robotoRegular18', 'black') + iw.text(90, 2, weather['description'].title().strip(), 'robotoBlack24', 'black') + iw.text(90, 35, weather['sunrise'], 'robotoRegular18', 'black') + iw.text(192, 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_string = str(weather['temp_cur']) + deg_symbol + (t_x, t_y) = iw.getFont('robotoBlack54').getsize(temp_string) 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') + iw.text(temp_left, 2, temp_string, 'robotoBlack54', 'white') + t_desc_posx = (temp_left + t_x) - 18 + iw.text(t_desc_posx, 28, u_temp, 'robotoBlack24', 'white') # Wind - iw.text(405, 5, weather['wind']['dir'], 'robotoBlack18', 'black') - iw.text(380, 35, str(weather['wind']['speed']) + u_speed, 'robotoRegular18', 'black') + iw.bitmap(480, 0, "windSmall.bmp") # Wind Icon + iw.text(520, 5, weather['wind']['dir'], 'robotoBlack18', 'black') + iw.text(480, 35, str(weather['wind']['speed']) + u_speed, 'robotoRegular18', 'black') + iw.line(576, 0, 576, 64, 'black') # Third Vertical Line # Rain - iw.text(481, 29, "1hr: " + str(weather['rain']['1h']), 'robotoRegular18', 'black') - iw.text(481, 44, "3hr: " + str(weather['rain']['3h']), 'robotoRegular18', 'black') + iw.bitmap(616, 0, "rainSmall.bmp") # Rain Icon + iw.text(601, 29, "1hr: " + str(weather['rain']['1h']), 'robotoRegular18', 'black') + iw.text(601, 44, "3hr: " + str(weather['rain']['3h']), 'robotoRegular18', 'black') + iw.line(687, 0, 687, 64, 'black') # Fourth Vertical Line # Snow - iw.text(573, 29, "1hr: " + str(weather['snow']['1h']), 'robotoRegular18', 'black') - iw.text(573, 44, "3hr: " + str(weather['snow']['3h']), 'robotoRegular18', 'black') + iw.bitmap(728, 0, "snowSmall.bmp") # Snow Icon + iw.text(716, 29, "1hr: " + str(weather['snow']['1h']), 'robotoRegular18', 'black') + iw.text(716, 44, "3hr: " + str(weather['snow']['3h']), 'robotoRegular18', 'black') # Write to screen # ========================================================================= diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index dab37e8..c159630 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -1,4 +1,4 @@ -from driver import epd7in5b +from driver import epd7in5b_V2 from PIL import Image from PIL import ImageDraw from PIL import ImageFont @@ -8,13 +8,39 @@ import tempfile +def display_frame(epd, image_data): + rgb_image = image_data.convert('RGB') + width, height = image_data.size + no_red = Image.new('RGB', (width, height), (255, 255, 255)) + only_red = Image.new('RGB', (width, height), (255, 255, 255)) + + for col in range(width): + for row in range(height): + + r, g, b = rgb_image.getpixel((col, row)) + if r == 255 and g == 255 and b == 255: + no_red.putpixel((col, row), (255, 255, 255)) + elif r == 0 and g == 0 and b == 0: + no_red.putpixel((col, row), (0, 0, 0)) + else: + no_red.putpixel((col, row), (0, g, b)) + + if r == 255 and g == 0 and b == 0: + only_red.putpixel((col, row), (0, 0, 0)) + else: + only_red.putpixel((col, row), (255, 255, 255)) + + epd.display(epd.getbuffer(no_red), epd.getbuffer(only_red)) + epd.sleep() + + class InfoWindow: def __init__(self, options): - self.epd = epd7in5b.EPD() + self.epd = epd7in5b_V2.EPD() self.epd.init() - self.width = 640 - self.height = 384 - self.image = Image.new('L', (640, 384), 255) + self.width = 800 + self.height = 480 + self.image = Image.new(mode="RGB", size=(800, 480), color=(255, 255, 255)) self.draw = ImageDraw.Draw(self.image) self.fonts = {} self.initFonts() @@ -54,7 +80,7 @@ def rotate(self, angle): 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) + self.draw.bitmap((x, y), bitmap, fill=(0, 0, 0)) def getFont(self, font_name): return self.fonts[font_name] @@ -63,11 +89,12 @@ 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) + 'robotoBlack18': ImageFont.truetype(roboto + "Black.ttf", 18), + 'robotoBlack24': ImageFont.truetype(roboto + "Black.ttf", 24), + 'robotoBlack54': ImageFont.truetype(roboto + "Black.ttf", 54), + 'robotoRegular18': ImageFont.truetype(roboto + "Regular.ttf", 18), + 'robotoRegular14': ImageFont.truetype(roboto + "Regular.ttf", 14), + 'robotoRegular22': ImageFont.truetype(roboto + "Regular.ttf", 22), } def truncate(self, string, font, max_size): @@ -95,7 +122,6 @@ def display(self, angle): 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() + display_frame(self.epd, self.image) else: logging.info("No new information found. Not updating the screen.") diff --git a/resources/black.png b/resources/black.png deleted file mode 100644 index 41e62e226a9dee366b3e4960696d1850b149d2eb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 662 zcmeAS@N?(olHy`uVBq!ia0y~yU}|7sU~J$33NT3c^&|l~Dw)pC0iMpz3I#>^X_+~x z3=A3*=T6w`btFKbb^n@0N>M+XS9oyUW|a=U`J?$@QsUWl30qF7MaK9fd2G_z*Oc&)Mp1{-IAM`SSrgPt-7 zGgd6MF9Qm)mw5WRvOi&z6O}g9TNEG+3|Cf97srr_TW=2c%lRHw5O||%Q~loCIAeDt(yP< diff --git a/resources/red.png b/resources/red.png deleted file mode 100644 index 9298929c318bafaf5e25d5071b48ba6642ff8562..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1101 zcmeAS@N?(olHy`uVBq!ia0y~yU}|7sU~J$33NT3c^&|l~Dw)pC0iMpz3I#>^X_+~x z3=A3*=T6w`btFKbb^n@0N>M+XS9oyUW|a=U`J?$@QsUWl30qF7MaK9fd2G_z*Oc&)Mp1{-IAM`SSrgPt-7 zGgd6MF9Qm)mw5WRvOi&z6P30;SH6=M7|?q>T^vIyZoR$aD9FITaoE7&{-4*5Y&Jri zd&{`q^{_|?FgGz8Dv(UEu`nk&7$}fV9cf5#;Nf5+k?LSP*dQUmLk2~L-=Kj(y5B%y zLb~6efkB4fKw-k{)j&@C0X32rf8_g-^X_+~x z3=A3*=T6w`btFKbb^n@0N>M+XS9oyUW|a=U`J?$@QsUWl30qF7MaK9fd2G_z*Oc&)Mp1{-IAM`SSrgPt-7 zGgd6MF9Qm)mw5WRvOi&z6O~qRuHW(l7|^>tT^vIyZoR$aD9FITaoFI%{^yF!a-A)4 z)~w}jOgtTo2OA^=NT!&Z7!4I91W2cn91IlLSeQwqjx;1V@Nlq^L6PA%Xkd`;H&B?6 z?l)*)kl{CAm~=3z9v~=3> BK^p)7 diff --git a/screensaver.py b/screensaver.py index 4f3ac48..85a0bcc 100755 --- a/screensaver.py +++ b/screensaver.py @@ -4,23 +4,46 @@ import os from PIL import Image -from driver import epd7in5b +from driver import epd7in5b_V2 # Setup Logging - change to logging.DEBUG if you are having issues. logging.basicConfig(level=logging.INFO) logging.info("Screen saver starting") +def display_image(epd, image_data): + rgb_image = image_data.convert('RGB') + width, height = image_data.size + no_red = Image.new('RGB', (width, height), (255, 255, 255)) + only_red = Image.new('RGB', (width, height), (255, 255, 255)) + + for col in range(width): + for row in range(height): + + r, g, b = rgb_image.getpixel((col, row)) + no_red.putpixel((col, row), (0, g, b)) + + if r == 255 and g == 0 and b == 0: + only_red.putpixel((col, row), (0, 0 ,0)) + else: + only_red.putpixel((col, row), (255, 255, 255)) + + epd.display(epd.getbuffer(no_red), epd.getbuffer(only_red)) + def main(): - epd = epd7in5b.EPD() + epd = epd7in5b_V2.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)) + width = 800 + height = 480 + + logging.info("Display black screen") + display_image(epd, Image.new('RGB', (width, height), (0, 0, 0))) + + logging.info("Display red screen") + display_image(epd, Image.new('RGB', (width, height), (255, 0, 0))) + epd.Clear() epd.sleep() logging.info("Screen saver finished") From 0114535ed93375468d6861e8ba9273d2a46a5d88 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 3 Jul 2022 20:43:54 +0200 Subject: [PATCH 062/113] improve readability and rewrite image creation --- infowindow.py | 40 ++++++----- mod_infowindow/infowindow.py | 124 ++++++++++++++++++----------------- 2 files changed, 87 insertions(+), 77 deletions(-) diff --git a/infowindow.py b/infowindow.py index 459ce85..ad6fa0e 100755 --- a/infowindow.py +++ b/infowindow.py @@ -68,9 +68,13 @@ def main(): iw = infowindow.InfoWindow(infowindow_opts) # Set some things - calendar_date_font = "robotoRegular14" - calendar_entry_font = "robotoRegular22" - tasks_font = "robotoRegular22" + weather_font = "robotoBlack18" + temperature_font = "robotoBlack54" + calendar_date_font = "robotoBlack14" + calendar_entry_font = "robotoBlack22" + calendar_entry_font_highlited = "robotoBlack22" + tasks_font = "robotoBlack22" + tasks_font_highlited = "robotoBlack22" # Weather Grid temp_rect_width = 128 @@ -108,11 +112,13 @@ def main(): for todo_item in todo_items: color = 'black' if 'today' in list(todo_item.keys()): + current_font = tasks_font if todo_item['today']: color = 'red' + current_font = tasks_font_highlited iw.text(416, (current_task_y + infowindow_opts["cell_spacing"]), todo_item['content'].strip(), - tasks_font, color) + current_font, color) iw.line(408, (current_task_y + line_height + 1), 800, (current_task_y + line_height + 1), 'black') # set next loop height @@ -143,8 +149,10 @@ def main(): current_calendar_y = 92 for cal_item in cal_items: + current_font = calendar_entry_font font_color = 'black' if cal_item['today']: + current_font = calendar_entry_font_highlited font_color = calendar_opts['today_text_color'] iw.rectangle(0, current_calendar_y, 391, (current_calendar_y + line_height), @@ -172,8 +180,8 @@ def main(): max_event_text_length = 391 - calendar_event_text_start - infowindow_opts["cell_spacing"] iw.text(calendar_event_text_start, (current_calendar_y + ((line_height - it_y) / 2)), - iw.truncate(cal_item['content'].strip(), calendar_entry_font, max_event_text_length), - calendar_entry_font, font_color) + iw.truncate(cal_item['content'].strip(), current_font, max_event_text_length), + current_font, font_color) # set new line height for next round current_calendar_y = (current_calendar_y + line_height + 2) @@ -200,33 +208,33 @@ def main(): deg_symbol = "°" iw.bitmap(2, 2, weather['icon']) iw.text(90, 2, weather['description'].title().strip(), 'robotoBlack24', 'black') - iw.text(90, 35, weather['sunrise'], 'robotoRegular18', 'black') - iw.text(192, 35, weather['sunset'], 'robotoRegular18', 'black') + iw.text(90, 35, weather['sunrise'], weather_font, 'black') + iw.text(192, 35, weather['sunset'], weather_font, 'black') # Temp ( adjust for str length ) temp_string = str(weather['temp_cur']) + deg_symbol - (t_x, t_y) = iw.getFont('robotoBlack54').getsize(temp_string) + (t_x, t_y) = iw.getFont(temperature_font).getsize(temp_string) temp_left = (iw.width / 2) - (t_x / 2) - iw.text(temp_left, 2, temp_string, 'robotoBlack54', 'white') + iw.text(temp_left, 2, temp_string, temperature_font, 'white') t_desc_posx = (temp_left + t_x) - 18 iw.text(t_desc_posx, 28, u_temp, 'robotoBlack24', 'white') # Wind iw.bitmap(480, 0, "windSmall.bmp") # Wind Icon - iw.text(520, 5, weather['wind']['dir'], 'robotoBlack18', 'black') - iw.text(480, 35, str(weather['wind']['speed']) + u_speed, 'robotoRegular18', 'black') + iw.text(520, 5, weather['wind']['dir'], weather_font, 'black') + iw.text(480, 35, str(weather['wind']['speed']) + u_speed, weather_font, 'black') iw.line(576, 0, 576, 64, 'black') # Third Vertical Line # Rain iw.bitmap(616, 0, "rainSmall.bmp") # Rain Icon - iw.text(601, 29, "1hr: " + str(weather['rain']['1h']), 'robotoRegular18', 'black') - iw.text(601, 44, "3hr: " + str(weather['rain']['3h']), 'robotoRegular18', 'black') + iw.text(601, 29, "1hr: " + str(weather['rain']['1h']), weather_font, 'black') + iw.text(601, 44, "3hr: " + str(weather['rain']['3h']), weather_font, 'black') iw.line(687, 0, 687, 64, 'black') # Fourth Vertical Line # Snow iw.bitmap(728, 0, "snowSmall.bmp") # Snow Icon - iw.text(716, 29, "1hr: " + str(weather['snow']['1h']), 'robotoRegular18', 'black') - iw.text(716, 44, "3hr: " + str(weather['snow']['3h']), 'robotoRegular18', 'black') + iw.text(716, 29, "1hr: " + str(weather['snow']['1h']), weather_font, 'black') + iw.text(716, 44, "3hr: " + str(weather['snow']['3h']), weather_font, 'black') # Write to screen # ========================================================================= diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index c159630..1895fbb 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -8,79 +8,69 @@ import tempfile -def display_frame(epd, image_data): - rgb_image = image_data.convert('RGB') - width, height = image_data.size - no_red = Image.new('RGB', (width, height), (255, 255, 255)) - only_red = Image.new('RGB', (width, height), (255, 255, 255)) - - for col in range(width): - for row in range(height): - - r, g, b = rgb_image.getpixel((col, row)) - if r == 255 and g == 255 and b == 255: - no_red.putpixel((col, row), (255, 255, 255)) - elif r == 0 and g == 0 and b == 0: - no_red.putpixel((col, row), (0, 0, 0)) - else: - no_red.putpixel((col, row), (0, g, b)) - - if r == 255 and g == 0 and b == 0: - only_red.putpixel((col, row), (0, 0, 0)) - else: - only_red.putpixel((col, row), (255, 255, 255)) - - epd.display(epd.getbuffer(no_red), epd.getbuffer(only_red)) - epd.sleep() - - class InfoWindow: def __init__(self, options): self.epd = epd7in5b_V2.EPD() self.epd.init() self.width = 800 self.height = 480 - self.image = Image.new(mode="RGB", size=(800, 480), color=(255, 255, 255)) - self.draw = ImageDraw.Draw(self.image) + self.red_image = Image.new(mode="1", size=(800, 480), color=1) + self.black_image = Image.new(mode="1", size=(800, 480), color=1) + self.red_draw = ImageDraw.Draw(self.red_image) + self.black_draw = ImageDraw.Draw(self.black_image) self.fonts = {} self.initFonts() - self.tmpImagePath = os.path.join(tempfile.gettempdir(), "InfoWindow.png") + self.tmpImagePathRed = os.path.join(tempfile.gettempdir(), "InfoWindowRed.png") + self.tmpImagePathBlack = os.path.join(tempfile.gettempdir(), "InfoWindowBlack.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 line(self, left_1, top_1, left_2, top_2, fill): + if fill == 'black': + self.black_draw.line((left_1, top_1, left_2, top_2), fill=0) + elif fill == 'red': + self.red_draw.line((left_1, top_1, left_2, top_2), fill=0) + elif fill == 'white': + self.black_draw.line((left_1, top_1, left_2, top_2), fill=1) + self.red_draw.line((left_1, top_1, left_2, top_2), fill=1) def rectangle(self, tl, tr, bl, br, fill): - self.draw.rectangle(((tl, tr), (bl, br)), fill=fill) + if fill == 'black': + self.black_draw.rectangle(((tl, tr), (bl, br)), fill=0) + elif fill == 'red': + self.red_draw.rectangle(((tl, tr), (bl, br)), fill=0) + elif fill == 'white': + self.black_draw.rectangle(((tl, tr), (bl, br)), fill=1) + self.red_draw.rectangle(((tl, tr), (bl, br)), fill=1) 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) + if fill == 'black': + font = self.fonts[font] + self.black_draw.text((left, top), text, font=font, fill=0) + return self.black_draw.textsize(text, font=font) + elif fill == 'red': + font = self.fonts[font] + self.black_draw.text((left, top), text, font=font, fill=0) + self.red_draw.text((left, top), text, font=font, fill=0) + return self.red_draw.textsize(text, font=font) + elif fill == 'white': + font = self.fonts[font] + self.red_draw.text((left, top), text, font=font, fill=1) + self.black_draw.text((left, top), text, font=font, fill=1) + return self.red_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) + self.red_image.rotate(angle) + self.black_image.rotate(angle) 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, fill=(0, 0, 0)) + # self.draw.bitmap((x, y), bitmap, fill=(0, 0, 0)) + self.black_draw.bitmap((x, y), bitmap, fill=0) def getFont(self, font_name): return self.fonts[font_name] @@ -89,12 +79,12 @@ def initFonts(self): roboto = self.getCWD()+"/fonts/roboto/Roboto-" self.fonts = { + 'robotoBlack14': ImageFont.truetype(roboto + "Black.ttf", 14), 'robotoBlack18': ImageFont.truetype(roboto + "Black.ttf", 18), + 'robotoBold22': ImageFont.truetype(roboto + "Bold.ttf", 22), + 'robotoBlack22': ImageFont.truetype(roboto + "Black.ttf", 22), 'robotoBlack24': ImageFont.truetype(roboto + "Black.ttf", 24), 'robotoBlack54': ImageFont.truetype(roboto + "Black.ttf", 54), - 'robotoRegular18': ImageFont.truetype(roboto + "Regular.ttf", 18), - 'robotoRegular14': ImageFont.truetype(roboto + "Regular.ttf", 14), - 'robotoRegular22': ImageFont.truetype(roboto + "Regular.ttf", 22), } def truncate(self, string, font, max_size): @@ -110,18 +100,30 @@ def truncate(self, string, font, max_size): return string def display(self, angle): - self.image = self.image.rotate(angle) + self.black_image = self.black_image.rotate(angle) + self.red_image = self.red_image.rotate(angle) + + new_image_found = False + if os.path.exists(self.tmpImagePathRed): + diff = ImageChops.difference(self.red_image, Image.open(self.tmpImagePathRed)) + if diff.getbbox(): + new_image_found = True + else: + new_image_found = True - 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 os.path.exists(self.tmpImagePathBlack): + diff = ImageChops.difference(self.black_image, Image.open(self.tmpImagePathBlack)) + if diff.getbbox(): + new_image_found = True + else: + new_image_found = True if new_image_found: logging.info("New information in the image detected. Updating the screen.") - self.image.save(self.tmpImagePath) - display_frame(self.epd, self.image) + self.black_image.save(self.tmpImagePathBlack) + self.red_image.save(self.tmpImagePathRed) + self.epd.display(self.epd.getbuffer(self.black_image), self.epd.getbuffer(self.red_image)) + self.epd.sleep() + else: logging.info("No new information found. Not updating the screen.") From 8816b26d3a1138ea54e6b29863299c6d0aa98422 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Wed, 22 Mar 2023 10:36:20 +0100 Subject: [PATCH 063/113] updating the readme --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eafc3d0..51ebd90 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ to the data. In your face reminder. Features | Installation | Configuration | - Running | + Running
## Features @@ -29,6 +29,11 @@ to the data. In your face reminder. ### Raspberry Pi setup Activate SPI on your Raspberry Pi by using the `raspi-config` tool under Interface Options and reboot. +Also for some RaspiOS versions, you have to install the `libopenjp2-7` package: +``` +sudo apt-get install libopenjp2-7 +``` + ### 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` @@ -41,6 +46,7 @@ cd /home/pi/InfoWindow export CFLAGS=-fcommon sudo apt install python3-dev python3 -m venv venv +. activate/bin/activate pip install -r requirements.txt ``` From a40c4f7df0e00279bb33bff35f9116efce0fa36e Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sat, 6 May 2023 12:44:52 +0200 Subject: [PATCH 064/113] * improve date/time readability * implement title rendering method * if there are no todos, show more calendar * rename variables for more human understanding * fix deprecated .getsize in pillow --- infowindow.py | 156 ++++++++++++++++++++++++++++---------------------- 1 file changed, 88 insertions(+), 68 deletions(-) diff --git a/infowindow.py b/infowindow.py index ad6fa0e..aaadbbe 100755 --- a/infowindow.py +++ b/infowindow.py @@ -46,15 +46,22 @@ # helper to calculate max char width and height def get_max_char_size(iw, chars, font): - max_x = 0 - max_y = 0 + max_width = 0 + max_height = 0 for char in chars: - (x, y) = iw.getFont(font).getsize(char) - if x > max_x: - max_x = x - if y > max_y: - max_y = y - return max_x, max_y + left, top, right, bottom = iw.getFont(font).getbbox(char) + width, height = right - left, bottom - top + if width > max_width: + max_width = width + if height > max_height: + max_height = height + return max_width, max_height + + +def render_centered_text(iw, text, font, color, center_position, y_position): + length = iw.getFont(font).getlength(text) + x_position = int(center_position - (length / 2)) + iw.text(x_position, y_position, text, font, color) # Main Program ################################################################ @@ -95,24 +102,20 @@ def main(): iw.rectangle(0, 65, 800, 90, 'red') # Red Rectangle iw.line(0, 91, 800, 91, 'black') # Bottom Black Line - # Weather Titles - iw.text(550, 64, "TODO", 'robotoBlack24', 'white') - iw.text(118, 64, "CALENDAR", 'robotoBlack24', 'white') - # DISPLAY TO DO INFO # ========================================================================= todo_items = todo.list() logging.debug("Todo Items") logging.debug("-----------------------------------------------------------------------") - (t_x, t_y) = get_max_char_size(iw, string.printable, tasks_font) - line_height = t_y + (2 * infowindow_opts["cell_spacing"]) + (text_width, text_height) = get_max_char_size(iw, string.printable, tasks_font) + line_height = text_height + (2 * infowindow_opts["cell_spacing"]) current_task_y = 92 for todo_item in todo_items: color = 'black' + current_font = tasks_font if 'today' in list(todo_item.keys()): - current_font = tasks_font if todo_item['today']: color = 'red' current_font = tasks_font_highlited @@ -131,62 +134,78 @@ def main(): logging.debug("Calendar Items") logging.debug("-----------------------------------------------------------------------") + (text_width, text_height) = get_max_char_size(iw, string.digits, calendar_date_font) 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') - dt_x = dt_x + (4 * t_x) - if t_y > dt_y: - dt_y = t_y - + left, top, right, bottom = iw.getFont(calendar_date_font).getbbox(': pm') 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('.') - dt_x = dt_x + (4 * t_x) + left, top, right, bottom = iw.getFont(calendar_date_font).getbbox('.') + date_time_width, date_time_height = right - left, bottom - top + date_time_width = date_time_width + (4 * text_width) + if text_height > date_time_height: + date_time_height = text_height - (it_x, it_y) = get_max_char_size(iw, string.printable, calendar_entry_font) + (chars_max_width, chars_max_height) = get_max_char_size(iw, string.printable, calendar_entry_font) + line_height = (2 * date_time_height) + (2 * infowindow_opts["cell_spacing"]) - line_height = (2 * dt_y) + (2 * infowindow_opts["cell_spacing"]) + def render_calendar(x_min, x_max, loop_start=0): + current_index = 0 + current_calendar_y = 92 + loop_date_time_width = x_min + date_time_width - current_calendar_y = 92 - for cal_item in cal_items: current_font = calendar_entry_font - font_color = 'black' - if cal_item['today']: - current_font = calendar_entry_font_highlited - font_color = calendar_opts['today_text_color'] - iw.rectangle(0, current_calendar_y, - 391, (current_calendar_y + line_height), - calendar_opts['today_background_color']) - - # draw horizontal line - iw.line(0, (current_calendar_y + line_height + 1), - 391, (current_calendar_y + line_height + 1), - 'black') - # draw vertical line - iw.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"]), - (current_calendar_y + infowindow_opts["cell_spacing"]), - cal_item['date'].strip(), calendar_date_font, font_color) - # draw event time - iw.text((infowindow_opts["cell_spacing"]), - (current_calendar_y + ((line_height - 2 * infowindow_opts["cell_spacing"]) / 2)), - cal_item['time'].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 = 391 - calendar_event_text_start - infowindow_opts["cell_spacing"] - iw.text(calendar_event_text_start, - (current_calendar_y + ((line_height - it_y) / 2)), - iw.truncate(cal_item['content'].strip(), current_font, max_event_text_length), - current_font, font_color) - - # set new line height for next round - current_calendar_y = (current_calendar_y + line_height + 2) - # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) - logging.debug("ITEM: %s" % cal_item['content'].strip()) + for cal_item in cal_items[loop_start:]: + font_color = 'black' + if cal_item['today']: + current_font = calendar_entry_font_highlited + font_color = calendar_opts['today_text_color'] + iw.rectangle(x_min, current_calendar_y, + x_max, (current_calendar_y + line_height), + calendar_opts['today_background_color']) + + # draw horizontal line + iw.line(x_min, (current_calendar_y + line_height + 1), x_max, (current_calendar_y + line_height + 1), 'black') + + # draw vertical line + iw.line((loop_date_time_width + (2 * infowindow_opts["cell_spacing"]) + 1), current_calendar_y, + (loop_date_time_width + (2 * infowindow_opts["cell_spacing"]) + 1), (current_calendar_y + line_height), 'black') + + # draw event date + iw.text((infowindow_opts["cell_spacing"]) + x_min, + current_calendar_y, + cal_item['date'].strip(), calendar_date_font, font_color) + # draw event time + iw.text((infowindow_opts["cell_spacing"]) + x_min, + current_calendar_y + 1 + date_time_height, + cal_item['time'].strip(), calendar_date_font, font_color) + # draw event text + calendar_event_text_start = loop_date_time_width + (3 * infowindow_opts["cell_spacing"]) + 1 + max_event_text_length = x_max - calendar_event_text_start - infowindow_opts["cell_spacing"] + iw.text(calendar_event_text_start, + (current_calendar_y + ((line_height - chars_max_height) / 2)), + iw.truncate(cal_item['content'].strip(), current_font, max_event_text_length), + current_font, font_color) + + # set new line height for next round + current_calendar_y = (current_calendar_y + line_height + 2) + # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) + logging.debug("ITEM: %s" % cal_item['content'].strip()) + current_index = cal_items.index(cal_item) + if current_calendar_y > 480: + logging.debug("Max height detected, breaking loop") + break + + return current_index + + last_item = render_calendar(0, 391) + if current_task_y == 92: # there are no tasks + render_calendar(408, 800, last_item + 1) + left_column_title = "CALENDAR 1/2" + right_column_title = "CALENDAR 2/2" + else: + left_column_title = "CALENDAR" + right_column_title = "TODO" + render_centered_text(iw, left_column_title, 'robotoBlack24', 'white', 200, 64) + render_centered_text(iw, right_column_title, 'robotoBlack24', 'white', 600, 64) # DISPLAY WEATHER INFO # ========================================================================= @@ -213,10 +232,11 @@ def main(): # Temp ( adjust for str length ) temp_string = str(weather['temp_cur']) + deg_symbol - (t_x, t_y) = iw.getFont(temperature_font).getsize(temp_string) - temp_left = (iw.width / 2) - (t_x / 2) + left, top, right, bottom = iw.getFont(temperature_font).getbbox(temp_string) + text_width, text_height = right - left, bottom - top + temp_left = (iw.width / 2) - (text_width / 2) iw.text(temp_left, 2, temp_string, temperature_font, 'white') - t_desc_posx = (temp_left + t_x) - 18 + t_desc_posx = (temp_left + text_width) - 18 iw.text(t_desc_posx, 28, u_temp, 'robotoBlack24', 'white') # Wind From 7e405311fe85483865ed66f1006f60a5f1fd57aa Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 7 May 2023 17:06:14 +0200 Subject: [PATCH 065/113] implement "weeks_away" red line --- infowindow.py | 16 ++++++++++++++-- mod_calendar/mod_google.py | 7 +++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/infowindow.py b/infowindow.py index aaadbbe..30fbffc 100755 --- a/infowindow.py +++ b/infowindow.py @@ -150,6 +150,7 @@ def main(): def render_calendar(x_min, x_max, loop_start=0): current_index = 0 current_calendar_y = 92 + current_weeks_away = -1 loop_date_time_width = x_min + date_time_width current_font = calendar_entry_font @@ -163,11 +164,22 @@ def render_calendar(x_min, x_max, loop_start=0): calendar_opts['today_background_color']) # draw horizontal line - iw.line(x_min, (current_calendar_y + line_height + 1), x_max, (current_calendar_y + line_height + 1), 'black') + # on first run, set current_weeks_away to first event weeks + if current_weeks_away < 0: + current_weeks_away = cal_item['weeks_away'] + + if current_weeks_away != cal_item['weeks_away']: + # override the black line with red since the "weeks away" number changed + current_weeks_away = cal_item['weeks_away'] + iw.line(x_min, (current_calendar_y - 1), x_max, (current_calendar_y - 1), 'red') + + iw.line(x_min, (current_calendar_y + line_height + 1), x_max, (current_calendar_y + line_height + 1), + 'black') # draw vertical line iw.line((loop_date_time_width + (2 * infowindow_opts["cell_spacing"]) + 1), current_calendar_y, - (loop_date_time_width + (2 * infowindow_opts["cell_spacing"]) + 1), (current_calendar_y + line_height), 'black') + (loop_date_time_width + (2 * infowindow_opts["cell_spacing"]) + 1), + (current_calendar_y + line_height), 'black') # draw event date iw.text((infowindow_opts["cell_spacing"]) + x_min, diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index c566bd2..9410c0d 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -55,7 +55,7 @@ def list(self): events[start] = event - # 2019-11-05T10:00:00-08:00 + day_start_ts_now = dt.timestamp(dt.now().replace(hour=0, minute=0, second=0, microsecond=0)) for event_key in sorted(events.keys()): start = events[event_key]['start'].get('dateTime', events[event_key]['start'].get('date')) @@ -72,11 +72,14 @@ def list(self): st_date = dt.strftime(dtparse(start), format='%d.%m') st_time = dt.strftime(dtparse(start), format='%H:%M') + event_start_ts_now = dt.timestamp(dtparse(start).replace(hour=0, minute=0, second=0, microsecond=0)) + items.append({ "date": st_date, "time": st_time, "content": events[event_key]['summary'], - "today": today + "today": today, + "weeks_away": (event_start_ts_now - day_start_ts_now) // 604800 }) return items From 14e4b781f1a049c6f545fd814f9e63b99914589e Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 8 May 2023 10:05:53 +0200 Subject: [PATCH 066/113] increase "weeks_away" red line thickness --- infowindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infowindow.py b/infowindow.py index 30fbffc..5b32191 100755 --- a/infowindow.py +++ b/infowindow.py @@ -171,7 +171,7 @@ def render_calendar(x_min, x_max, loop_start=0): if current_weeks_away != cal_item['weeks_away']: # override the black line with red since the "weeks away" number changed current_weeks_away = cal_item['weeks_away'] - iw.line(x_min, (current_calendar_y - 1), x_max, (current_calendar_y - 1), 'red') + iw.line(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') iw.line(x_min, (current_calendar_y + line_height + 1), x_max, (current_calendar_y + line_height + 1), 'black') From 212519b03bd1c7b123bef80aed230b6e74c547ff Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 8 May 2023 10:07:55 +0200 Subject: [PATCH 067/113] increase "weeks_away" red line thickness --- infowindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infowindow.py b/infowindow.py index 5b32191..332d6ac 100755 --- a/infowindow.py +++ b/infowindow.py @@ -171,7 +171,7 @@ def render_calendar(x_min, x_max, loop_start=0): if current_weeks_away != cal_item['weeks_away']: # override the black line with red since the "weeks away" number changed current_weeks_away = cal_item['weeks_away'] - iw.line(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') + iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') iw.line(x_min, (current_calendar_y + line_height + 1), x_max, (current_calendar_y + line_height + 1), 'black') From 8dae153629edd80f2546c5507e8a2b0c75a14ae7 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 18 Sep 2023 13:45:53 +0200 Subject: [PATCH 068/113] fetch more events since we now have 2 columns --- README.md | 1 + mod_calendar/mod_google.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 51ebd90..e12a76f 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ 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! +*Please be aware that this version is built for the v2 version of the e-ink screen!* 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 diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 9410c0d..ec6828b 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -39,7 +39,7 @@ def list(self): for id in calendar_ids: result = service.events().list(calendarId=id, timeMin=now, - maxResults=20, + maxResults=30, singleEvents=True, orderBy='startTime').execute() From 4b3057c89cb6ce18ad851acc6b3b40c428c9d6b5 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 18 Sep 2023 13:49:13 +0200 Subject: [PATCH 069/113] fetch more cal items since we now have 2 columns --- mod_calendar/mod_google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 9410c0d..ec6828b 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -39,7 +39,7 @@ def list(self): for id in calendar_ids: result = service.events().list(calendarId=id, timeMin=now, - maxResults=20, + maxResults=30, singleEvents=True, orderBy='startTime').execute() From ad3b88b3d23549ee556e83cbd467dd01ce53046b Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Nov 2023 01:58:29 +0100 Subject: [PATCH 070/113] add more divider line styles for calendar --- config.json-sample | 1 + infowindow.py | 35 +++++++++++++++++++++++++++++++---- mod_calendar/mod_google.py | 20 ++++++++++++++------ 3 files changed, 46 insertions(+), 10 deletions(-) diff --git a/config.json-sample b/config.json-sample index 7742349..beaf8f4 100644 --- a/config.json-sample +++ b/config.json-sample @@ -2,6 +2,7 @@ "general": { "rotation": 180, "timeformat": "12h", + "sunday_first_dow": false, "cell_spacing": 2 }, "todo": { diff --git a/infowindow.py b/infowindow.py index 332d6ac..ed4fd2a 100755 --- a/infowindow.py +++ b/infowindow.py @@ -32,6 +32,7 @@ infowindow_opts = {} # give the timeformat to all the modules needing it calendar_opts["timeformat"] = config_data["general"]["timeformat"] +calendar_opts["sunday_first_dow"] = config_data["general"]["sunday_first_dow"] weather_opts["timeformat"] = config_data["general"]["timeformat"] infowindow_opts["timeformat"] = config_data["general"]["timeformat"] infowindow_opts["cell_spacing"] = config_data["general"]["cell_spacing"] @@ -150,7 +151,9 @@ def main(): def render_calendar(x_min, x_max, loop_start=0): current_index = 0 current_calendar_y = 92 + current_days_away = -1 current_weeks_away = -1 + current_week = -1 loop_date_time_width = x_min + date_time_width current_font = calendar_entry_font @@ -163,16 +166,40 @@ def render_calendar(x_min, x_max, loop_start=0): x_max, (current_calendar_y + line_height), calendar_opts['today_background_color']) - # draw horizontal line - # on first run, set current_weeks_away to first event weeks + # draw horizontal line(s) at the top + # on first run, initialize several vars with the first values from the first event + if current_days_away < 0: + current_days_away = cal_item['days_away'] if current_weeks_away < 0: current_weeks_away = cal_item['weeks_away'] - + if current_week < 0: + current_week = cal_item['week'] + + # per default, draw a dashed line (same day event) + for x in range(x_min, x_max, 8): + iw.line(x, (current_calendar_y + line_height + 1), x+4, (current_calendar_y + line_height + 1), + 'black') + # iw.line(x+5, (current_calendar_y + line_height + 1), x+8, (current_calendar_y + line_height + 1), + # 'white') + + # override the dotted line with a black line since the "days away" number changed + if current_days_away != cal_item['days_away']: + current_days_away = cal_item['days_away'] + iw.line(x_min, (current_calendar_y + line_height + 1), x_max, (current_calendar_y + line_height + 1), + 'black') + + # override the dotted line with a black rectangle ("thicker line") since the week changed number changed + if current_week != cal_item['week']: + current_week = cal_item['week'] + iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'black') + + # override the black line with a red a rectangle ("thicker line") the "weeks away" number changed if current_weeks_away != cal_item['weeks_away']: - # override the black line with red since the "weeks away" number changed current_weeks_away = cal_item['weeks_away'] iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') + # draw ending horizontal line, just to ensure that the last element is not hanging in the air + # this gets overridden almost all the time iw.line(x_min, (current_calendar_y + line_height + 1), x_max, (current_calendar_y + line_height + 1), 'black') diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index ec6828b..ea29968 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -15,6 +15,7 @@ def __init__(self, options): self.timeformat = options["timeformat"] self.additional = options["additional"] self.ignored = options["ignored"] + self.sunday_first_dow = options["sunday_first_dow"] def list(self): calendar_ids = [] @@ -59,18 +60,23 @@ def list(self): for event_key in sorted(events.keys()): start = events[event_key]['start'].get('dateTime', events[event_key]['start'].get('date')) - if int(dt.strftime(dtparse(start), format='%Y%m%d')) <= int(dt.strftime(dt.today(), format='%Y%m%d')): + if int(dt.strftime(dtparse(start), '%Y%m%d')) <= int(dt.strftime(dt.today(), '%Y%m%d')): today = True else: today = False # Sunrise and Sunset. if self.timeformat == "12h": - st_date = dt.strftime(dtparse(start), format='%m-%d') - st_time = dt.strftime(dtparse(start), format='%I:%M %p') + st_date = dt.strftime(dtparse(start), '%m-%d') + st_time = dt.strftime(dtparse(start), '%I:%M %p') else: - st_date = dt.strftime(dtparse(start), format='%d.%m') - st_time = dt.strftime(dtparse(start), format='%H:%M') + st_date = dt.strftime(dtparse(start), '%d.%m') + st_time = dt.strftime(dtparse(start), '%H:%M') + + if self.sunday_first_dow: + week = dt.strftime(dtparse(start), '%U') + else: + week = dt.strftime(dtparse(start), '%W') event_start_ts_now = dt.timestamp(dtparse(start).replace(hour=0, minute=0, second=0, microsecond=0)) @@ -79,7 +85,9 @@ def list(self): "time": st_time, "content": events[event_key]['summary'], "today": today, - "weeks_away": (event_start_ts_now - day_start_ts_now) // 604800 + "week": week, + "days_away": (event_start_ts_now - day_start_ts_now) // 86400, # days away + "weeks_away": (event_start_ts_now - day_start_ts_now) // 604800 # weeks away }) return items From 6d2d2180511a235bf702980f68a3dbbf1f4a9ca7 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Nov 2023 02:00:03 +0100 Subject: [PATCH 071/113] add more divider line styles for calendar --- mod_calendar/mod_google.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index ea29968..d2e59b3 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -85,7 +85,7 @@ def list(self): "time": st_time, "content": events[event_key]['summary'], "today": today, - "week": week, + "week": int(week), "days_away": (event_start_ts_now - day_start_ts_now) // 86400, # days away "weeks_away": (event_start_ts_now - day_start_ts_now) // 604800 # weeks away }) From 5dfd8399bad9ca57ee75dbc68c6d2d4a879f24ec Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Nov 2023 02:01:21 +0100 Subject: [PATCH 072/113] add more divider line styles for calendar --- infowindow.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/infowindow.py b/infowindow.py index ed4fd2a..afbd85b 100755 --- a/infowindow.py +++ b/infowindow.py @@ -179,8 +179,8 @@ def render_calendar(x_min, x_max, loop_start=0): for x in range(x_min, x_max, 8): iw.line(x, (current_calendar_y + line_height + 1), x+4, (current_calendar_y + line_height + 1), 'black') - # iw.line(x+5, (current_calendar_y + line_height + 1), x+8, (current_calendar_y + line_height + 1), - # 'white') + iw.line(x+5, (current_calendar_y + line_height + 1), x+8, (current_calendar_y + line_height + 1), + 'white') # override the dotted line with a black line since the "days away" number changed if current_days_away != cal_item['days_away']: From 1397c069f69a35baa5bb2e3193ff5e54ab906d7e Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Nov 2023 02:11:35 +0100 Subject: [PATCH 073/113] add more divider line styles for calendar --- infowindow.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index afbd85b..45b4f1d 100755 --- a/infowindow.py +++ b/infowindow.py @@ -176,26 +176,30 @@ def render_calendar(x_min, x_max, loop_start=0): current_week = cal_item['week'] # per default, draw a dashed line (same day event) + divider_str = "same day" for x in range(x_min, x_max, 8): - iw.line(x, (current_calendar_y + line_height + 1), x+4, (current_calendar_y + line_height + 1), + iw.line(x, (current_calendar_y + line_height + 1), x+3, (current_calendar_y + line_height + 1), 'black') - iw.line(x+5, (current_calendar_y + line_height + 1), x+8, (current_calendar_y + line_height + 1), + iw.line(x+4, (current_calendar_y + line_height + 1), x+7, (current_calendar_y + line_height + 1), 'white') # override the dotted line with a black line since the "days away" number changed if current_days_away != cal_item['days_away']: current_days_away = cal_item['days_away'] + divider_str = "new day" iw.line(x_min, (current_calendar_y + line_height + 1), x_max, (current_calendar_y + line_height + 1), 'black') # override the dotted line with a black rectangle ("thicker line") since the week changed number changed if current_week != cal_item['week']: current_week = cal_item['week'] + divider_str = "new week day" iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'black') # override the black line with a red a rectangle ("thicker line") the "weeks away" number changed if current_weeks_away != cal_item['weeks_away']: current_weeks_away = cal_item['weeks_away'] + divider_str = "in one week" iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') # draw ending horizontal line, just to ensure that the last element is not hanging in the air @@ -227,7 +231,7 @@ def render_calendar(x_min, x_max, loop_start=0): # set new line height for next round current_calendar_y = (current_calendar_y + line_height + 2) # logging.debug("ITEM: "+str(cal_item['date']), str(cal_item['time']), str(cal_item['content'])) - logging.debug("ITEM: %s" % cal_item['content'].strip()) + logging.debug("ITEM (%s): %s" % (divider_str, cal_item['content'].strip())) current_index = cal_items.index(cal_item) if current_calendar_y > 480: logging.debug("Max height detected, breaking loop") From 8e9154aa20af96a521e69ee7dac27a28ccdb61ad Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Nov 2023 02:16:01 +0100 Subject: [PATCH 074/113] add more divider line styles for calendar --- infowindow.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/infowindow.py b/infowindow.py index 45b4f1d..0627039 100755 --- a/infowindow.py +++ b/infowindow.py @@ -178,17 +178,14 @@ def render_calendar(x_min, x_max, loop_start=0): # per default, draw a dashed line (same day event) divider_str = "same day" for x in range(x_min, x_max, 8): - iw.line(x, (current_calendar_y + line_height + 1), x+3, (current_calendar_y + line_height + 1), - 'black') - iw.line(x+4, (current_calendar_y + line_height + 1), x+7, (current_calendar_y + line_height + 1), - 'white') + iw.line(x, current_calendar_y, x+3, current_calendar_y, 'black') + iw.line(x+4, current_calendar_y, x+7, current_calendar_y, 'white') # override the dotted line with a black line since the "days away" number changed if current_days_away != cal_item['days_away']: current_days_away = cal_item['days_away'] divider_str = "new day" - iw.line(x_min, (current_calendar_y + line_height + 1), x_max, (current_calendar_y + line_height + 1), - 'black') + iw.line(x_min, current_calendar_y, x_max, current_calendar_y, 'black') # override the dotted line with a black rectangle ("thicker line") since the week changed number changed if current_week != cal_item['week']: From 121bc2fd80ccff5d5d83fedfc7fcc6408ec2f0da Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Nov 2023 02:23:30 +0100 Subject: [PATCH 075/113] add more divider line styles for calendar --- infowindow.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/infowindow.py b/infowindow.py index 0627039..4c097de 100755 --- a/infowindow.py +++ b/infowindow.py @@ -155,6 +155,7 @@ def render_calendar(x_min, x_max, loop_start=0): current_weeks_away = -1 current_week = -1 loop_date_time_width = x_min + date_time_width + first_loop = True current_font = calendar_entry_font for cal_item in cal_items[loop_start:]: @@ -175,11 +176,15 @@ def render_calendar(x_min, x_max, loop_start=0): if current_week < 0: current_week = cal_item['week'] - # per default, draw a dashed line (same day event) - divider_str = "same day" - for x in range(x_min, x_max, 8): - iw.line(x, current_calendar_y, x+3, current_calendar_y, 'black') - iw.line(x+4, current_calendar_y, x+7, current_calendar_y, 'white') + if first_loop: + # don't draw a line at the top of the first event + first_loop = False + else: + # per default, draw a dashed line (same day event) + divider_str = "same day" + for x in range(x_min, x_max, 8): + iw.line(x, current_calendar_y, x+3, current_calendar_y, 'black') + iw.line(x+4, current_calendar_y, x+7, current_calendar_y, 'white') # override the dotted line with a black line since the "days away" number changed if current_days_away != cal_item['days_away']: @@ -201,7 +206,7 @@ def render_calendar(x_min, x_max, loop_start=0): # draw ending horizontal line, just to ensure that the last element is not hanging in the air # this gets overridden almost all the time - iw.line(x_min, (current_calendar_y + line_height + 1), x_max, (current_calendar_y + line_height + 1), + iw.line(x_min, (current_calendar_y + line_height + 2), x_max, (current_calendar_y + line_height + 2), 'black') # draw vertical line From e5e7e98c5db5fdc062731e6de2341c7e471d4de8 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Nov 2023 02:24:08 +0100 Subject: [PATCH 076/113] add more divider line styles for calendar --- infowindow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/infowindow.py b/infowindow.py index 4c097de..e512f7b 100755 --- a/infowindow.py +++ b/infowindow.py @@ -179,6 +179,7 @@ def render_calendar(x_min, x_max, loop_start=0): if first_loop: # don't draw a line at the top of the first event first_loop = False + divider_str = "initial element" else: # per default, draw a dashed line (same day event) divider_str = "same day" From 3441e7242ed71ff7a65c58bc9ee64a7d4835ebeb Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Nov 2023 11:21:20 +0100 Subject: [PATCH 077/113] add more divider line styles for calendar --- infowindow.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index e512f7b..982a082 100755 --- a/infowindow.py +++ b/infowindow.py @@ -199,11 +199,20 @@ def render_calendar(x_min, x_max, loop_start=0): divider_str = "new week day" iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'black') - # override the black line with a red a rectangle ("thicker line") the "weeks away" number changed + # check if the new event is a week away (the "weeks away" number changed) if current_weeks_away != cal_item['weeks_away']: + # override the black line with a red a rectangle ("thicker line") the "weeks away" number changed current_weeks_away = cal_item['weeks_away'] - divider_str = "in one week" - iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') + + # decide on style depending on what the option above was + if current_week != cal_item['week']: + divider_str = "in one week and the week changes" + for x in range(x_min, x_max, 8): + iw.rectangle(x, (current_calendar_y - 1), x + 3, current_calendar_y, 'black') + iw.rectangle(x + 4, (current_calendar_y - 1), x + 7, current_calendar_y, 'red') + else: + divider_str = "in one week" + iw.line(x_min, (current_calendar_y - 1), x_max, (current_calendar_y - 1), 'red') # draw ending horizontal line, just to ensure that the last element is not hanging in the air # this gets overridden almost all the time From bc7721d528919a3a07c7f392219cf9d51a280ae2 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Nov 2023 11:23:23 +0100 Subject: [PATCH 078/113] add more divider line styles for calendar --- infowindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infowindow.py b/infowindow.py index 982a082..f5b4c4b 100755 --- a/infowindow.py +++ b/infowindow.py @@ -212,7 +212,7 @@ def render_calendar(x_min, x_max, loop_start=0): iw.rectangle(x + 4, (current_calendar_y - 1), x + 7, current_calendar_y, 'red') else: divider_str = "in one week" - iw.line(x_min, (current_calendar_y - 1), x_max, (current_calendar_y - 1), 'red') + iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') # draw ending horizontal line, just to ensure that the last element is not hanging in the air # this gets overridden almost all the time From 25ac0f1522716604306c09a20545304339e932d0 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 3 Nov 2023 11:25:47 +0100 Subject: [PATCH 079/113] add more divider line styles for calendar --- infowindow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/infowindow.py b/infowindow.py index f5b4c4b..8fc9021 100755 --- a/infowindow.py +++ b/infowindow.py @@ -212,7 +212,8 @@ def render_calendar(x_min, x_max, loop_start=0): iw.rectangle(x + 4, (current_calendar_y - 1), x + 7, current_calendar_y, 'red') else: divider_str = "in one week" - iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') + # iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') + iw.line(x_min, current_calendar_y, x_max, current_calendar_y, 'red') # draw ending horizontal line, just to ensure that the last element is not hanging in the air # this gets overridden almost all the time From 2ddef7615251d6257d908c3c46a15a58a0d97539 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 5 Nov 2023 17:46:28 +0100 Subject: [PATCH 080/113] add more divider line styles for calendar --- infowindow.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/infowindow.py b/infowindow.py index 8fc9021..904ac72 100755 --- a/infowindow.py +++ b/infowindow.py @@ -156,10 +156,13 @@ def render_calendar(x_min, x_max, loop_start=0): current_week = -1 loop_date_time_width = x_min + date_time_width first_loop = True + new_week = False current_font = calendar_entry_font for cal_item in cal_items[loop_start:]: font_color = 'black' + new_week = False + if cal_item['today']: current_font = calendar_entry_font_highlited font_color = calendar_opts['today_text_color'] @@ -197,6 +200,7 @@ def render_calendar(x_min, x_max, loop_start=0): if current_week != cal_item['week']: current_week = cal_item['week'] divider_str = "new week day" + new_week = True iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'black') # check if the new event is a week away (the "weeks away" number changed) @@ -205,7 +209,7 @@ def render_calendar(x_min, x_max, loop_start=0): current_weeks_away = cal_item['weeks_away'] # decide on style depending on what the option above was - if current_week != cal_item['week']: + if new_week: divider_str = "in one week and the week changes" for x in range(x_min, x_max, 8): iw.rectangle(x, (current_calendar_y - 1), x + 3, current_calendar_y, 'black') From 0f65cf4b4e664849c4ffb477b397a76fda41d893 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 5 Nov 2023 17:48:27 +0100 Subject: [PATCH 081/113] add more divider line styles for calendar --- infowindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index 904ac72..80a104a 100755 --- a/infowindow.py +++ b/infowindow.py @@ -211,9 +211,9 @@ def render_calendar(x_min, x_max, loop_start=0): # decide on style depending on what the option above was if new_week: divider_str = "in one week and the week changes" - for x in range(x_min, x_max, 8): - iw.rectangle(x, (current_calendar_y - 1), x + 3, current_calendar_y, 'black') - iw.rectangle(x + 4, (current_calendar_y - 1), x + 7, current_calendar_y, 'red') + for x in range(x_min, x_max, 16): + iw.rectangle(x, (current_calendar_y - 1), x + 7, current_calendar_y, 'black') + iw.rectangle(x + 8, (current_calendar_y - 1), x + 15, current_calendar_y, 'red') else: divider_str = "in one week" # iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') From f9a8fbe6397d137fde4acd34d41a1da9335d02b5 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 5 Nov 2023 17:49:24 +0100 Subject: [PATCH 082/113] add more divider line styles for calendar --- infowindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index 80a104a..8887db6 100755 --- a/infowindow.py +++ b/infowindow.py @@ -211,9 +211,9 @@ def render_calendar(x_min, x_max, loop_start=0): # decide on style depending on what the option above was if new_week: divider_str = "in one week and the week changes" - for x in range(x_min, x_max, 16): - iw.rectangle(x, (current_calendar_y - 1), x + 7, current_calendar_y, 'black') - iw.rectangle(x + 8, (current_calendar_y - 1), x + 15, current_calendar_y, 'red') + for x in range(x_min, x_max, 50): + iw.rectangle(x, (current_calendar_y - 1), x + 24, current_calendar_y, 'black') + iw.rectangle(x + 25, (current_calendar_y - 1), x + 49, current_calendar_y, 'red') else: divider_str = "in one week" # iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') From 97b6343680258651cc5d6e6cd94a55422098df0f Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 5 Nov 2023 17:52:55 +0100 Subject: [PATCH 083/113] add more divider line styles for calendar --- infowindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index 8887db6..86225d7 100755 --- a/infowindow.py +++ b/infowindow.py @@ -211,9 +211,9 @@ def render_calendar(x_min, x_max, loop_start=0): # decide on style depending on what the option above was if new_week: divider_str = "in one week and the week changes" - for x in range(x_min, x_max, 50): - iw.rectangle(x, (current_calendar_y - 1), x + 24, current_calendar_y, 'black') - iw.rectangle(x + 25, (current_calendar_y - 1), x + 49, current_calendar_y, 'red') + for x in range(x_min, x_max, 32): + iw.rectangle(x, (current_calendar_y - 1), x + 15, current_calendar_y, 'black') + iw.rectangle(x + 16, (current_calendar_y - 1), x + 31, current_calendar_y, 'red') else: divider_str = "in one week" # iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') From d5cb5cbb861419bfea88bf83a454942a1051d5f8 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 5 Nov 2023 17:55:10 +0100 Subject: [PATCH 084/113] add more divider line styles for calendar --- infowindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index 86225d7..7701c0c 100755 --- a/infowindow.py +++ b/infowindow.py @@ -211,9 +211,9 @@ def render_calendar(x_min, x_max, loop_start=0): # decide on style depending on what the option above was if new_week: divider_str = "in one week and the week changes" - for x in range(x_min, x_max, 32): - iw.rectangle(x, (current_calendar_y - 1), x + 15, current_calendar_y, 'black') - iw.rectangle(x + 16, (current_calendar_y - 1), x + 31, current_calendar_y, 'red') + for x in range(x_min, x_max, 46): + iw.rectangle(x, (current_calendar_y - 1), x + 22, current_calendar_y, 'black') + iw.rectangle(x + 23, (current_calendar_y - 1), x + 45, current_calendar_y, 'red') else: divider_str = "in one week" # iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') From 1ed278dadb3f2115af20c4ee41f75c011de61891 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 5 Nov 2023 18:00:25 +0100 Subject: [PATCH 085/113] add more divider line styles for calendar --- infowindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index 7701c0c..b5fbbe6 100755 --- a/infowindow.py +++ b/infowindow.py @@ -211,9 +211,9 @@ def render_calendar(x_min, x_max, loop_start=0): # decide on style depending on what the option above was if new_week: divider_str = "in one week and the week changes" - for x in range(x_min, x_max, 46): - iw.rectangle(x, (current_calendar_y - 1), x + 22, current_calendar_y, 'black') - iw.rectangle(x + 23, (current_calendar_y - 1), x + 45, current_calendar_y, 'red') + for x in range(x_min, x_max, 167): + iw.rectangle(x, (current_calendar_y - 1), x + 83, current_calendar_y, 'black') + iw.rectangle(x + 84, (current_calendar_y - 1), x + 166, current_calendar_y, 'red') else: divider_str = "in one week" # iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') From 6c5f5fa15a58b1003476995f97e6b6186d534c6b Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 5 Nov 2023 18:02:59 +0100 Subject: [PATCH 086/113] add more divider line styles for calendar --- infowindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index b5fbbe6..021fef5 100755 --- a/infowindow.py +++ b/infowindow.py @@ -211,9 +211,9 @@ def render_calendar(x_min, x_max, loop_start=0): # decide on style depending on what the option above was if new_week: divider_str = "in one week and the week changes" - for x in range(x_min, x_max, 167): - iw.rectangle(x, (current_calendar_y - 1), x + 83, current_calendar_y, 'black') - iw.rectangle(x + 84, (current_calendar_y - 1), x + 166, current_calendar_y, 'red') + for x in range(x_min, x_max, 23): + iw.rectangle(x, (current_calendar_y - 1), x + 11, current_calendar_y, 'black') + iw.rectangle(x + 12, (current_calendar_y - 1), x + 23, current_calendar_y, 'red') else: divider_str = "in one week" # iw.rectangle(x_min, (current_calendar_y - 1), x_max, current_calendar_y, 'red') From ada1bfaabb50b6ba4e598824388491b10375c9db Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 5 Nov 2023 18:04:26 +0100 Subject: [PATCH 087/113] add more divider line styles for calendar --- infowindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index 021fef5..3b7e593 100755 --- a/infowindow.py +++ b/infowindow.py @@ -186,9 +186,9 @@ def render_calendar(x_min, x_max, loop_start=0): else: # per default, draw a dashed line (same day event) divider_str = "same day" - for x in range(x_min, x_max, 8): - iw.line(x, current_calendar_y, x+3, current_calendar_y, 'black') - iw.line(x+4, current_calendar_y, x+7, current_calendar_y, 'white') + for x in range(x_min, x_max, 23): + iw.line(x, current_calendar_y, x + 11, current_calendar_y, 'black') + iw.line(x + 12, current_calendar_y, x + 23, current_calendar_y, 'white') # override the dotted line with a black line since the "days away" number changed if current_days_away != cal_item['days_away']: From 854f44c2fab5abe54cb9a8c11b985f8dd212d349 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 5 Nov 2023 18:36:57 +0100 Subject: [PATCH 088/113] add more divider line styles for calendar --- infowindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index 3b7e593..a1416b2 100755 --- a/infowindow.py +++ b/infowindow.py @@ -186,9 +186,9 @@ def render_calendar(x_min, x_max, loop_start=0): else: # per default, draw a dashed line (same day event) divider_str = "same day" - for x in range(x_min, x_max, 23): - iw.line(x, current_calendar_y, x + 11, current_calendar_y, 'black') - iw.line(x + 12, current_calendar_y, x + 23, current_calendar_y, 'white') + for x in range(x_min, x_max, 17): + iw.line(x, current_calendar_y, x + 8, current_calendar_y, 'black') + iw.line(x + 9, current_calendar_y, x + 16, current_calendar_y, 'white') # override the dotted line with a black line since the "days away" number changed if current_days_away != cal_item['days_away']: From 4c11a8f45562618c6e58d584ced89546251c3752 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 5 Nov 2023 18:39:58 +0100 Subject: [PATCH 089/113] add more divider line styles for calendar --- infowindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infowindow.py b/infowindow.py index a1416b2..163dd57 100755 --- a/infowindow.py +++ b/infowindow.py @@ -186,9 +186,9 @@ def render_calendar(x_min, x_max, loop_start=0): else: # per default, draw a dashed line (same day event) divider_str = "same day" - for x in range(x_min, x_max, 17): - iw.line(x, current_calendar_y, x + 8, current_calendar_y, 'black') - iw.line(x + 9, current_calendar_y, x + 16, current_calendar_y, 'white') + for x in range(x_min, x_max, 8): + iw.line(x, current_calendar_y, x + 3, current_calendar_y, 'black') + iw.line(x + 4, current_calendar_y, x + 7, current_calendar_y, 'white') # override the dotted line with a black line since the "days away" number changed if current_days_away != cal_item['days_away']: From c6f4f04e2bdce84d0ad5f22bc86f67379295d675 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 11 Dec 2023 16:42:20 +0100 Subject: [PATCH 090/113] Working on deprecation stuff --- README.md | 12 ++- driver/__init__.py | 0 driver/epd7in5b_V2.py | 192 ----------------------------------- driver/epdif.py | 63 ------------ mod_infowindow/infowindow.py | 6 +- requirements.txt | 3 +- 6 files changed, 15 insertions(+), 261 deletions(-) delete mode 100644 driver/__init__.py delete mode 100644 driver/epd7in5b_V2.py delete mode 100644 driver/epdif.py diff --git a/README.md b/README.md index e12a76f..c849283 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ to the data. In your face reminder. Activate SPI on your Raspberry Pi by using the `raspi-config` tool under Interface Options and reboot. Also for some RaspiOS versions, you have to install the `libopenjp2-7` package: -``` +```bash sudo apt-get install libopenjp2-7 ``` @@ -39,10 +39,18 @@ sudo apt-get install libopenjp2-7 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` +### Clone the e-Paper driver from waveshare +Waveshare sometimes changes things in this driver. So this setup might need some updates, be aware! +```bash +cd /home/pi +git clone https://github.com/waveshareteam/e-Paper.git +ln -s /home/pi/e-Paper/RaspberryPi_JetsonNano/python/lib/waveshare_epd/ /home/pi/InfoWindow/driver +``` + ### Setup python modules Run the following commands to install the requirements. I stuck to basic standard modules for ease of installation. -``` +```bash cd /home/pi/InfoWindow export CFLAGS=-fcommon sudo apt install python3-dev diff --git a/driver/__init__.py b/driver/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/driver/epd7in5b_V2.py b/driver/epd7in5b_V2.py deleted file mode 100644 index df09ae3..0000000 --- a/driver/epd7in5b_V2.py +++ /dev/null @@ -1,192 +0,0 @@ -# ***************************************************************************** -# * | File : epd7in5b_V2.py -# * | Author : Waveshare team -# * | Function : Electronic paper driver -# * | Info : -# *---------------- -# * | This version: V4.2 -# * | Date : 2022-01-08 -# # | 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 = 800 -EPD_HEIGHT = 480 - -logger = logging.getLogger(__name__) - -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 send_data2(self, data): #faster - 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): - logger.debug("e-Paper busy") - self.send_command(0x71) - busy = epdconfig.digital_read(self.busy_pin) - while(busy == 0): - self.send_command(0x71) - busy = epdconfig.digital_read(self.busy_pin) - epdconfig.delay_ms(200) - logger.debug("e-Paper busy release") - - def init(self): - if (epdconfig.module_init() != 0): - return -1 - - self.reset() - - # self.send_command(0x06) # btst - # self.send_data(0x17) - # self.send_data(0x17) - # self.send_data(0x38) # If an exception is displayed, try using 0x38 - # self.send_data(0x17) - - self.send_command(0x01); #POWER SETTING - self.send_data(0x07); - self.send_data(0x07); #VGH=20V,VGL=-20V - self.send_data(0x3f); #VDH=15V - self.send_data(0x3f); #VDL=-15V - - self.send_command(0x04); #POWER ON - epdconfig.delay_ms(100); - self.ReadBusy(); - - self.send_command(0X00); #PANNEL SETTING - self.send_data(0x0F); #KW-3f KWR-2F BWROTP 0f BWOTP 1f - - self.send_command(0x61); #tres - self.send_data(0x03); #source 800 - self.send_data(0x20); - self.send_data(0x01); #gate 480 - self.send_data(0xE0); - - self.send_command(0X15); - self.send_data(0x00); - - self.send_command(0X50); #VCOM AND DATA INTERVAL SETTING - self.send_data(0x11); - self.send_data(0x07); - - self.send_command(0X60); #TCON SETTING - self.send_data(0x22); - - self.send_command(0x65); - self.send_data(0x00); - self.send_data(0x00); - self.send_data(0x00); - self.send_data(0x00); - - 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): - # image has correct dimensions, but needs to be rotated - img = img.rotate(90, expand=True).convert('1') - else: - logger.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')) - # The bytes need to be inverted, because in the PIL world 0=black and 1=white, but - # in the e-paper world 0=white and 1=black. - for i in range(len(buf)): - buf[i] ^= 0xFF - return buf - - def display(self, imageblack, imagered): - self.send_command(0x10) - # The black bytes need to be inverted back from what getbuffer did - for i in range(len(imageblack)): - imageblack[i] ^= 0xFF - self.send_data2(imageblack) - - self.send_command(0x13) - self.send_data2(imagered) - - self.send_command(0x12) - epdconfig.delay_ms(100) - self.ReadBusy() - - def Clear(self): - buf = [0x00] * (int(self.width/8) * self.height) - buf2 = [0xff] * (int(self.width/8) * self.height) - self.send_command(0x10) - self.send_data2(buf2) - - self.send_command(0x13) - self.send_data2(buf) - - self.send_command(0x12) - 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) - - epdconfig.delay_ms(2000) - epdconfig.module_exit() -### END OF FILE ### - diff --git a/driver/epdif.py b/driver/epdif.py deleted file mode 100644 index d158589..0000000 --- a/driver/epdif.py +++ /dev/null @@ -1,63 +0,0 @@ -## - # @filename : epdif.py - # @brief : EPD hardware interface implements (GPIO, SPI) - # @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 spidev -import RPi.GPIO as GPIO -import time - -# Pin definition -RST_PIN = 17 -DC_PIN = 25 -CS_PIN = 8 -BUSY_PIN = 24 - -# SPI device, bus = 0, device = 0 -SPI = spidev.SpiDev(0, 0) - -def epd_digital_write(pin, value): - GPIO.output(pin, value) - -def epd_digital_read(pin): - return GPIO.input(pin) - -def epd_delay_ms(delaytime): - time.sleep(delaytime / 1000.0) - -def spi_transfer(data): - SPI.writebytes(data) - -def epd_init(): - GPIO.setmode(GPIO.BCM) - GPIO.setwarnings(False) - GPIO.setup(RST_PIN, GPIO.OUT) - GPIO.setup(DC_PIN, GPIO.OUT) - GPIO.setup(CS_PIN, GPIO.OUT) - GPIO.setup(BUSY_PIN, GPIO.IN) - SPI.max_speed_hz = 2000000 - SPI.mode = 0b00 - return 0 - -### END OF FILE ### diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index 1895fbb..7328ce1 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -50,17 +50,17 @@ def text(self, left, top, text, font, fill): if fill == 'black': font = self.fonts[font] self.black_draw.text((left, top), text, font=font, fill=0) - return self.black_draw.textsize(text, font=font) + #return self.black_draw.textsize(text, font=font) elif fill == 'red': font = self.fonts[font] self.black_draw.text((left, top), text, font=font, fill=0) self.red_draw.text((left, top), text, font=font, fill=0) - return self.red_draw.textsize(text, font=font) + #return self.red_draw.textsize(text, font=font) elif fill == 'white': font = self.fonts[font] self.red_draw.text((left, top), text, font=font, fill=1) self.black_draw.text((left, top), text, font=font, fill=1) - return self.red_draw.textsize(text, font=font) + #return self.red_draw.textsize(text, font=font) def rotate(self, angle): self.red_image.rotate(angle) diff --git a/requirements.txt b/requirements.txt index 9f877d8..d1f2644 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,5 @@ google-auth-oauthlib python-dateutil requests todoist-python -RPi.GPIO \ No newline at end of file +RPi.GPIO +gpiozero From 23bc1f9e10a3d8e9b9e252026d56a5b375e4f906 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 11 Dec 2023 16:43:07 +0100 Subject: [PATCH 091/113] Also ignore the driver directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 32e2431..7276169 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ config.json infowindow.jpg venv/ .idea/ +driver/" From 2ba66019c2af1fe72c67566dcaf3df301c499294 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 11 Dec 2023 16:44:57 +0100 Subject: [PATCH 092/113] Also ignore the driver directory --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7276169..3251187 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,4 @@ config.json infowindow.jpg venv/ .idea/ -driver/" +driver From 0b5c5fb2b9e6d20624f6bb1e285ace7940b7d990 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 11 Dec 2023 16:48:14 +0100 Subject: [PATCH 093/113] Working on Pillow 10 --- mod_infowindow/infowindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index 7328ce1..63e3513 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -90,7 +90,7 @@ def initFonts(self): def truncate(self, string, font, max_size): num_chars = len(string) for char in string: - (np_x, np_y) = self.getFont(font).getsize(string) + (a, b, np_x, np_y) = self.getFont(font).getbbox(string) if np_x >= max_size: string = string[:-1] From 1e16d5c484f8557c5163acf1b749e9ef709baec5 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Mon, 11 Dec 2023 16:58:05 +0100 Subject: [PATCH 094/113] Pillow deprication cleanup --- README.md | 5 ++--- mod_infowindow/infowindow.py | 7 +------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index c849283..c2a3ec7 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,9 @@ Clone this repo onto your raspberry pi. Does not really matter where it is, but directory: `/home/pi/InfoWindow` ### Clone the e-Paper driver from waveshare -Waveshare sometimes changes things in this driver. So this setup might need some updates, be aware! +Waveshare sometimes changes things in their driver. So this part might need some changes, be aware! ```bash -cd /home/pi -git clone https://github.com/waveshareteam/e-Paper.git +git clone https://github.com/waveshareteam/e-Paper.git /home/pi/e-Paper ln -s /home/pi/e-Paper/RaspberryPi_JetsonNano/python/lib/waveshare_epd/ /home/pi/InfoWindow/driver ``` diff --git a/mod_infowindow/infowindow.py b/mod_infowindow/infowindow.py index 63e3513..8a6e9fb 100644 --- a/mod_infowindow/infowindow.py +++ b/mod_infowindow/infowindow.py @@ -50,17 +50,14 @@ def text(self, left, top, text, font, fill): if fill == 'black': font = self.fonts[font] self.black_draw.text((left, top), text, font=font, fill=0) - #return self.black_draw.textsize(text, font=font) elif fill == 'red': font = self.fonts[font] self.black_draw.text((left, top), text, font=font, fill=0) self.red_draw.text((left, top), text, font=font, fill=0) - #return self.red_draw.textsize(text, font=font) elif fill == 'white': font = self.fonts[font] self.red_draw.text((left, top), text, font=font, fill=1) self.black_draw.text((left, top), text, font=font, fill=1) - #return self.red_draw.textsize(text, font=font) def rotate(self, angle): self.red_image.rotate(angle) @@ -68,8 +65,6 @@ def rotate(self, angle): 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, fill=(0, 0, 0)) self.black_draw.bitmap((x, y), bitmap, fill=0) def getFont(self, font_name): @@ -90,7 +85,7 @@ def initFonts(self): def truncate(self, string, font, max_size): num_chars = len(string) for char in string: - (a, b, np_x, np_y) = self.getFont(font).getbbox(string) + (_unused_a, _unused_b, np_x, np_y) = self.getFont(font).getbbox(string) if np_x >= max_size: string = string[:-1] From 6ba233c322d7daeecbb5e0ec54ae066d32b5e3c3 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Wed, 20 Mar 2024 00:10:41 +0100 Subject: [PATCH 095/113] Fixing a typo. Merci Michi ;) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c2a3ec7..980c58d 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ cd /home/pi/InfoWindow export CFLAGS=-fcommon sudo apt install python3-dev python3 -m venv venv -. activate/bin/activate +. venv/bin/activate pip install -r requirements.txt ``` From 071b015b07c70d3ca505d6c3ea0709e418aa0c26 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sun, 19 Jan 2025 15:55:36 +0100 Subject: [PATCH 096/113] Update requirements.txt Only install rpi specific libs on pis --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index d1f2644..60a3064 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,9 @@ Pillow -spidev +spidev; platform_system == "Linux" and "arm" in platform_machine google-api-python-client google-auth-oauthlib python-dateutil requests todoist-python -RPi.GPIO -gpiozero +RPi.GPIO; platform_system == "Linux" and "arm" in platform_machine +gpiozero; platform_system == "Linux" and "arm" in platform_machine From 203a6c7357618bb37c424fd65c5793ba7373bd7f Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 27 Mar 2025 23:01:59 +0100 Subject: [PATCH 097/113] Add caldav support and allow to use google and caldav together --- README.md | 6 ++- config.json-sample | 16 ++++++- infowindow.py | 24 +++++++--- mod_calendar/mod_caldav.py | 97 ++++++++++++++++++++++++++++++++++++++ mod_calendar/mod_google.py | 5 +- mod_todo/mod_caldav.py | 90 +++++++++++++++++++++++++++++++++++ mod_todo/mod_google.py | 13 ++--- requirements.txt | 1 + 8 files changed, 232 insertions(+), 20 deletions(-) create mode 100644 mod_calendar/mod_caldav.py create mode 100644 mod_todo/mod_caldav.py diff --git a/README.md b/README.md index 980c58d..fc627dd 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,12 @@ to the data. In your face reminder. ## Features * **Calendar** - * Google Calendar is the only calendar currently supported + * Google Calendar + * CalDAV Calendar (added for Nextcloud support) * **Todo List** * Todoist * Teamwork.com + * CalDAV Todos (added for Nextcloud support) * **Weather** * Open Weather Map current data only. Future plan for forecast data. @@ -32,7 +34,7 @@ Activate SPI on your Raspberry Pi by using the `raspi-config` tool under Interfa Also for some RaspiOS versions, you have to install the `libopenjp2-7` package: ```bash -sudo apt-get install libopenjp2-7 +sudo apt-get install libopenjp2-7 libxslt1 ``` ### Get software diff --git a/config.json-sample b/config.json-sample index beaf8f4..1f18f3b 100644 --- a/config.json-sample +++ b/config.json-sample @@ -9,11 +9,25 @@ "api_key": "1234" }, "calendar": { - "additional": ["Contacts", "Birthdays"], "ignored": ["Buy ticket!"], "today_text_color": "red", "today_background_color": "white" }, + "calendar_google": { + "additional": ["Contacts", "Birthdays"], + }, + "calendar_caldav": { + "caldav_url": "https://your.domain.tld/some/caldav", + "username": "john", + "password": "supersecret", + "additional": ["Contact birthdays", "some calendar"] + }, + "todo_caldav": { + "caldav_url": "https://your.domain.tld/some/caldav", + "username": "john", + "password": "supersecret", + "additional": ["another calendar"] + }, "weather": { "api_key": "1234", "city": "Sacramento,US", diff --git a/infowindow.py b/infowindow.py index 163dd57..01a8b08 100755 --- a/infowindow.py +++ b/infowindow.py @@ -13,8 +13,10 @@ # 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_calendar import mod_google as modCalendar # CALENDAR +from mod_todo import mod_google as modTodoGoogle # Google todo +from mod_todo import mod_caldav as modTodoCaldav # Caldav todo +from mod_calendar import mod_google as modCalendarGoogle # Google calendar +from mod_calendar import mod_caldav as modCalendarCaldav # Caldav calendar from mod_weather import mod_owm as modWeather # WEATHER # TODO: Create dictionaries for API args. so that they can be custom. @@ -27,12 +29,16 @@ # Rotation. 0 for desktop, 180 for hanging upside down rotation = config_data["general"]["rotation"] todo_opts = config_data["todo"] +todo_opts["timeformat"] = config_data["general"]["timeformat"] +todo_opts["todo_caldav"] = config_data["todo_caldav"] 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"] calendar_opts["sunday_first_dow"] = config_data["general"]["sunday_first_dow"] +calendar_opts["calendar_google"] = config_data["calendar_google"] +calendar_opts["calendar_caldav"] = config_data["calendar_caldav"] weather_opts["timeformat"] = config_data["general"]["timeformat"] infowindow_opts["timeformat"] = config_data["general"]["timeformat"] infowindow_opts["cell_spacing"] = config_data["general"]["cell_spacing"] @@ -44,7 +50,6 @@ logging.basicConfig(level=logging.DEBUG) logging.info("Configuration Complete") - # helper to calculate max char width and height def get_max_char_size(iw, chars, font): max_width = 0 @@ -68,8 +73,10 @@ def render_centered_text(iw, text, font, color, center_position, y_position): # Main Program ################################################################ def main(): # Instantiate API modules - todo = modTodo.ToDo(todo_opts) - cal = modCalendar.Cal(calendar_opts) + todoGoogle = modTodoGoogle.ToDo(todo_opts) + todoCaldav = modTodoCaldav.ToDo(todo_opts) + calGoogle = modCalendarGoogle.Cal(calendar_opts) + calCaldav = modCalendarCaldav.Cal(calendar_opts) weather = modWeather.Weather(weather_opts) # Setup e-ink initial drawings @@ -105,7 +112,10 @@ def main(): # DISPLAY TO DO INFO # ========================================================================= - todo_items = todo.list() + todo_items = sorted( + todoGoogle.list() + todoCaldav.list(), + key=lambda x: (x["priority"] == 0, x["priority"]) + ) logging.debug("Todo Items") logging.debug("-----------------------------------------------------------------------") @@ -131,7 +141,7 @@ def main(): # DISPLAY CALENDAR INFO # ========================================================================= - cal_items = cal.list() + cal_items = sorted(calGoogle.list() + calCaldav.list(), key=lambda x: x["start_ts"], reverse=False) logging.debug("Calendar Items") logging.debug("-----------------------------------------------------------------------") diff --git a/mod_calendar/mod_caldav.py b/mod_calendar/mod_caldav.py new file mode 100644 index 0000000..ce3f5b4 --- /dev/null +++ b/mod_calendar/mod_caldav.py @@ -0,0 +1,97 @@ +from caldav import DAVClient +from dateutil.parser import parse as dtparse +from datetime import datetime as dt, timedelta +import logging + +# Disable excessive logging from caldav library +# logging.getLogger("caldav").setLevel(logging.WARNING) + +class Cal: + def __init__(self, options): + logging.debug("Initializing Module: Calendar: CalDAV") + self.client = DAVClient( + url=options["calendar_caldav"]["caldav_url"], + username=options["calendar_caldav"]["username"], + password=options["calendar_caldav"]["password"], + ) + self.timeformat = options["timeformat"] + self.additional = options["calendar_caldav"]["additional"] + self.ignored = options["ignored"] + self.sunday_first_dow = options["sunday_first_dow"] + + def list(self): + events = [] + items = [] + now = dt.utcnow() + day_start_ts_now = dt.timestamp(now.replace(hour=0, minute=0, second=0, microsecond=0)) + + # Fetch calendars + principal = self.client.principal() + calendars = principal.calendars() + + # Filter calendars + logging.info(f"Available CalDAV calendars: {', '.join([x.name for x in calendars])}") + selected_calendars = [ + cal for cal in calendars if cal.name in self.additional or not self.additional + ] + + for calendar in selected_calendars: + logging.debug(f"Fetching calendar: {calendar.name}") + results = calendar.search( + start=now, end=now + timedelta(days=30), event=True + ) + logging.debug(f"Found {len(results)} results: {results}") + + for event in results: + ical = event.icalendar_component + logging.debug(f"Event: {event}") + for comp in ical.walk(): + if comp.name != "VEVENT": + continue + + summary = comp.get("SUMMARY", "No Title") + if summary in self.ignored: + continue + + start = comp.get("DTSTART").dt + if isinstance(start, dt): # Ensure it's datetime, not date + start_str = start.isoformat() + else: + start = dt.combine(start, dt.min.time()) + start_str = start.isoformat() + + events.append((start_str, summary)) + + # Sort events by start date + events.sort() + + for start_str, summary in events: + start = dtparse(start_str) + today = start.date() <= dt.today().date() + + if self.timeformat == "12h": + st_date = start.strftime('%m-%d') + st_time = start.strftime('%I:%M %p') + else: + st_date = start.strftime('%d.%m') + st_time = start.strftime('%H:%M') + + if self.sunday_first_dow: + week = start.strftime('%U') + else: + week = start.strftime('%W') + + event_start_ts_now = dt.timestamp(start.replace(hour=0, minute=0, second=0, microsecond=0)) + + items.append({ + "date": st_date, + "time": st_time, + "content": summary, + "today": today, + "week": int(week), + "start_ts": dt.timestamp(dtparse(start_str)), + "days_away": (event_start_ts_now - day_start_ts_now) // 86400, # days away + "weeks_away": (event_start_ts_now - day_start_ts_now) // 604800 # weeks away + }) + + return items diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index d2e59b3..75bc095 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -7,13 +7,13 @@ # Silence goofy google deprecated errors logging.getLogger('googleapiclient.discovery_cache').setLevel(logging.ERROR) - class Cal: def __init__(self, options): + logging.debug("Initializing Module: Calendar: Google") ga = mod_google_auth.GoogleAuth() self.creds = ga.login() self.timeformat = options["timeformat"] - self.additional = options["additional"] + self.additional = options["calendar_google"]["additional"] self.ignored = options["ignored"] self.sunday_first_dow = options["sunday_first_dow"] @@ -86,6 +86,7 @@ def list(self): "content": events[event_key]['summary'], "today": today, "week": int(week), + "start_ts": dt.timestamp(dtparse(start)), "days_away": (event_start_ts_now - day_start_ts_now) // 86400, # days away "weeks_away": (event_start_ts_now - day_start_ts_now) // 604800 # weeks away }) diff --git a/mod_todo/mod_caldav.py b/mod_todo/mod_caldav.py new file mode 100644 index 0000000..e005eac --- /dev/null +++ b/mod_todo/mod_caldav.py @@ -0,0 +1,90 @@ +from caldav import DAVClient +from datetime import datetime, timedelta, date +import logging + +today = date.today() +tomorrow = date.today() + timedelta(days=1) + +class ToDo: + def __init__(self, options): + logging.debug("Initializing Module: ToDo: CalDAV") + self.client = DAVClient( + url=options["todo_caldav"]["caldav_url"], + username=options["todo_caldav"]["username"], + password=options["todo_caldav"]["password"], + ) + self.timeformat = options["timeformat"] + self.additional = options["todo_caldav"]["additional"] + # self.sunday_first_dow = options["sunday_first_dow"] + + def list(self): + todos_without_due = [] + todos_with_due = [] + todos = [] + now = datetime.utcnow() + + # Fetch calendars + principal = self.client.principal() + calendars = principal.calendars() + + # Filter calendars + logging.info(f"Available CalDAV calendars for todo: {', '.join([x.name for x in calendars])}") + selected_calendars = [ + cal for cal in calendars if cal.name in self.additional or not self.additional + ] + + for calendar in selected_calendars: + logging.debug(f"Fetching todos from calendar: {calendar.name}") + results = calendar.search( + start=now - timedelta(days=30), end=now + timedelta(days=30), todo=True + ) + logging.debug(f"Found {len(results)} results: {results}") + + for todo in results: + ical = todo.icalendar_component + logging.debug(f"Todo: {todo}") + for comp in ical.walk(): + if comp.name != "VTODO": + continue + + summary = comp.get("SUMMARY", "No Title") + print(summary) + if "DUE" in comp.keys(): + due = comp.get("DUE").dt + if isinstance(due, datetime): # Ensure it's datetime, not date + due_str = due.isoformat() + else: + due = datetime.combine(due, datetime.min.time()) + due_str = due.isoformat() + + todos_with_due.append((due_str, comp)) + + else: + todos_without_due.append((summary, comp)) + + for summary, todo in sorted(todos_without_due): + todos.append({"content": todo.get("SUMMARY", "No Title"), + "priority": todo.get("PRIORITY", 0), + "today": False + }) + + for due_str, todo in sorted(todos_with_due): + is_today = False + due = datetime.fromisoformat(due_str.replace("Z", "+00:00")).date() + if due < today: + is_today = True + todo['SUMMARY'] = f"Overdue: {todo['SUMMARY']}" + todo['PRIORITY'] = 1 + elif due == today: + is_today = True + elif due == tomorrow: + pass + else: + continue + + todos.append({"content": todo.get("SUMMARY", "No Title"), + "priority": todo.get("PRIORITY", 0), + "today": is_today + }) + + return todos \ No newline at end of file diff --git a/mod_todo/mod_google.py b/mod_todo/mod_google.py index d7abcb4..e269723 100644 --- a/mod_todo/mod_google.py +++ b/mod_todo/mod_google.py @@ -6,19 +6,15 @@ today = date.today() tomorrow = date.today() + timedelta(days=1) -logger = logging.getLogger(__name__) - - class ToDo: def __init__(self, api_key): # This module authenticates from Google Auth API. We pull in the auth module # wrapper to keep it clean. - logger.info("Initializing Module: ToDo: GOOGLE") + logging.debug("Initializing Module: ToDo: Google") ga = mod_google_auth.GoogleAuth() self.creds = ga.login() def list(self): - logging.info("Entering ToDo.list()") service = build('tasks', 'v1', credentials=self.creds) tasks_with_due = [] @@ -38,7 +34,7 @@ def list(self): else: items_without_due.append({ "content": task['title'], - "priority": task['position'], + "priority": int(task['position']), "today": False }) @@ -47,7 +43,8 @@ def list(self): due = datetime.fromisoformat(task['due'].replace("Z", "+00:00")).date() if due < today: is_today = True - task['title'] = "Overdue: %s" % task['title'] + task['title'] = f"Overdue: {task['title']}" + task['position'] = 1 elif due == today: is_today = True elif due == tomorrow: @@ -57,7 +54,7 @@ def list(self): items_with_due.append({ "content": task['title'], - "priority": task['position'], + "priority": int(task['position']), "today": is_today }) diff --git a/requirements.txt b/requirements.txt index 60a3064..cf99768 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ requests todoist-python RPi.GPIO; platform_system == "Linux" and "arm" in platform_machine gpiozero; platform_system == "Linux" and "arm" in platform_machine +caldav \ No newline at end of file From 4aa062693d9341f58bd3da2b7dda4364e014394c Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 27 Mar 2025 23:35:09 +0100 Subject: [PATCH 098/113] Replace emoji with ascii "art" and calculate age --- mod_calendar/mod_caldav.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mod_calendar/mod_caldav.py b/mod_calendar/mod_caldav.py index ce3f5b4..898ec51 100644 --- a/mod_calendar/mod_caldav.py +++ b/mod_calendar/mod_caldav.py @@ -1,11 +1,21 @@ from caldav import DAVClient from dateutil.parser import parse as dtparse from datetime import datetime as dt, timedelta +import re import logging # Disable excessive logging from caldav library # logging.getLogger("caldav").setLevel(logging.WARNING) +def replace_birth_year_with_age(summary): + match = re.search(r"\((\d{4})\)", summary) # Find (YYYY) + if match: + birth_year = int(match.group(1)) + current_year = dt.now().year + age = current_year - birth_year + summary = summary.replace(f"({birth_year})", f"(Age {age})") # Replace with age + return summary + class Cal: def __init__(self, options): logging.debug("Initializing Module: Calendar: CalDAV") @@ -52,6 +62,9 @@ def list(self): summary = comp.get("SUMMARY", "No Title") if summary in self.ignored: continue + # replace birthday emoji with ascii + summary = summary.replace("🎂", "[_i_]") + summary = replace_birth_year_with_age(summary) start = comp.get("DTSTART").dt if isinstance(start, dt): # Ensure it's datetime, not date From ff8826587951569a2722b93ce65bed57484f4240 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 10 Apr 2025 00:16:03 +0200 Subject: [PATCH 099/113] WIP --- config.json-sample | 3 ++- infowindow.py | 1 + mod_calendar/mod_caldav.py | 37 +++++++++++++++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/config.json-sample b/config.json-sample index 1f18f3b..06049e0 100644 --- a/config.json-sample +++ b/config.json-sample @@ -3,7 +3,8 @@ "rotation": 180, "timeformat": "12h", "sunday_first_dow": false, - "cell_spacing": 2 + "cell_spacing": 2, + "timezone": "Europe/Zurich" }, "todo": { "api_key": "1234" diff --git a/infowindow.py b/infowindow.py index 01a8b08..2df9b93 100755 --- a/infowindow.py +++ b/infowindow.py @@ -39,6 +39,7 @@ calendar_opts["sunday_first_dow"] = config_data["general"]["sunday_first_dow"] calendar_opts["calendar_google"] = config_data["calendar_google"] calendar_opts["calendar_caldav"] = config_data["calendar_caldav"] +calendar_opts["timezone"] = config_data["general"]["timezone"] weather_opts["timeformat"] = config_data["general"]["timeformat"] infowindow_opts["timeformat"] = config_data["general"]["timeformat"] infowindow_opts["cell_spacing"] = config_data["general"]["cell_spacing"] diff --git a/mod_calendar/mod_caldav.py b/mod_calendar/mod_caldav.py index 898ec51..874962a 100644 --- a/mod_calendar/mod_caldav.py +++ b/mod_calendar/mod_caldav.py @@ -1,6 +1,7 @@ from caldav import DAVClient from dateutil.parser import parse as dtparse from datetime import datetime as dt, timedelta +import pytz import re import logging @@ -28,12 +29,21 @@ def __init__(self, options): self.additional = options["calendar_caldav"]["additional"] self.ignored = options["ignored"] self.sunday_first_dow = options["sunday_first_dow"] + self.timezone = pytz.timezone(options["timezone"]) def list(self): events = [] items = [] - now = dt.utcnow() + # now = dt.utcnow() + now = dt.now(self.timezone) + # now = dt.now() day_start_ts_now = dt.timestamp(now.replace(hour=0, minute=0, second=0, microsecond=0)) + # + # today = dt.now(self.timezone).date() + # midnight_today = self.timezone.localize(dt.combine(today, dt.min.time())) + # last_second_tomorrow = self.timezone.localize(dt.combine(today + timedelta(days=1), dt.max.time())) + # midnight_today_utc = midnight_today.astimezone(pytz.utc) + # last_second_tomorrow_utc = last_second_tomorrow.astimezone(pytz.utc) # Fetch calendars principal = self.client.principal() @@ -80,7 +90,30 @@ def list(self): for start_str, summary in events: start = dtparse(start_str) - today = start.date() <= dt.today().date() + today = start.date() <= dt.today().date() # oxi orig + # today = start.date() == dt.today().date() # chatgpt 1 + + # event_end = comp.get("DTEND") + # if event_end: + # end = event_end.dt + # if isinstance(end, dt): # Ensure it's a datetime object + # if end.tzinfo is None: # If naive, localize it + # end = self.timezone.localize(end) # <-- This fixes the issue + # else: + # end = end.astimezone(self.timezone) + # else: # If it's a date, set it to the end of that day + # end = dt.combine(end, dt.max.time()) + # end = self.timezone.localize(end) + # else: + # end = start # If no DTEND, assume it's a single-instance event + # + # now_local = dt.now(self.timezone) + # + # # Debugging output + # print(f"DEBUG: end={end} (tzinfo={end.tzinfo}), now_local={now_local} (tzinfo={now_local.tzinfo})") + # + # if end < now_local: + # continue # Skip past events if self.timeformat == "12h": st_date = start.strftime('%m-%d') From 3b9321a8bed793f18813b5cfb1da4c1ca0be08c9 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 11 Apr 2025 14:15:25 +0200 Subject: [PATCH 100/113] This seems to work :) --- infowindow.py | 2 +- mod_calendar/mod_caldav.py | 144 +++++++++++++++++++++---------------- 2 files changed, 83 insertions(+), 63 deletions(-) diff --git a/infowindow.py b/infowindow.py index 2df9b93..9430702 100755 --- a/infowindow.py +++ b/infowindow.py @@ -48,7 +48,7 @@ ############################################################################### # Setup Logging - change to logging.DEBUG if you are having issues. -logging.basicConfig(level=logging.DEBUG) +logging.basicConfig(level=logging.INFO) logging.info("Configuration Complete") # helper to calculate max char width and height diff --git a/mod_calendar/mod_caldav.py b/mod_calendar/mod_caldav.py index 874962a..3b62ad5 100644 --- a/mod_calendar/mod_caldav.py +++ b/mod_calendar/mod_caldav.py @@ -1,6 +1,8 @@ from caldav import DAVClient from dateutil.parser import parse as dtparse -from datetime import datetime as dt, timedelta +from dateutil.tz import gettz +from datetime import datetime as dt, timedelta, time, date +from dateutil.rrule import rrulestr import pytz import re import logging @@ -29,42 +31,31 @@ def __init__(self, options): self.additional = options["calendar_caldav"]["additional"] self.ignored = options["ignored"] self.sunday_first_dow = options["sunday_first_dow"] - self.timezone = pytz.timezone(options["timezone"]) + self.local_tz = gettz(options.get("calendar_caldav", {}).get("timezone", "Europe/Zurich")) def list(self): events = [] items = [] - # now = dt.utcnow() - now = dt.now(self.timezone) - # now = dt.now() - day_start_ts_now = dt.timestamp(now.replace(hour=0, minute=0, second=0, microsecond=0)) - # - # today = dt.now(self.timezone).date() - # midnight_today = self.timezone.localize(dt.combine(today, dt.min.time())) - # last_second_tomorrow = self.timezone.localize(dt.combine(today + timedelta(days=1), dt.max.time())) - # midnight_today_utc = midnight_today.astimezone(pytz.utc) - # last_second_tomorrow_utc = last_second_tomorrow.astimezone(pytz.utc) + now = dt.now(self.local_tz) # Fetch calendars principal = self.client.principal() calendars = principal.calendars() - # Filter calendars - logging.info(f"Available CalDAV calendars: {', '.join([x.name for x in calendars])}") + logging.info(f"Available CalDAV calendars: {', '.join([x.name for x in calendars])})") selected_calendars = [ cal for cal in calendars if cal.name in self.additional or not self.additional ] for calendar in selected_calendars: logging.debug(f"Fetching calendar: {calendar.name}") - results = calendar.search( - start=now, end=now + timedelta(days=30), event=True - ) - logging.debug(f"Found {len(results)} results: {results}") + results = calendar.search(start=now - timedelta(days=1), end=now + timedelta(days=30), event=True) + + logging.debug(f"Found {len(results)} results") for event in results: ical = event.icalendar_component - logging.debug(f"Event: {event}") + for comp in ical.walk(): if comp.name != "VEVENT": continue @@ -72,48 +63,78 @@ def list(self): summary = comp.get("SUMMARY", "No Title") if summary in self.ignored: continue - # replace birthday emoji with ascii - summary = summary.replace("🎂", "[_i_]") + + summary = summary.replace("\U0001F382", "[_i_]") summary = replace_birth_year_with_age(summary) - start = comp.get("DTSTART").dt - if isinstance(start, dt): # Ensure it's datetime, not date - start_str = start.isoformat() + start_orig = comp.get("DTSTART").dt + end_orig = comp.get("DTEND", None) + rrule = comp.get("RRULE") + is_all_day = not isinstance(start_orig, dt) + + logging.debug(f"Raw DTSTART: {start_orig}, DTEND: {end_orig}, RRULE: {rrule}, is_all_day: {is_all_day}") + + if rrule: + rrule_str = "RRULE:" + rrule.to_ical().decode() + rule_start = dt.combine(start_orig, time.min).replace(tzinfo=self.local_tz) if is_all_day else start_orig + rule = rrulestr(rrule_str, dtstart=rule_start) + next_occurrence = rule.after(now.replace(hour=0, minute=0, second=0, microsecond=0), inc=True) + if not next_occurrence: + logging.debug(f"No next occurrence found for recurring event: {summary}") + continue + if isinstance(next_occurrence, date) and not isinstance(next_occurrence, dt): + start = dt.combine(next_occurrence, time.min).replace(tzinfo=self.local_tz) + else: + if next_occurrence.tzinfo is None: + start = next_occurrence.replace(tzinfo=self.local_tz) + else: + start = next_occurrence.astimezone(self.local_tz) + + if is_all_day: + end = start + timedelta(days=1) - timedelta(seconds=1) + else: + end = start + timedelta(hours=1) else: - start = dt.combine(start, dt.min.time()) - start_str = start.isoformat() + start = start_orig + if is_all_day: + start = start if isinstance(start, dt) else dt.combine(start, time.min).replace(tzinfo=self.local_tz) + if end_orig: + end_date = end_orig.dt if hasattr(end_orig, 'dt') else end_orig + end = dt.combine(end_date, time.min).replace(tzinfo=self.local_tz) - timedelta(seconds=1) + else: + end = start + timedelta(days=1) + else: + if start.tzinfo is None: + start = pytz.utc.localize(start) + start = start.astimezone(self.local_tz) + + if end_orig: + end = end_orig.dt if hasattr(end_orig, 'dt') else end_orig + if end.tzinfo is None: + end = pytz.utc.localize(end) + end = end.astimezone(self.local_tz) + else: + end = start + timedelta(hours=1) + + if end < now: + logging.debug(f"Skipping event (in the past): {summary} (start: {start}, end: {end}, now: {now})") + continue - events.append((start_str, summary)) + logging.debug(f"Adding event: {summary} at {start.isoformat()} (end: {end.isoformat()})") + events.append((start.isoformat(), summary)) + + if not events: + return [] - # Sort events by start date events.sort() + items = [] for start_str, summary in events: start = dtparse(start_str) - today = start.date() <= dt.today().date() # oxi orig - # today = start.date() == dt.today().date() # chatgpt 1 - - # event_end = comp.get("DTEND") - # if event_end: - # end = event_end.dt - # if isinstance(end, dt): # Ensure it's a datetime object - # if end.tzinfo is None: # If naive, localize it - # end = self.timezone.localize(end) # <-- This fixes the issue - # else: - # end = end.astimezone(self.timezone) - # else: # If it's a date, set it to the end of that day - # end = dt.combine(end, dt.max.time()) - # end = self.timezone.localize(end) - # else: - # end = start # If no DTEND, assume it's a single-instance event - # - # now_local = dt.now(self.timezone) - # - # # Debugging output - # print(f"DEBUG: end={end} (tzinfo={end.tzinfo}), now_local={now_local} (tzinfo={now_local.tzinfo})") - # - # if end < now_local: - # continue # Skip past events + now_local = dt.now(self.local_tz) + today = start.date() == now_local.date() + days_away = (start.date() - now_local.date()).days + weeks_away = days_away // 7 if self.timeformat == "12h": st_date = start.strftime('%m-%d') @@ -123,21 +144,20 @@ def list(self): st_time = start.strftime('%H:%M') if self.sunday_first_dow: - week = start.strftime('%U') + week = int(start.strftime('%U')) else: - week = start.strftime('%W') - - event_start_ts_now = dt.timestamp(start.replace(hour=0, minute=0, second=0, microsecond=0)) + week = int(start.strftime('%W')) items.append({ + "summary": summary, "date": st_date, "time": st_time, - "content": summary, + "week": week, + "start_ts": start.timestamp(), "today": today, - "week": int(week), - "start_ts": dt.timestamp(dtparse(start_str)), - "days_away": (event_start_ts_now - day_start_ts_now) // 86400, # days away - "weeks_away": (event_start_ts_now - day_start_ts_now) // 604800 # weeks away + "days_away": days_away, + "weeks_away": weeks_away, + "content": f"{summary}", }) - return items + return items \ No newline at end of file From 27e2da75838baf72665475326e22002cc4271fa4 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 11 Apr 2025 14:25:27 +0200 Subject: [PATCH 101/113] This seems to work :) --- mod_calendar/mod_caldav.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mod_calendar/mod_caldav.py b/mod_calendar/mod_caldav.py index 3b62ad5..35789ee 100644 --- a/mod_calendar/mod_caldav.py +++ b/mod_calendar/mod_caldav.py @@ -132,6 +132,7 @@ def list(self): for start_str, summary in events: start = dtparse(start_str) now_local = dt.now(self.local_tz) + logging.debug(f"Checking if today: event {summary} at {start}, now is {now_local}") today = start.date() == now_local.date() days_away = (start.date() - now_local.date()).days weeks_away = days_away // 7 From c18ddf647f13f312efc5769a30f6b65b5fade008 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 11 Apr 2025 15:47:26 +0200 Subject: [PATCH 102/113] Allow to disable google and caldav calendar and todo modules --- config.json-sample | 6 ++++++ infowindow.py | 1 + mod_calendar/mod_caldav.py | 28 +++++++++++++++++----------- mod_calendar/mod_google.py | 18 ++++++++++++------ mod_todo/mod_caldav.py | 24 +++++++++++++++--------- mod_todo/mod_google.py | 12 +++++++++--- 6 files changed, 60 insertions(+), 29 deletions(-) diff --git a/config.json-sample b/config.json-sample index 06049e0..6e90b49 100644 --- a/config.json-sample +++ b/config.json-sample @@ -15,15 +15,21 @@ "today_background_color": "white" }, "calendar_google": { + "enabled": true, "additional": ["Contacts", "Birthdays"], }, "calendar_caldav": { + "enabled": true, "caldav_url": "https://your.domain.tld/some/caldav", "username": "john", "password": "supersecret", "additional": ["Contact birthdays", "some calendar"] }, + "todo_google": { + "enabled": true + }, "todo_caldav": { + "enabled": true, "caldav_url": "https://your.domain.tld/some/caldav", "username": "john", "password": "supersecret", diff --git a/infowindow.py b/infowindow.py index 9430702..6e7032b 100755 --- a/infowindow.py +++ b/infowindow.py @@ -30,6 +30,7 @@ rotation = config_data["general"]["rotation"] todo_opts = config_data["todo"] todo_opts["timeformat"] = config_data["general"]["timeformat"] +todo_opts["todo_google"] = config_data["todo_google"] todo_opts["todo_caldav"] = config_data["todo_caldav"] calendar_opts = config_data["calendar"] weather_opts = config_data["weather"] diff --git a/mod_calendar/mod_caldav.py b/mod_calendar/mod_caldav.py index 35789ee..f90ab51 100644 --- a/mod_calendar/mod_caldav.py +++ b/mod_calendar/mod_caldav.py @@ -22,18 +22,24 @@ def replace_birth_year_with_age(summary): class Cal: def __init__(self, options): logging.debug("Initializing Module: Calendar: CalDAV") - self.client = DAVClient( - url=options["calendar_caldav"]["caldav_url"], - username=options["calendar_caldav"]["username"], - password=options["calendar_caldav"]["password"], - ) - self.timeformat = options["timeformat"] - self.additional = options["calendar_caldav"]["additional"] - self.ignored = options["ignored"] - self.sunday_first_dow = options["sunday_first_dow"] - self.local_tz = gettz(options.get("calendar_caldav", {}).get("timezone", "Europe/Zurich")) + self.enabled = options["calendar_caldav"]["enabled"] + if self.enabled: + self.client = DAVClient( + url=options["calendar_caldav"]["caldav_url"], + username=options["calendar_caldav"]["username"], + password=options["calendar_caldav"]["password"], + ) + self.timeformat = options["timeformat"] + self.additional = options["calendar_caldav"]["additional"] + self.ignored = options["ignored"] + self.sunday_first_dow = options["sunday_first_dow"] + self.local_tz = gettz(options.get("calendar_caldav", {}).get("timezone", "Europe/Zurich")) def list(self): + if not self.enabled: + logging.debug("Calendar: CalDAV not enabled") + return [] + events = [] items = [] now = dt.now(self.local_tz) @@ -49,7 +55,7 @@ def list(self): for calendar in selected_calendars: logging.debug(f"Fetching calendar: {calendar.name}") - results = calendar.search(start=now - timedelta(days=1), end=now + timedelta(days=30), event=True) + results = calendar.search(start=now - timedelta(days=1), end=now + timedelta(days=60), event=True) logging.debug(f"Found {len(results)} results") diff --git a/mod_calendar/mod_google.py b/mod_calendar/mod_google.py index 75bc095..7d119b7 100644 --- a/mod_calendar/mod_google.py +++ b/mod_calendar/mod_google.py @@ -10,14 +10,20 @@ class Cal: def __init__(self, options): logging.debug("Initializing Module: Calendar: Google") - ga = mod_google_auth.GoogleAuth() - self.creds = ga.login() - self.timeformat = options["timeformat"] - self.additional = options["calendar_google"]["additional"] - self.ignored = options["ignored"] - self.sunday_first_dow = options["sunday_first_dow"] + self.enabled = options["calendar_google"]["enabled"] + if self.enabled: + ga = mod_google_auth.GoogleAuth() + self.creds = ga.login() + self.timeformat = options["timeformat"] + self.additional = options["calendar_google"]["additional"] + self.ignored = options["ignored"] + self.sunday_first_dow = options["sunday_first_dow"] def list(self): + if not self.enabled: + logging.debug("Calendar: Google not enabled") + return [] + calendar_ids = [] events = {} items = [] diff --git a/mod_todo/mod_caldav.py b/mod_todo/mod_caldav.py index e005eac..3ea7944 100644 --- a/mod_todo/mod_caldav.py +++ b/mod_todo/mod_caldav.py @@ -8,16 +8,22 @@ class ToDo: def __init__(self, options): logging.debug("Initializing Module: ToDo: CalDAV") - self.client = DAVClient( - url=options["todo_caldav"]["caldav_url"], - username=options["todo_caldav"]["username"], - password=options["todo_caldav"]["password"], - ) - self.timeformat = options["timeformat"] - self.additional = options["todo_caldav"]["additional"] - # self.sunday_first_dow = options["sunday_first_dow"] + self.enabled = options["todo_caldav"]["enabled"] + if self.enabled: + self.client = DAVClient( + url=options["todo_caldav"]["caldav_url"], + username=options["todo_caldav"]["username"], + password=options["todo_caldav"]["password"], + ) + self.timeformat = options["timeformat"] + self.additional = options["todo_caldav"]["additional"] + # self.sunday_first_dow = options["sunday_first_dow"] def list(self): + if not self.enabled: + logging.debug("Todo: CalDAV not enabled") + return [] + todos_without_due = [] todos_with_due = [] todos = [] @@ -36,7 +42,7 @@ def list(self): for calendar in selected_calendars: logging.debug(f"Fetching todos from calendar: {calendar.name}") results = calendar.search( - start=now - timedelta(days=30), end=now + timedelta(days=30), todo=True + start=now - timedelta(days=30), end=now + timedelta(days=60), todo=True ) logging.debug(f"Found {len(results)} results: {results}") diff --git a/mod_todo/mod_google.py b/mod_todo/mod_google.py index e269723..6030869 100644 --- a/mod_todo/mod_google.py +++ b/mod_todo/mod_google.py @@ -7,14 +7,20 @@ tomorrow = date.today() + timedelta(days=1) class ToDo: - def __init__(self, api_key): + def __init__(self, options): # This module authenticates from Google Auth API. We pull in the auth module # wrapper to keep it clean. logging.debug("Initializing Module: ToDo: Google") - ga = mod_google_auth.GoogleAuth() - self.creds = ga.login() + self.enabled = options["todo_google"]["enabled"] + if self.enabled: + ga = mod_google_auth.GoogleAuth() + self.creds = ga.login() def list(self): + if not self.enabled: + logging.debug("Todo: Google not enabled") + return [] + service = build('tasks', 'v1', credentials=self.creds) tasks_with_due = [] From 0db96a5a19c9a95e05a34bd5f9fa3377fe7f31a0 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sat, 12 Apr 2025 00:53:41 +0200 Subject: [PATCH 103/113] Allow to disable google and caldav calendar and todo modules --- mod_calendar/mod_caldav.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mod_calendar/mod_caldav.py b/mod_calendar/mod_caldav.py index f90ab51..d763f7a 100644 --- a/mod_calendar/mod_caldav.py +++ b/mod_calendar/mod_caldav.py @@ -139,8 +139,13 @@ def list(self): start = dtparse(start_str) now_local = dt.now(self.local_tz) logging.debug(f"Checking if today: event {summary} at {start}, now is {now_local}") - today = start.date() == now_local.date() + if start.date() <= now_local.date(): + today = True + else: + today = False + days_away = (start.date() - now_local.date()).days + logging.debug(f"days_away: {days_away}") weeks_away = days_away // 7 if self.timeformat == "12h": From 6ee3cc4da32acee75722755d5b7066e55a300c2e Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sat, 12 Apr 2025 00:58:38 +0200 Subject: [PATCH 104/113] New birthday ascii cake --- mod_calendar/mod_caldav.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_calendar/mod_caldav.py b/mod_calendar/mod_caldav.py index d763f7a..657dd4e 100644 --- a/mod_calendar/mod_caldav.py +++ b/mod_calendar/mod_caldav.py @@ -70,7 +70,7 @@ def list(self): if summary in self.ignored: continue - summary = summary.replace("\U0001F382", "[_i_]") + summary = summary.replace("\U0001F382", "_i_") summary = replace_birth_year_with_age(summary) start_orig = comp.get("DTSTART").dt From 720638b9e56ecb3081a210b12317304016422f98 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sat, 12 Apr 2025 02:31:31 +0200 Subject: [PATCH 105/113] Support tasks with start and due --- mod_todo/mod_caldav.py | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/mod_todo/mod_caldav.py b/mod_todo/mod_caldav.py index 3ea7944..d549b13 100644 --- a/mod_todo/mod_caldav.py +++ b/mod_todo/mod_caldav.py @@ -24,8 +24,8 @@ def list(self): logging.debug("Todo: CalDAV not enabled") return [] - todos_without_due = [] - todos_with_due = [] + todos_without_date = [] + todos_with_date = [] todos = [] now = datetime.utcnow() @@ -48,14 +48,14 @@ def list(self): for todo in results: ical = todo.icalendar_component - logging.debug(f"Todo: {todo}") + logging.debug(f"Todo URL: {todo}") for comp in ical.walk(): if comp.name != "VTODO": continue summary = comp.get("SUMMARY", "No Title") - print(summary) if "DUE" in comp.keys(): + logging.debug(f"Found {summary} with DUE") due = comp.get("DUE").dt if isinstance(due, datetime): # Ensure it's datetime, not date due_str = due.isoformat() @@ -63,18 +63,31 @@ def list(self): due = datetime.combine(due, datetime.min.time()) due_str = due.isoformat() - todos_with_due.append((due_str, comp)) + logging.debug(f"Due {due_str}") + todos_with_date.append((due_str, comp)) + + elif "DTSTART" in comp.keys(): + logging.debug(f"Found {summary} with DTSTART") + start = comp.get("DTSTART").dt + if isinstance(start, datetime): # Ensure it's datetime, not date + start_str = start.isoformat() + else: + start = datetime.combine(start, datetime.min.time()) + start_str = start.isoformat() + + logging.debug(f"Start {start_str}") + todos_with_date.append((start_str, comp)) else: - todos_without_due.append((summary, comp)) + todos_without_date.append((summary, comp)) - for summary, todo in sorted(todos_without_due): + for summary, todo in sorted(todos_without_date): todos.append({"content": todo.get("SUMMARY", "No Title"), "priority": todo.get("PRIORITY", 0), "today": False }) - for due_str, todo in sorted(todos_with_due): + for due_str, todo in sorted(todos_with_date, key=lambda tup: tup[0]): is_today = False due = datetime.fromisoformat(due_str.replace("Z", "+00:00")).date() if due < today: From f0633e9bff0a2af790b07296f6c1c4f4cda80651 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Wed, 16 Apr 2025 10:09:16 +0200 Subject: [PATCH 106/113] Update mod_caldav.py --- mod_calendar/mod_caldav.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mod_calendar/mod_caldav.py b/mod_calendar/mod_caldav.py index 657dd4e..c7f1139 100644 --- a/mod_calendar/mod_caldav.py +++ b/mod_calendar/mod_caldav.py @@ -55,7 +55,7 @@ def list(self): for calendar in selected_calendars: logging.debug(f"Fetching calendar: {calendar.name}") - results = calendar.search(start=now - timedelta(days=1), end=now + timedelta(days=60), event=True) + results = calendar.search(start=now - timedelta(days=1), end=now + timedelta(days=60), event=True, expand=True) logging.debug(f"Found {len(results)} results") @@ -172,4 +172,4 @@ def list(self): "content": f"{summary}", }) - return items \ No newline at end of file + return items From c099b6d459b8d05e7ed794a48941aad918c68d8c Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Thu, 22 May 2025 10:00:08 +0200 Subject: [PATCH 107/113] Update mod_caldav.py Testing expand=True --- mod_todo/mod_caldav.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mod_todo/mod_caldav.py b/mod_todo/mod_caldav.py index d549b13..ea2d742 100644 --- a/mod_todo/mod_caldav.py +++ b/mod_todo/mod_caldav.py @@ -42,7 +42,7 @@ def list(self): for calendar in selected_calendars: logging.debug(f"Fetching todos from calendar: {calendar.name}") results = calendar.search( - start=now - timedelta(days=30), end=now + timedelta(days=60), todo=True + start=now - timedelta(days=30), end=now + timedelta(days=60), todo=True, expand=True ) logging.debug(f"Found {len(results)} results: {results}") @@ -106,4 +106,4 @@ def list(self): "today": is_today }) - return todos \ No newline at end of file + return todos From 3a20192717baed9a6106671cd5f044bab972d569 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 23 May 2025 01:26:02 +0200 Subject: [PATCH 108/113] Use the better property --- infowindow.py | 13 +++++++++++-- mod_calendar/mod_caldav.py | 2 +- mod_todo/mod_caldav.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/infowindow.py b/infowindow.py index 6e7032b..6e8d475 100755 --- a/infowindow.py +++ b/infowindow.py @@ -48,8 +48,17 @@ # END CONFIGURATION ########################################################### ############################################################################### -# Setup Logging - change to logging.DEBUG if you are having issues. -logging.basicConfig(level=logging.INFO) +# Setup Logging - set an env var DEBUG to anything to enable debug +log_level = logging.INFO +if os.getenv('DEBUG', False): + log_level = logging.DEBUG + +logging.basicConfig( + format='%(asctime)s %(levelname)-7s %(message)s', + datefmt='%Y-%d-%m %H:%M:%S', + level=log_level +) + logging.info("Configuration Complete") # helper to calculate max char width and height diff --git a/mod_calendar/mod_caldav.py b/mod_calendar/mod_caldav.py index c7f1139..270ba0e 100644 --- a/mod_calendar/mod_caldav.py +++ b/mod_calendar/mod_caldav.py @@ -60,7 +60,7 @@ def list(self): logging.debug(f"Found {len(results)} results") for event in results: - ical = event.icalendar_component + ical = event.icalendar_instance for comp in ical.walk(): if comp.name != "VEVENT": diff --git a/mod_todo/mod_caldav.py b/mod_todo/mod_caldav.py index ea2d742..e1837fb 100644 --- a/mod_todo/mod_caldav.py +++ b/mod_todo/mod_caldav.py @@ -47,7 +47,7 @@ def list(self): logging.debug(f"Found {len(results)} results: {results}") for todo in results: - ical = todo.icalendar_component + ical = todo.icalendar_instance logging.debug(f"Todo URL: {todo}") for comp in ical.walk(): if comp.name != "VTODO": From 7cb584251fb872c6fd217eb519595a46e5191ed3 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 23 May 2025 01:26:32 +0200 Subject: [PATCH 109/113] Add some minimal CalDAV documentation --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index fc627dd..11bfd0b 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,11 @@ In the google cloud console, do the following things: 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. +### CalDAV calendar and ToDo list (Modules) +To use CalDAV, configure the corresponding modules in the `config.json`. If you use a Nextcloud server, you can +find the CalDAV URL in the settings of your calendar. As a example (where `USERNAME` is you username): +`https://cloud.domain.tld/remote.php/dav/calendars/USERNAME` + #### 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 From 2b2439e9965ee102d5d0fd959213df2ca5543fe7 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 23 May 2025 01:27:03 +0200 Subject: [PATCH 110/113] No longer use deprecated datetime method --- mod_todo/mod_caldav.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mod_todo/mod_caldav.py b/mod_todo/mod_caldav.py index e1837fb..0fadbc8 100644 --- a/mod_todo/mod_caldav.py +++ b/mod_todo/mod_caldav.py @@ -1,5 +1,5 @@ from caldav import DAVClient -from datetime import datetime, timedelta, date +from datetime import datetime, timedelta, date, UTC import logging today = date.today() @@ -27,7 +27,7 @@ def list(self): todos_without_date = [] todos_with_date = [] todos = [] - now = datetime.utcnow() + now = datetime.now(UTC) # Fetch calendars principal = self.client.principal() From e083cb3eeba2dc944c69aec6229e73e1886d007e Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Sat, 24 May 2025 10:03:32 +0200 Subject: [PATCH 111/113] No longer use deprecated datetime method --- mod_todo/mod_caldav.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_todo/mod_caldav.py b/mod_todo/mod_caldav.py index 0fadbc8..5e049e9 100644 --- a/mod_todo/mod_caldav.py +++ b/mod_todo/mod_caldav.py @@ -42,7 +42,7 @@ def list(self): for calendar in selected_calendars: logging.debug(f"Fetching todos from calendar: {calendar.name}") results = calendar.search( - start=now - timedelta(days=30), end=now + timedelta(days=60), todo=True, expand=True + start=now - timedelta(days=360), end=now + timedelta(days=60), todo=True, expand=True ) logging.debug(f"Found {len(results)} results: {results}") From ad0e9bb37cfd49dab3fc04a21f5a508a12dff8f0 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 26 Dec 2025 15:56:17 +0100 Subject: [PATCH 112/113] Improve screensaver.py --- screensaver.py | 31 ++++++++++--------------------- 1 file changed, 10 insertions(+), 21 deletions(-) diff --git a/screensaver.py b/screensaver.py index 85a0bcc..0c9b317 100755 --- a/screensaver.py +++ b/screensaver.py @@ -10,39 +10,28 @@ logging.basicConfig(level=logging.INFO) logging.info("Screen saver starting") -def display_image(epd, image_data): - rgb_image = image_data.convert('RGB') - width, height = image_data.size - no_red = Image.new('RGB', (width, height), (255, 255, 255)) - only_red = Image.new('RGB', (width, height), (255, 255, 255)) - - for col in range(width): - for row in range(height): - - r, g, b = rgb_image.getpixel((col, row)) - no_red.putpixel((col, row), (0, g, b)) +def display_image(epd, black_fill, red_fill): + width = 800 + height = 480 - if r == 255 and g == 0 and b == 0: - only_red.putpixel((col, row), (0, 0 ,0)) - else: - only_red.putpixel((col, row), (255, 255, 255)) + # Create 1-bit monochrome images like the working infowindow.py + black_image = Image.new('1', (width, height), black_fill) + red_image = Image.new('1', (width, height), red_fill) - epd.display(epd.getbuffer(no_red), epd.getbuffer(only_red)) + epd.display(epd.getbuffer(black_image), epd.getbuffer(red_image)) def main(): epd = epd7in5b_V2.EPD() epd.init() - width = 800 - height = 480 - logging.info("Display black screen") - display_image(epd, Image.new('RGB', (width, height), (0, 0, 0))) + display_image(epd, black_fill=0, red_fill=1) logging.info("Display red screen") - display_image(epd, Image.new('RGB', (width, height), (255, 0, 0))) + display_image(epd, black_fill=1, red_fill=0) + logging.info("Display white screen") epd.Clear() epd.sleep() logging.info("Screen saver finished") From 6934e90adde19d22dbb04b35d0f0e70ed6287c39 Mon Sep 17 00:00:00 2001 From: Marc Urben Date: Fri, 26 Dec 2025 16:03:18 +0100 Subject: [PATCH 113/113] Add .gitattributes to enforce LF line endings across platforms" --- .gitattributes | 20 ++++++++++++++++++++ 3d_files/fusion360/README.md | 2 +- clearScreen.py | 6 +++--- 3 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ae6b62c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,20 @@ +# Set default behavior to automatically normalize line endings +* text=auto + +# Force LF line endings for all text files +*.py text eol=lf +*.txt text eol=lf +*.md text eol=lf +*.json text eol=lf +*.sh text eol=lf +*.yml text eol=lf +*.yaml text eol=lf + +# Binary files +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.ttf binary +*.pickle binary diff --git a/3d_files/fusion360/README.md b/3d_files/fusion360/README.md index 8ef2602..9e64ad2 100644 --- a/3d_files/fusion360/README.md +++ b/3d_files/fusion360/README.md @@ -1 +1 @@ -This file is the "Assembly" of all the parts needed to print. I left out the model of the Raspberry PI due to licensing. If you need the raspberry pi model, do a search on GrabCad there are a ton! +This file is the "Assembly" of all the parts needed to print. I left out the model of the Raspberry PI due to licensing. If you need the raspberry pi model, do a search on GrabCad there are a ton! diff --git a/clearScreen.py b/clearScreen.py index af59ca7..4c92d76 100644 --- a/clearScreen.py +++ b/clearScreen.py @@ -1,3 +1,3 @@ -from mod_infowindow import infowindow -iw = infowindow.InfoWindow() -iw.display() +from mod_infowindow import infowindow +iw = infowindow.InfoWindow() +iw.display()