Skip to content

Commit d28940d

Browse files
authored
Show only dates with events (#5)
* Rename Arduino sketch * Render the next 5 days of events, skipping empty days * Fix timezone issues * Update README * poetry update * Bump version
1 parent b56965c commit d28940d

File tree

10 files changed

+122
-136
lines changed

10 files changed

+122
-136
lines changed

README.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ Some features of the dashboard:
3737

3838
4. Using the DNS name or IP of your host machine, you can go to <http://IP_ADDRESS:5000/docs> to see whether the API is running.
3939

40-
5. As for the Inkplate, I'm not going to devote too much space here since there are [official resources that describe how to set it up](https://inkplate.readthedocs.io/en/latest/get-started.html). It may take some trial and error for those new to microcontroller programming but it's all worth it! Only the Arduino portion of the guide is relevant, and you'll need to be able to run *.ino scripts via Arduino IDE before proceeding. From there, run the `inkplate.ino` file from the `inkplate` folder from the Arduino IDE when connected to the Inkplate.
40+
5. As for the Inkplate, I'm not going to devote too much space here since there are [official resources that describe how to set it up](https://inkplate.readthedocs.io/en/latest/get-started.html). It may take some trial and error for those new to microcontroller programming but it's all worth it! Only the Arduino portion of the guide is relevant, and you'll need to be able to run *.ino scripts via Arduino IDE before proceeding. From there, run the `inkplate10.ino` file from the `inkplate10` folder from the Arduino IDE when connected to the Inkplate.
4141

4242
6. That's all! Your Magic Dashboard should now be refreshed every hour!
4343

@@ -57,10 +57,9 @@ DISPLAY_TZ | No | America/Los_Angeles | Time zone for displaying the calendar
5757
LAT | No | 34.118333 | Latitude in decimal of the location to retrieve weather forecast for
5858
LNG | No | -118.300333 | Longitude in decimal of the location to retrieve weather forecast for
5959
WEATHER_UNITS | No | metric | Units of measurement for the temperature, `metric` and `imperial` units are available
60-
NUM_CAL_DATS_TO_SHOW | No | 5 | Number of days to show from the calendar
60+
NUM_CAL_DAYS_TO_QUERY | No | 30 | Number of days to query from the calendar
6161
IMAGE_WIDTH | No | 1200 | Width of image to be generated for display
6262
IMAGE_HEIGHT | No | 825 | Height of image to be generated for display
63-
ROTATE_ANGLE | No | 0 | If image is rendered in portrait orientation, angle to rotate to fit screen
6463

6564
## Development
6665

File renamed without changes.

poetry.lock

Lines changed: 10 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "mag-ink-dash-plus"
3-
version = "0.2.0"
3+
version = "0.3.0"
44
description = "E-Ink Magic Dashboard that runs off a battery powered Inkplate 10; displaying content from an ICS calendar feed and OpenWeatherMap that are retrieved and rendered by a Docker container."
55
authors = ["speedyg0nz", "stefanthoss"]
66
license = "Apache License 2.0"

src/config.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
import sys
33

44
import structlog
5-
from pytz import timezone
65

76
from owm.owm import WeatherUnits
87

@@ -20,14 +19,16 @@ class MagInkDashConfig:
2019
if not OWM_API_KEY:
2120
logger.error("OWM_API_KEY needs to be set.")
2221
sys.exit(1)
23-
DISPLAY_TZ: timezone = timezone(os.getenv("DISPLAY_TZ", "America/Los_Angeles"))
24-
NUM_CAL_DATS_TO_SHOW: int = int(os.getenv("NUM_CAL_DATS_TO_SHOW", 5))
22+
DISPLAY_TZ: str = os.getenv("DISPLAY_TZ", "America/Los_Angeles")
23+
NUM_CAL_DAYS_TO_QUERY: int = int(os.getenv("NUM_CAL_DAYS_TO_QUERY", 30))
2524
IMAGE_WIDTH: int = int(os.getenv("IMAGE_WIDTH", 1200))
2625
IMAGE_HEIGHT: int = int(os.getenv("IMAGE_HEIGHT", 825))
27-
ROTATE_ANGLE: int = int(os.getenv("ROTATE_ANGLE", 0))
2826
LAT: float = float(os.getenv("LAT", 34.118333))
2927
LNG: float = float(os.getenv("LNG", -118.300333))
3028
WEATHER_UNITS: WeatherUnits = WeatherUnits[os.getenv("WEATHER_UNITS", "metric")]
29+
NUM_DAYS_IN_TEMPLATE: int = (
30+
5 # Not configurable because it's hard-coded in the HTML template
31+
)
3132

3233
def get_config():
3334
global _current_config

src/ics_cal/ics.py

Lines changed: 16 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
This is where we retrieve events from an ICS calendar.
33
"""
44

5+
import datetime as dt
6+
57
import structlog
68

79
from ics_cal.icshelper import IcsHelper
@@ -12,10 +14,6 @@ def __init__(self):
1214
self.logger = structlog.get_logger()
1315
self.calHelper = IcsHelper()
1416

15-
def get_day_in_cal(self, startDate, eventDate):
16-
delta = eventDate - startDate
17-
return delta.days
18-
1917
def get_short_time(self, datetimeObj):
2018
datetime_str = ""
2119
if datetimeObj.minute > 0:
@@ -31,28 +29,23 @@ def get_short_time(self, datetimeObj):
3129
datetime_str = "{}{}am".format(str(datetimeObj.hour), datetime_str)
3230
return datetime_str
3331

34-
def get_events(
35-
self, currDate, ics_url, calStartDatetime, calEndDatetime, displayTZ, numDays
36-
):
32+
def get_events(self, ics_url, calStartDatetime, calEndDatetime, displayTZ, numDays):
3733
eventList = self.calHelper.retrieve_events(
3834
ics_url, calStartDatetime, calEndDatetime, displayTZ
3935
)
4036

41-
# check if event stretches across multiple days
42-
calList = []
43-
for i in range(numDays):
44-
calList.append([])
37+
calDict = {}
38+
4539
for event in eventList:
46-
idx = self.get_day_in_cal(currDate, event["startDatetime"].date())
4740
if event["isMultiday"]:
48-
end_idx = self.get_day_in_cal(currDate, event["endDatetime"].date())
49-
if idx < 0:
50-
idx = 0
51-
if end_idx >= len(calList):
52-
end_idx = len(calList) - 1
53-
for i in range(idx, end_idx + 1):
54-
calList[i].append(event)
55-
elif idx >= 0:
56-
calList[idx].append(event)
57-
58-
return calList
41+
numDays = (
42+
event["endDatetime"].date() - event["startDatetime"].date()
43+
).days
44+
for day in range(0, numDays):
45+
calDict.setdefault(
46+
event["startDatetime"].date() + dt.timedelta(days=day), []
47+
).append(event)
48+
else:
49+
calDict.setdefault(event["startDatetime"].date(), []).append(event)
50+
51+
return calDict

src/ics_cal/icshelper.py

Lines changed: 30 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import datetime as dt
1010
import sys
1111

12+
import arrow
1213
import requests
1314
import structlog
1415
from ics import Calendar
@@ -18,37 +19,11 @@ class IcsHelper:
1819
def __init__(self):
1920
self.logger = structlog.get_logger()
2021

21-
def to_datetime(self, isoDatetime, localTZ):
22-
# replace Z with +00:00 is a workaround until datetime library decides what to do with the Z notation
23-
to_datetime = dt.datetime.fromisoformat(isoDatetime.replace("Z", "+00:00"))
24-
return to_datetime.astimezone(localTZ)
25-
26-
def adjust_end_time(self, endTime, localTZ):
27-
# check if end time is at 00:00 of next day, if so set to max time for day before
28-
if endTime.hour == 0 and endTime.minute == 0 and endTime.second == 0:
29-
newEndtime = localTZ.localize(
30-
dt.datetime.combine(
31-
endTime.date() - dt.timedelta(days=1), dt.datetime.max.time()
32-
)
33-
)
34-
return newEndtime
35-
else:
36-
return endTime
37-
38-
def is_multiday(self, start, end):
39-
# check if event stretches across multiple days
40-
return start.date() != end.date()
41-
4222
def retrieve_events(self, ics_url, startDatetime, endDatetime, localTZ):
43-
# Call the Google Calendar API and return a list of events that fall within the specified dates
23+
# Call the ICS calendar and return a list of events that fall within the specified dates
4424
event_list = []
4525

46-
# TODO: Filter calendar events by time
47-
min_time_str = startDatetime.isoformat()
48-
max_time_str = endDatetime.isoformat()
49-
5026
self.logger.info("Retrieving events from ICS...")
51-
5227
response = requests.get(ics_url)
5328
if response.ok:
5429
cal = Calendar(response.text)
@@ -61,19 +36,34 @@ def retrieve_events(self, ics_url, startDatetime, endDatetime, localTZ):
6136
if not cal.events:
6237
self.logger.info("No upcoming calendar events found.")
6338
for event in cal.events:
64-
# extracting and converting events data into a new list
65-
new_event = {"summary": event.name}
66-
new_event["allday"] = event.all_day
67-
new_event["startDatetime"] = self.to_datetime(
68-
event.begin.isoformat(), localTZ
69-
)
70-
new_event["endDatetime"] = self.adjust_end_time(
71-
self.to_datetime(event.end.isoformat(), localTZ), localTZ
72-
)
73-
new_event["isMultiday"] = self.is_multiday(
74-
new_event["startDatetime"], new_event["endDatetime"]
75-
)
76-
event_list.append(new_event)
39+
if event.begin >= arrow.Arrow.fromdatetime(
40+
startDatetime
41+
) and event.begin < arrow.Arrow.fromdatetime(endDatetime):
42+
# extracting and converting events data into a new list
43+
new_event = {"summary": event.name}
44+
new_event["allday"] = event.all_day
45+
46+
if new_event["allday"]:
47+
# All-day events are always midnight UTC to midnight UTC, therefore timezone needs to be set
48+
new_event["startDatetime"] = dt.datetime.fromisoformat(
49+
event.begin.replace(tzinfo=localTZ).isoformat()
50+
)
51+
new_event["endDatetime"] = dt.datetime.fromisoformat(
52+
event.end.replace(tzinfo=localTZ).isoformat()
53+
)
54+
else:
55+
# Other events need to be translated to local timezone
56+
new_event["startDatetime"] = dt.datetime.fromisoformat(
57+
event.begin.to(localTZ).isoformat()
58+
)
59+
new_event["endDatetime"] = dt.datetime.fromisoformat(
60+
event.end.to(localTZ).isoformat()
61+
)
62+
63+
new_event["isMultiday"] = (
64+
new_event["endDatetime"] - new_event["startDatetime"]
65+
) > dt.timedelta(days=1)
66+
event_list.append(new_event)
7767

7868
# We need to sort eventList because the event will be sorted in "calendar order" instead of hours order
7969
# TODO: improve because of double cycle for now is not much cost

src/main.py

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,11 @@
66
retrieve the information. So feel free to change up the code and amend it to your needs.
77
"""
88

9-
import datetime
10-
import os
9+
import datetime as dt
1110
import tempfile
1211
import time
13-
from datetime import datetime as dt
14-
from typing import Optional
1512

13+
import pytz
1614
import structlog
1715
import uvicorn
1816
from fastapi import FastAPI
@@ -25,7 +23,7 @@
2523

2624
cfg = MagInkDashConfig.get_config()
2725

28-
app = FastAPI(title="MagInkDashPlus Server", version="0.2.0")
26+
app = FastAPI(title="MagInkDashPlus Server", version="0.3.0")
2927

3028
logger = structlog.get_logger()
3129

@@ -56,24 +54,23 @@ def get_image() -> FileResponse:
5654
cfg.LAT, cfg.LNG, cfg.OWM_API_KEY, cfg.WEATHER_UNITS
5755
)
5856

