forked from JeffJetton/retrofeed
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathretrofeed.py
225 lines (185 loc) · 8.2 KB
/
retrofeed.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# Standard library imports
import datetime as dt
import importlib.util
import os
import sys
import textwrap as tw
import time
# RetroFeed imports
from display import Display
# Globals...
VERSION = '0.2.0'
EXPECTED_TABLES = ['display', 'segments', 'playlist']
# The config dictionary sets all parameters, specifies segments to use, and display order
# Eventually this will be read in from a config.toml file, but I'm waiting until the
# standard Rasperry Pi OS install includes Python 3.11 (which has tomllib)
CONFIG = {'display': {'height': 24,
'width': 40,
'cps': 20,
'newline_cps': 100,
'beat_seconds': 1,
'force_uppercase': True,
'verbose_updates': True,
'24hr_time':True,
'show_intros':True,
},
# Segment modules must be declared and given a name (key) before use.
# Each must have at least a 'module' value specifying a .py file in
# the 'segments' directory. Other keys/values depend on the segment.
'segments': {'otd': {'module': 'wiki_on_this_day.py'},
'lucky': {'module': 'lucky_numbers.py'},
'nash_wx': {'module': 'us_weather.py',
'refresh': 15,
'lat': 36.118542,
'lon': -86.798358,
'location': 'Nashville Intl Airport (BNA)'},
# Example of declaring a segment module twice, with a
# different name (key) and different initialization
# parameters. Note that show_intro() will only be
# called when the first us_weather segment is set up,
# and not a second time when this one is set up.
# Also note the ".py" at the end of a segment module's
# name is assumed if missing, so we can be lazy...
'bos_wx': {'module': 'us_weather',
'refresh': 30,
'lat': 42.365738,
'lon': -71.017027,
'location': 'Boston Logan Intl Airport'},
'news': {'module': 'ap_news'},
'iss': {'module': 'spot_the_station', 'country':'United_States', 'region':'Tennessee', 'city':'Nashville'},
'fin': {'module': 'yahoo_finance'},
'datetime': {'module': 'date_time.py'}
},
# Segment names in the 'order' part of the playlist must match the
# names (keys) given above. They can either be a string by itself,
# or a two-element list with the name (string) as the first element
# and a dictionary of format specifications as the second.
'playlist': {'segment_pause': 6,
'order': ['datetime',
'nash_wx',
'datetime',
'news',
'otd',
'datetime',
'fin',
# Ask datetime to use a slightly different
# format for this showing...
['datetime', {'format': 'short'}],
'iss',
'datetime',
# Use abbreviated forecase for Boston
['bos_wx', {'forecast_periods':1}],
'datetime',
'news',
'datetime',
'lucky',
'otd',
]
}
}
def check_config_tables(config):
# Just throw an error if any of the main three sections aren't in config
# Nothing fancy...
missing_tables = []
for table in EXPECTED_TABLES:
if table not in config:
missing_tables.append(table)
if len(missing_tables) > 0:
raise RuntimeError('Table(s) missing in config: ' + ', '.join(missing_tables))
# Make sure each declared segment has at least a module key
bad_segments = []
for key in config['segments']:
if 'module' not in config['segments'][key]:
bad_segments.append(key)
if len(bad_segments) > 0:
raise RuntimeError('No module defined for segment(s) in config: ' + ', '.join(bad_segments))
def override_timings(config):
config['display']['cps'] = 1000
config['display']['newline_cps'] = 1000
config['display']['beat_seconds'] = 0.1
config['playlist']['segment_pause'] = 1
return config
def instantiate_segments(config, d):
# Segments dictionary holds references to all instantiated objects
segments = {}
# This just keeps track of unique modules already instantiated,
# so we only call show_intro() once per module
instantiated = []
# Go through config and initialize all required segments
# We don't check to see if they exist, so... fingers crossed!
for key in config['segments']:
mod_name = config['segments'][key]['module']
# Just in case the user put the .py on the end...
if mod_name.endswith('.py'):
mod_name = mod_name[0:-3]
# Import, instantiate, and add to segments dictionary
# using the specified key (which will match in playlist)
module = importlib.import_module('segments.' + mod_name)
segments[key] = module.Segment(d, config['segments'][key])
# If we haven't already, give module chance to introduce itself
if mod_name not in instantiated:
if d.show_intros:
segments[key].show_intro()
instantiated.append(mod_name)
return segments
def parse_seg_key_and_fmt(seg):
# If the segment is just a plain-old string,
# use that as the key and assume no formatting
seg_key = ''
seg_fmt = {}
if isinstance(seg, str):
seg_key = seg
# But if it's a list, use the first element as key
# and second element (if any) as format stuff
elif isinstance(seg, list):
seg_key = seg[0]
if len(seg) > 1:
seg_fmt = seg[1]
return (seg_key, seg_fmt)
def show_title(d):
os.system('clear')
for i in range(24):
print()
d.print(f'RETROFEED - VERSION {VERSION}')
d.print('Copyright (c) 2023 Jeff Jetton')
d.print('MIT License')
d.newline()
###############################################################################
def main():
# TODO: read in config from toml file once Python 3.11 is standard on Pis
# Until then, we'll pull it in from a big honkin' global
config = CONFIG
check_config_tables(config)
# Override with faster timings if there are any command-line args at all
if len(sys.argv) > 1:
config = override_timings(config)
# Create Display object from config settings
# This will be used by all segments
d = Display(config['display'])
# Segment modules may display intros on initialization,
# but we want the main title to come first
if d.show_intros:
show_title(d)
segments = instantiate_segments(config, d)
# Unpack the playlist
segment_pause = config['playlist']['segment_pause']
order = config['playlist']['order']
d.newline()
d.newline()
# Main loop
while True:
for seg in order:
d.newline()
d.newline()
(seg_key, seg_fmt) = parse_seg_key_and_fmt(seg)
if seg_key not in segments:
d.newline()
d.print_header(f'Missing Segment "{seg_key}"', '*')
d.newline(segment_pause)
continue
# Show the segment, with any special formating
segments[seg_key].show(seg_fmt)
d.newline()
d.newline(segment_pause)
if __name__ == "__main__":
main()