Make gkeep understand list indentation, tweaks to the countdown bar in
[kiosk.git] / kiosk.py
1 #!/usr/local/bin/python3.7
2
3 import sys
4 import traceback
5 import os
6 from threading import Thread
7 import time
8 from datetime import datetime
9 import constants
10 import renderer
11 import renderer
12 import renderer_catalog
13 import chooser
14 import logging
15 import trigger_catalog
16 import utils
17
18 def thread_change_current():
19     page_chooser = chooser.weighted_random_chooser_with_triggers(
20         trigger_catalog.get_triggers())
21     swap_page_target = 0
22     last_page = ""
23     while True:
24         now = time.time()
25         (page, triggered) = page_chooser.choose_next_page()
26
27         if triggered:
28             print('chooser[%s] - WE ARE TRIGGERED.' % utils.timestamp())
29             if page != last_page:
30                 print('chooser[%s] - EMERGENCY PAGE %s LOAD NEEDED' % (
31                     utils.timestamp(), page))
32                 try:
33                     f = open(os.path.join(constants.pages_dir,
34                                           'current.shtml'), 'w')
35                     emit_wrapped(f, page)
36                     f.close()
37                 except:
38                     print('chooser[%s] - page does not exist?!' % (
39                         utils.timestamp()))
40                     continue
41                 last_page = page
42                 swap_page_target = now + constants.refresh_period_sec
43
44                 # Also notify XMLHTTP clients that they need to refresh now.
45                 path = os.path.join(constants.pages_dir,
46                                     'reload_immediately.html')
47                 f = open(path, 'w')
48                 f.write('Reload, suckers!')
49                 f.close()
50
51                 # Fix this hack... maybe read the webserver logs and see if it
52                 # actually was picked up?
53                 time.sleep(0.750)
54                 os.remove(path)
55
56         elif now >= swap_page_target:
57             if (page == last_page):
58                 print(('chooser[%s] - nominal choice got the same page...' % (
59                     utils.timestamp())))
60                 continue
61             print('chooser[%s] - nominal choice of %s' % (utils.timestamp(), page))
62             try:
63                 f = open(os.path.join(constants.pages_dir,
64                                       'current.shtml'), 'w')
65                 emit_wrapped(f, page)
66                 f.close()
67             except:
68                 print('chooser[%s] - page does not exist?!' % (utils.timestamp()))
69                 continue
70             last_page = page
71             swap_page_target = now + constants.refresh_period_sec
72         time.sleep(1)
73
74 def pick_background_color():
75     now = datetime.now()
76     if now.hour <= 6 or now.hour >= 21:
77         return "E6B8B8"
78     elif now.hour == 7 or now.hour == 20:
79         return "EECDCD"
80     else:
81         return "FFFFFF"
82
83 def emit_wrapped(f, filename):
84     age = utils.describe_age_of_file_briefly("pages/%s" % filename)
85     bgcolor = pick_background_color()
86     f.write("""
87 <HEAD>
88   <TITLE>Kitchen Kiosk</TITLE>
89   <LINK rel="stylesheet" type="text/css" href="style.css">
90   <SCRIPT TYPE="text/javascript">
91
92   // Zoom the 'contents' div to fit without scrollbars and then make
93   // it visible.
94   function zoomScreen() {
95     z = 285;
96     do {
97       document.getElementById("content").style.zoom = z+"%%";
98       var body = document.body;
99       var html = document.documentElement;
100       var height = Math.max(body.scrollHeight,
101                             body.offsetHeight,
102                             html.clientHeight,
103                             html.scrollHeight,
104                             html.offsetHeight);
105       var windowHeight = window.innerHeight;
106       var width = Math.max(body.scrollWidth,
107                            body.offsetWidth,
108                            html.clientWidth,
109                            html.scrollWidth,
110                            html.offsetWidth);
111       var windowWidth = window.innerWidth;
112       var heightRatio = height / windowHeight;
113       var widthRatio = width / windowWidth;
114
115       if (heightRatio <= 1.0 && widthRatio <= 1.0) {
116         break;
117       }
118       z -= 4;
119     } while(z >= 70);
120     document.getElementById("content").style.visibility = "visible";
121   }
122
123   // Load IMG tags with DATA-SRC attributes late.
124   function lateLoadImages() {
125     var image = document.getElementsByTagName('img');
126     for (var i = 0; i < image.length; i++) {
127       if (image[i].getAttribute('DATA-SRC')) {
128         image[i].setAttribute('SRC', image[i].getAttribute('DATA-SRC'));
129       }
130     }
131   }
132
133   // Operate the clock at the top of the page.
134   function runClock() {
135     var today = new Date();
136     var h = today.getHours();
137     var ampm = h >= 12 ? 'pm' : 'am';
138     h = h %% 12;
139     h = h ? h : 12; // the hour '0' should be '12'
140     var m = maybeAddZero(today.getMinutes());
141     var colon = ":";
142     if (today.getSeconds() %% 2 == 0) {
143       colon = "<FONT STYLE='color: #%s; font-size: 4vmin; font-weight: bold'>:</FONT>";
144     }
145     document.getElementById("time").innerHTML = h + colon + m + ampm;
146     document.getElementById("date").innerHTML = today.toDateString();
147     var t = setTimeout(function(){runClock()}, 1000);
148   }
149
150   // Helper method for running the clock.
151   function maybeAddZero(x) {
152     return (x < 10) ? "0" + x : x;
153   }
154
155   // Do something on page load.
156   function addLoadEvent(func) {
157     var oldonload = window.onload;
158     if (typeof window.onload != 'function') {
159       window.onload = func;
160     } else {
161       window.onload = function() {
162         if (oldonload) {
163           oldonload();
164         }
165         func();
166       }
167     }
168   }
169
170   // Sleep thread helper.
171   const sleep = (milliseconds) => {
172     return new Promise(resolve => setTimeout(resolve, milliseconds))
173   }
174
175   var loadedDate = new Date();
176
177   addLoadEvent(zoomScreen);
178   addLoadEvent(runClock);
179   addLoadEvent(lateLoadImages);
180
181   // Runs the countdown line at the bottom and is responsible for
182   // normal page reloads caused by the expiration of a timer.
183   (function countdown() {
184     setTimeout(
185       function() {
186         var now = new Date();
187         var deltaMs = now.getTime() - loadedDate.getTime();
188         var totalMs = %d;
189         var remainingMs = (totalMs - deltaMs);
190
191         if (remainingMs > 0) {
192           var hr = document.getElementById("countdown");
193           var width = (remainingMs / (totalMs - 5000)) * 100.0;
194           if (width > 100) {
195             width = 100;
196           }
197           hr.style.width = " ".concat(width, "%%");
198           hr.style.backgroundColor = "maroon";
199         } else {
200           // Reload unconditionally after 22 sec.
201           window.location.reload();
202         }
203
204         // Brief sleep before doing it all over again.
205         sleep(50).then(() => {
206           countdown();
207         });
208       }, 50)
209   })();
210
211   // Periodically checks for emergency reload events.
212   (function poll() {
213     setTimeout(
214       function() {
215         var xhr = new XMLHttpRequest();
216         xhr.open('GET',
217                  'http://wannabe.house/kiosk/pages/reload_immediately.html');
218         xhr.onload =
219           function() {
220             if (xhr.status === 200) {
221               window.location.reload();
222             } else {
223               sleep(500).then(() => {
224                 poll();
225               });
226             }
227           };
228         xhr.send();
229       }, 500);
230   })();
231 </SCRIPT>
232 </HEAD>
233 <BODY BGCOLOR="#%s">
234     <TABLE style="height:100%%; width:100%%" BORDER=0>
235     <TR HEIGHT=30>
236         <TD ALIGN="left">
237             <DIV id="date">&nbsp;</DIV>
238         </TD>
239         <TD ALIGN="center"><FONT COLOR=#bbbbbb>
240             <DIV id="info"></DIV></FONT>
241         </TD>
242         <TD ALIGN="right">
243             <DIV id="time">&nbsp;</DIV>
244         </TD>
245     </TR>
246     <TR STYLE="vertical-align:top">
247         <TD COLSPAN=3>
248             <DIV ID="content" STYLE="zoom: 1; visibility: hidden;">
249                 <!-- BEGIN main page contents. -->
250 <!--#include virtual=\"%s\"-->
251                 <!-- END main page contents. -->
252             </DIV>
253             <BR>
254             <P ALIGN="right">
255                 <FONT SIZE=2 COLOR=#bbbbbb>%s @ %s ago.</FONT>
256             </P>
257             <HR id="countdown" STYLE="width:0px;
258                                       text-align:left;
259                                       border:none;
260                                       margin-top:0;
261                                       margin-left:0;
262                                       margin-bottom:2px;
263                                       height:5px;
264                                       background-color:#ffffff;">
265         </TD>
266     </TR>
267     </TABLE>
268 </BODY>""" % (bgcolor,
269               constants.refresh_period_sec * 1000,
270               bgcolor,
271               filename,
272               filename,
273               age))
274
275 def thread_invoke_renderers():
276     while True:
277         print("renderer[%s]: invoking all renderers in catalog..." % (
278             utils.timestamp()))
279         for r in renderer_catalog.get_renderers():
280             now = time.time()
281             try:
282                 r.render()
283             except Exception as e:
284                 traceback.print_exc()
285                 print("renderer[%s] unknown exception in %s, swallowing it." % (
286                     utils.timestamp(), r.get_name()))
287             except Error as e:
288                 traceback.print_exc()
289                 print("renderer[%s] unknown error in %s, swallowing it." % (
290                     utils.timestamp(), r.get_name()))
291             delta = time.time() - now
292             if (delta > 1.0):
293                 print("renderer[%s]: Warning: %s's rendering took %5.2fs." % (
294                     utils.timestamp(), r.get_name(), delta))
295         print("renderer[%s]: thread having a little break for %ds..." % (
296             utils.timestamp(), constants.render_period_sec))
297         time.sleep(constants.render_period_sec)
298
299 if __name__ == "__main__":
300     logging.basicConfig()
301     changer_thread = None
302     renderer_thread = None
303     while True:
304         if (changer_thread == None or
305             not changer_thread.is_alive()):
306             print("MAIN[%s] - (Re?)initializing chooser thread..." % (
307                 utils.timestamp()))
308             changer_thread = Thread(target = thread_change_current, args=())
309             changer_thread.start()
310         if (renderer_thread == None or
311             not renderer_thread.is_alive()):
312             print("MAIN[%s] - (Re?)initializing render thread..." % (
313                 utils.timestamp()))
314             renderer_thread = Thread(target = thread_invoke_renderers, args=())
315             renderer_thread.start()
316         time.sleep(60)
317     print("Should never get here.")