Initial commit
[kiosk.git] / kiosk.py
1 #!/usr/local/bin/python
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                 f = open(os.path.join(constants.pages_dir,
33                                       "current.shtml"), "w")
34                 emit_wrapped(f, page)
35                 f.close()
36
37                 # Notify XMLHTTP clients that they need to refresh now.
38                 path = os.path.join(constants.pages_dir,
39                                     "reload_immediately.html")
40                 f = open(path, 'w')
41                 f.write("Reload, suckers!")
42                 f.close()
43                 time.sleep(0.750)
44                 os.remove(path)
45                 last_page = page
46                 swap_page_target = now + constants.refresh_period_sec
47
48         elif now >= swap_page_target:
49             if (page == last_page):
50                 print('chooser[%s] - nominal choice got the same page...' % (
51                     utils.timestamp()))
52                 continue
53             print('chooser[%s] - nominal choice of %s' % (utils.timestamp(), page))
54             try:
55                 f = open(os.path.join(constants.pages_dir,
56                                       "current.shtml"), "w")
57                 emit_wrapped(f, page)
58                 f.close()
59                 last_page = page
60                 swap_page_target = now + constants.refresh_period_sec
61             except:
62                 print('chooser[%s] - page does not exist?!' % (utils.timestamp()))
63                 continue
64         time.sleep(1.0)
65
66 def pick_background_color():
67     now = datetime.now()
68     if now.hour <= 6 or now.hour >= 21:
69         return "E6B8B8"
70     elif now.hour == 7 or now.hour == 20:
71         return "EECDCD"
72     else:
73         return "FFFFFF"
74
75 def emit_wrapped(f, filename):
76     age = utils.describe_age_of_file_briefly("pages/%s" % filename)
77     bgcolor = pick_background_color()
78     f.write("""
79 <HEAD>
80   <TITLE>Kitchen Kiosk</TITLE>
81   <LINK rel="stylesheet" type="text/css" href="style.css">
82   <SCRIPT TYPE="text/javascript">
83
84   // Zoom the 'contents' div to fit without scrollbars and then make
85   // it visible.
86   function zoomScreen() {
87     z = 285;
88     do {
89       document.getElementById("content").style.zoom = z+"%%";
90       var body = document.body;
91       var html = document.documentElement;
92       var height = Math.max(body.scrollHeight,
93                             body.offsetHeight,
94                             html.clientHeight,
95                             html.scrollHeight,
96                             html.offsetHeight);
97       var windowHeight = window.innerHeight;
98       var width = Math.max(body.scrollWidth,
99                            body.offsetWidth,
100                            html.clientWidth,
101                            html.scrollWidth,
102                            html.offsetWidth);
103       var windowWidth = window.innerWidth;
104       var heightRatio = height / windowHeight;
105       var widthRatio = width / windowWidth;
106
107       if (heightRatio <= 1.0 && widthRatio <= 1.0) {
108         break;
109       }
110       z -= 4;
111     } while(z >= 70);
112     document.getElementById("content").style.visibility = "visible";
113   }
114
115   // Load IMG tags with DATA-SRC attributes late.
116   function lateLoadImages() {
117     var image = document.getElementsByTagName('img');
118     for (var i = 0; i < image.length; i++) {
119       if (image[i].getAttribute('DATA-SRC')) {
120         image[i].setAttribute('SRC', image[i].getAttribute('DATA-SRC'));
121       }
122     }
123   }
124
125   // Operate the clock at the top of the page.
126   function runClock() {
127     var today = new Date();
128     var h = today.getHours();
129     var ampm = h >= 12 ? 'pm' : 'am';
130     h = h %% 12;
131     h = h ? h : 12; // the hour '0' should be '12'
132     var m = maybeAddZero(today.getMinutes());
133     var colon = ":";
134     if (today.getSeconds() %% 2 == 0) {
135       colon = "<FONT STYLE='color: #%s; font-size: 4vmin; font-weight: bold'>:</FONT>";
136     }
137     document.getElementById("time").innerHTML = h + colon + m + ampm;
138     document.getElementById("date").innerHTML = today.toDateString();
139     var t = setTimeout(function(){runClock()}, 1000);
140   }
141
142   // Helper method for running the clock.
143   function maybeAddZero(x) {
144     return (x < 10) ? "0" + x : x;
145   }
146
147   // Do something on page load.
148   function addLoadEvent(func) {
149     var oldonload = window.onload;
150     if (typeof window.onload != 'function') {
151       window.onload = func;
152     } else {
153       window.onload = function() {
154         if (oldonload) {
155           oldonload();
156         }
157         func();
158       }
159     }
160   }
161
162   // Sleep thread helper.
163   const sleep = (milliseconds) => {
164     return new Promise(resolve => setTimeout(resolve, milliseconds))
165   }
166
167   var loaded = false;
168   var loadedDate = new Date();
169
170   addLoadEvent(zoomScreen);
171   addLoadEvent(runClock);
172   addLoadEvent(lateLoadImages);
173   addLoadEvent(function() {
174     loaded = true;
175   });
176
177   // Reload the page after a certain amount of time has passed or
178   // immediately if told to do so.
179   (function poll() {
180     setTimeout(
181       function() {
182         var now = new Date();
183         var deltaMs = now.getTime() - loadedDate.getTime();
184
185         // Reload unconditionally after 22 sec.
186         if (deltaMs > %d) {
187           window.location.reload();
188         }
189
190         // Reload immediately if told.
191         var xhr = new XMLHttpRequest();
192         xhr.open('GET',
193                  'http://wannabe.house/kiosk/pages/reload_immediately.html');
194         xhr.onload =
195           function() {
196             if (xhr.status === 200) {
197               window.location.reload();
198             } else {
199               sleep(500).then(() => {
200                 poll();
201               });
202             }
203           };
204         xhr.send();
205       }, 500);
206   })();
207 </SCRIPT>
208 </HEAD>
209 <BODY BGCOLOR="#%s">
210     <TABLE style="height:100%%; width:100%%" BORDER=0>
211     <TR HEIGHT=30>
212         <TD ALIGN="left">
213             <DIV id="date">&nbsp;</DIV>
214         </TD>
215         <TD ALIGN="center"><FONT COLOR=#bbbbbb>
216             <DIV id="info"></DIV></FONT>
217         </TD>
218         <TD ALIGN="right">
219             <DIV id="time">&nbsp;</DIV>
220         </TD>
221     </TR>
222     <TR STYLE="vertical-align:top">
223         <TD COLSPAN=3>
224             <DIV ID="content" STYLE="zoom: 1; visibility: hidden;">
225                 <!-- BEGIN main page contents. -->
226 <!--#include virtual=\"%s\"-->
227                 <!-- END main page contents. -->
228             </DIV>
229             <BR>
230             <P ALIGN="right">
231                 <FONT SIZE=2 COLOR=#bbbbbb>%s @ %s ago.</FONT>
232             </P>
233         </TD>
234     </TR>
235     </TABLE>
236 </BODY>""" % (bgcolor,
237               constants.refresh_period_sec * 1000,
238               bgcolor,
239               filename,
240               filename,
241               age))
242
243 def thread_invoke_renderers():
244     while True:
245         for r in renderer_catalog.get_renderers():
246             try:
247                 r.render()
248             except Exception as e:
249                 traceback.print_exc()
250                 print("renderer[%s] unknown exception, swallowing it." % (
251                     utils.timestamp()))
252             except Error as e:
253                 traceback.print_exc()
254                 print("renderer[%s] unknown error, swallowing it." % (
255                     utils.timestamp()))
256         time.sleep(constants.render_period_sec)
257
258 if __name__ == "__main__":
259     logging.basicConfig()
260     changer_thread = None
261     renderer_thread = None
262     while True:
263         if (changer_thread == None or
264             not changer_thread.is_alive()):
265             print("chooser[%s] - (Re?)initializing chooser thread..." % utils.timestamp())
266             changer_thread = Thread(target = thread_change_current, args=())
267             changer_thread.start()
268         if (renderer_thread == None or
269             not renderer_thread.is_alive()):
270             print("renderer[%s] - (Re?)initializing render thread..." % utils.timestamp())
271             renderer_thread = Thread(target = thread_invoke_renderers, args=())
272             renderer_thread.start()
273         time.sleep(10000)
274     print("Should never get here.")