59-
# Retrieve Calendar Data
60-
currTime = dt.now(cfg.DISPLAY_TZ)
57+
currTime = dt.datetime.now(pytz.timezone(cfg.DISPLAY_TZ))
6158
currDate = currTime.date()
62-
calStartDatetime = cfg.DISPLAY_TZ.localize(dt.combine(currDate, dt.min.time()))
63-
calEndDatetime = cfg.DISPLAY_TZ.localize(
64-
dt.combine(
65-
currDate + datetime.timedelta(days=cfg.NUM_CAL_DATS_TO_SHOW - 1),
66-
dt.max.time(),
67-
)
59+
calStartDatetime = currTime.replace(hour=0, minute=0, second=0, microsecond=0)
60+
calEndDatetime = calStartDatetime + dt.timedelta(
61+
days=cfg.NUM_CAL_DAYS_TO_QUERY, seconds=-1
6862
)
69-
eventList = calModule.get_events(
70-
currDate,
63+
64+
events = calModule.get_events(
7165
cfg.ICS_URL,
7266
calStartDatetime,
7367
calEndDatetime,
7468
cfg.DISPLAY_TZ,
75-
cfg.NUM_CAL_DATS_TO_SHOW,
69+
cfg.NUM_CAL_DAYS_TO_QUERY,
7670
)
71+
events_sorted = sorted(
72+
events.items(), key=lambda x: x[0]
73+
) # sort by date so we can later take the first N days
7774

7875
end_time = time.time()
7976
logger.info(
@@ -87,16 +84,13 @@ def get_image() -> FileResponse:
8784
start_time = time.time()
8885
logger.info(f"Generating image...")
8986

90-
renderService = RenderHelper(
91-
cfg.IMAGE_WIDTH, cfg.IMAGE_HEIGHT, cfg.ROTATE_ANGLE
92-
)
87+
renderService = RenderHelper(cfg)
9388
renderService.process_inputs(
9489
currTime,
9590
current_weather,
9691
hourly_forecast,
9792
daily_forecast,
98-
eventList,
99-
cfg.NUM_CAL_DATS_TO_SHOW,
93+
events_sorted[: cfg.NUM_DAYS_IN_TEMPLATE],
10094
tf.name,
10195
)
10296

0 commit comments

Comments
 (0)