-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtimetable.be
More file actions
572 lines (526 loc) · 18 KB
/
timetable.be
File metadata and controls
572 lines (526 loc) · 18 KB
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
# This block is used for live updates
do
import strict
# erases all TimetableWeb global instances.
# hopefully this allows GC to work
for o:global()
if classname(global.(o))=='TimetableWeb'
print('disabing TimetableWeb instance',o)
try
global.(o).disable()
except .. as err, msg
print(o, ".disable() :", err, ",", msg)
end
global.(o)=nil
end
end
global.timetableweb = nil
#print('gc=',tasmota.gc())
end
do
import strict
# erases all Timetable global instances.
# hopefully this allows GC to work
for o:global()
if classname(global.(o))=='Timetable'
print('disabing Timetable instance',o)
try
global.(o).disable()
except .. as err, msg
print(o, ".disable() :", err, ",", msg)
end
global.(o)=nil
end
end
global.timetable = nil
end
# encapsulates vars, strings, helper functions
# and the main class
#def ttable_combo()
do
#
import strict
import string
#import gpio
import json
#
var title='TTABLE :'
var IDXS=['1','2','3','4','5']
def idxcheck(idx)
if idx==nil || idx==0 || idx==1 idx='1' end
idx=str(idx)
if IDXS.find(idx)==nil
return -1
end
return idx
end
def datetime()
var t = tasmota.rtc()['local']
return tasmota.strftime("%d %B %Y %H:%M:%S", t)
end
# Parses the timetable ie '1000 13:55 8: 00'
def parse_timetable(s)
var tt = []
# Adds an element to tt, ensures that the
# timetable is sorted, without duplicates,
# the above example becomes '08:00 10:00 13:55'
def add(e)
if type(e)!='string'
print(e,'is not a string')
return
end
var i=0
while(i<size(tt))
if e<=tt[i] break end
i+=1
end
if i==size(tt) tt.push(e) return end
if e==tt[i] return end
tt.insert(i,e)
end
#
if type(s)!='string' print('parse_timetable : wrong arg ' .. s .. 'type=' .. type(s)) return '' end
s = string.tr(s,'\"\'\n\t\r,.#$-',' ')
if true
var s1
# replaces multiple spaces with a single space
while s != s1
s1=s
s=string.replace(s, ' ', ' ')
end
end
s=string.replace(s,': ',':') # allows ie 10: 00 to be converted to 10:00
s=string.replace(s,' :',':') # the same
s= string.tr(s,':','') # 10:00 -> 1000
s=string.split(s,' ') # '1000 1200' -> ['1000','1200']
for c:s
#if size(c)==3 c='0'+c end
if size(c)!=4
print('Bad time format : ' .. c)
continue
end # TODO msg
if string.tr(c,'0123456789','')!=''
print('Only digits allowed : '..c)
continue
end # todo msg
if int(c[0..1])>23 || int(c[2..3])>59
print('Bad time format : ' .. c)
continue
end
c=c[0..1]+':'+c[2..3] # '1000' -> '10:00' again
add(c) # add the entry in accending order and discards duplicates
end
return tt.concat(' ')
end # func parse_timetable
class Timetable
#
#var pin # this is the relay number in tasmota config
var idx # controls the topic used tt/topic/bell+idx etc
var disabled # disables all functionality bell, timers etc
#
var timetable # example '08:00 09:15 10:00 14:00 15:30'
#
var duration # time in seconds the bell is ringing (number)
var active_days # usually '*' or 'MON-FRI' or '1-5' must be understandabe by tasmota.set_cron()
#
static default_timetable = '08:10 08:55'
static default_duration = 6.0
static default_active_days = '1-5'
def init(idx)
print("init", idx)
self.disabled = false
#self.pin = pin
self.idx = idx
#
#print('relay =', self.pin)
#if idx != '' print('idx =', self.idx) end
print('idx =', self.idx)
# gpio.pin_mode(self.pin, gpio.OUTPUT)
##
do
var settings = self.fetch_disk_settings(true) # true = print verbose messages
#
self.duration = settings[0]
self.timetable = settings[1]
self.active_days = settings[2]
end
#
self.install_cron_entries() # we can do this as timetable amd active_days are loaded
self.set_pulsetime()
#
print('INIT OK')
end
def fetch_disk_settings(p) # p = true- print otherwise quiet
# Gets the settings from the disk and returns a [duration,timetable,active_days] list
#if self.disabled print('fetch_disk_settings: disabled') return end
var saveflag = false
var j # the settings as a map
try
var fn = '/.tt' .. self.idx .. '.json'
if p print('Opening "' .. fn .. '" to get the settings') end
var f = open(fn)
var data = f.read()
f.close()
j = json.load(data)
if classname(j) != 'map'
if p print('Cannot parse JSON') end
j=nil
end
except .. as err, msg
if p print('Error loading settings, using defaults') end
end
#
if j==nil || j=={}
self.save_settings_unc(Timetable.default_duration, Timetable.default_timetable, Timetable.default_active_days )
return [Timetable.default_duration, Timetable.default_timetable, Timetable.default_active_days]
end
#
var duration # = Timetable.default_duration
do
var duration_file = j.find('duration')
if type(duration_file)=='int' || type(duration_file)=='real'
# ok
if p print('Got duration from settings', duration_file) end
duration = self.parse_duration(duration_file) # TODO
if duration != duration_file
if p print('fetch_disk_settings: duration file=', duration_file, 'new=', duration) end
saveflag = true
end
else
# bogus val
if p print('bogus/missing duration :', duration, type(duration)) end
saveflag = true
duration = Timetable.default_duration
end
end
#
var timetable
do
var timetable_file = j.find('timetable')
if type(timetable_file) != 'string'
if p print('bogus/missing timetable : ' .. timetable_file) end
timetable = Timetable.default_timetable
saveflag = true
else
if p print('Got timetable from settings :', timetable_file) end
timetable = parse_timetable(timetable_file)
if size(timetable)==0
if p print('bogus/missing timetable') end
saveflag = true
timetable = Timetable.default_timetable
if p print('Revert to default timetable', timetable) end
end
if timetable != timetable_file
# Can happen only if timetable is edited directly on disk
if p print('fetch_disk_settings: timetable file=', timetable_file, 'new=', timetable) end
saveflag=true
end
end
end
#
# TODO active days_file
var active_days
do
var active_days_file = j.find('active_days')
if type(active_days_file)!='string' && type(active_days_file)!='int'
if p print('bogus/missing active_days :', active_days_file) end
active_days= Timetable.default_active_days
saveflag = true
if p print('Using default active_days =', active_days) end
else
active_days = str(active_days_file)
if p print('Got active_days from settings :', active_days) end
end
end
if saveflag self.save_settings_unc(duration, timetable, active_days) end
return [duration, timetable, active_days]
end
def add_cron_entry(e)
var hh = int(e[0..1])
var mm = int(e[3..4])
var cronjob = '0 ' .. mm .. ' ' .. hh .. ' * * ' .. self.active_days
print("Add cron entry", cronjob, self.cron_id(e))
tasmota.add_cron(cronjob ,/->self.bell_on_with_check(), self.cron_id(e))
end
def save_settings_unc(dur, tt, ad)
var j = {'duration':dur, 'timetable':tt, 'active_days':ad}
try
var f = open('/.tt' .. self.idx .. '.json', 'w')
f.write(json.dump(j))
f.close()
print("Saved settings to flash")
except .. as err, msg
print('save_settings(): Cannot write to flash', err, msg)
end
end
def save_settings()
if self.disabled print('save_settings: disabled') return end
do
var s = self.fetch_disk_settings()
if s[0]==self.duration && s[1]==self.timetable && s[2]==self.active_days
print('No need to save the settings')
return
end
end
self.save_settings_unc(self.duration, self.timetable, self.active_days)
end
def remove_cron_entries()
for c:string.split(self.timetable, ' ')
print('removing', self.cron_id(c))
tasmota.remove_cron(self.cron_id(c))
tasmota.yield()
end
end
def install_cron_entries() # accepts string '1020 1140' or list ['10:20','11:40']
for e:string.split(self.timetable, ' ')
self.add_cron_entry(e)
end
end
def cron_id(c)
# Used to create a unique name for every cronjob
return 'tt' .. self.idx .. '-' .. c
end
def bell_on_with_check()
# This function is called by cron, the difference is the check
# that the ESP32 time is correct, or at least it is set to a reasonable
# value. This check can save us from triggering the bell in a very unconvenient time.
if tasmota.rtc_utc() < 1720000000
print('The system time is wrong')
return
end
self.bell_on()
end
def bell_on()
if self.disabled print('bell_on: disabled') return end
if self.duration < 0.1
print('The bell is disabled')
return
end
#if gpio.digital_read(self.pin) == 0 # return end
# gpio.digital_write(self.pin, 1)
# tasmota.remove_timer(self)
# tasmota.set_timer( int(self.duration*1000) , /->self.bell_off() , self )
#end
#print('The bell is ON')
tasmota.set_power(self.idx-1, true)
end
def bell_off()
if self.disabled print('bell_off: disabled') return end
#tasmota.remove_timer(self)
#gpio.digital_write(self.pin, 0)
#print('The bell is OFF')
tasmota.set_power(self.idx-1, false)
end
def bell_onoff(x)
if self.disabled print('bell_onoff: disabled') return end
x = str(x)
x = string.tr(x, ' \"\'\n\t\r', '')
if x == '1'
self.bell_on()
elif x == '0'
self.bell_off()
else
print('bell_onoff : Use parameter 0/1')
end
end
def parse_duration(dur)
if type(dur)=='int' || type(dur)=='real' dur = str(dur) end
if type(dur) != 'string' print('set_duration: arg is', type(dur) ) return end
dur = string.tr(dur, ',', '.')
do
# we check dur is a valid decimal number
import re
if re.search('^\\s*[0-9]*\\.?[0-9]*\\s*$', dur) == nil
print('Wrong number format', dur)
return
end
end
dur = real(dur)
if dur < 1 dur = 1 end # do not allow the duration to be 0
if dur > 15 dur = 15 end # Maximum 15 seconds, no need for more
dur = int(dur*10+0.5)*1.0/10 # we need 1 decimal place maximum
return dur
end
def set_duration(dur) # accepts real, integer or string
if self.disabled print('set_duration: disabled') return end
dur = self.parse_duration(dur)
if dur==nil || self.duration == dur # berry can correctly do comparisons with reals, I dont know how, but it works
return
end
self.duration = dur
self.set_pulsetime()
self.save_settings()
print('New duartion', dur, 'saved')
end
def set_pulsetime()
if self.duration<1.0
#pulsetime(10)
tasmota.cmd("pulsetime"..self.idx.." 10")
return
end
if self.duration<=11.1
var p = int(self.duration *10+0.5)
tasmota.cmd("pulsetime"..self.idx.." "..p)
return
end
var duration=int(self.duration+0.5)
if duration<12
duration=12
end
tasmota.cmd("pulsetime"..self.idx.." "..(duration+100))
end
def set_timetable(tt)
if type(tt) != 'string' print('Cannot parse',tt, type(tt)) return end
tt = parse_timetable(tt)
if size(tt)==0
print('Cannot parse', tt)
return
end
if tt == self.timetable ## string NEW TODO .concat(' ')
#print('The timetable is the same bit by bit')
#self.update_timetable_mqtt()
return
end
self.remove_cron_entries() # removes the old cron
#
self.timetable = tt
self.save_settings() # saves the new timetable
self.install_cron_entries() # creates the new cron
#
end
def parse_active_days(active_days)
if active_days == nil return self.active_days end
active_days = string.tr(active_days,' \"\'\n\t\r', '')
if active_days == '' return self.active_days end
tasmota.remove_cron('test')
try
tasmota.add_cron("0 0 0 * * "+active_days, def() end , 'test') # empty closure for test
except 'value_error'
print('Invalid active days')
return self.active_days
end
tasmota.remove_cron('test')
return active_days
end
def set_active_days(active_days_raw)
var active_days = self.parse_active_days(active_days_raw)
if size(active_days) == 0 || self.active_days == active_days
# print('Not replacing active days')
return
end
self.remove_cron_entries()
self.active_days = active_days
self.save_settings()
self.install_cron_entries()
#self.update_active_days_mqtt()
print('active days updated')
end
def disable() # Releases recourses to be garbage collected by BerryVM
if global.('tt' .. self.idx) != self return end
self.remove_cron_entries()
self.bell_off()
self.disabled = true
end
def deinit()
if !self.disabled self.disable() end
print(self, 'deinit()')
end
end # class timetable
def tt_generator(idx)
#idx = idxcheck(idx)
#if idx==-1
# print('Wrong index, must be ', IDXS)
# return
#end
if global.('tt' .. idx) != nil
print('global var', 'tt' .. idx, 'is used')
return
end
#if type(pin)!='int' || pin<0 || pin>30
# print(title,'Wrong PIN, 0-30 accepted, be careful many pins are unusable', pin)
# return
#end
print('Creating timetable :', 'tt'..idx)
global.('tt'..idx) = Timetable(idx)
end # tt_generator
import webserver
def webpage_show(idx)
if !webserver.check_privileged_access() return nil end
var t = global.('tt'..idx)
webserver.content_start("Timetable Settings"..idx) # title of the web page
webserver.content_send_style() # standard Tasmota style
if webserver.arg_size()==1
print('arg0=',webserver.arg(0))
if webserver.arg(0)=='1'
t.bell_on()
end
elif webserver.arg_size()==3
var timetable = webserver.arg(0)
var duration = webserver.arg(1)
var active_days = webserver.arg(2)
t.set_active_days(active_days)
t.set_duration(duration)
t.set_timetable(timetable)
webserver.content_send('<p style="text-align:center; background-color: green; color: white;">The settings are stored</p>')
end
if global.ds3231 != nil && global.ds3231.active()
webserver.content_send('<p style="text-align:center">DS3231 is working</p>')
else
webserver.content_send('<p style="text-align:center; background-color: red; color: white;">DS3231 not found</p>')
end
webserver.content_send('<p style="text-align:center">Local Time (Refresh the page to update) : ')
webserver.content_send(datetime())
webserver.content_send('</p>')
webserver.content_send('<br><button onclick="location.href=\'/tt' .. idx .. '?bell=1\'" style="background-color:red;">Ring the bell</button><br><br>')
webserver.content_send('<form action="/tt' .. idx .. '" id="ttform">')
webserver.content_send('<label for="tt">Timetable ' .. idx .. ' (24h format, can be ie 08:50 or 0850) :</label>')
webserver.content_send('<input type="text" id="tt" name="tt" value="'+t.timetable+'"><br><br>')
webserver.content_send('<label for="dur">Bell duration: (5 or 4.5 etc seconds)</label><input type="text" id="dur" name="dur" value="' .. t.duration .. '"><br><br>')
webserver.content_send('<label for="ad">Active Days (1-5 means MON-FRI, * means all days)</label><input type="text" id="ad" name="ad" value="' .. t.active_days .. '"><br><br>')
webserver.content_send('</form>')
webserver.content_send('<button type="submit" form="ttform">Save settings ' .. idx .. '</button>')
webserver.content_button(webserver.BUTTON_MAIN)
webserver.content_stop()
end
class TimetableWeb
var idx
def init(idx)
self.idx = idx
if global.('tt'+self.idx)==nil
print('Error : timetable tt' .. self.idx, 'not found')
return
end
tasmota.add_driver(self)
if tasmota.wifi('up')
self.web_add_handler()
end
end
def web_add_main_button()
webserver.content_send('<button onclick="location.href=\'/tt' .. self.idx .. '\'">School Timer ' .. self.idx .. '</button><br><br>')
end
def web_add_handler()
webserver.on('/tt' .. self.idx, /-> webpage_show(self.idx))
print('Created web page for tt' .. self.idx)
end
def disable()
webserver.on('/tt' .. self.idx, / -> nil)
tasmota.remove_driver(self)
end
def deinit()
print(self, 'deinit()')
end
end
def web_generator(idx)
idx = idxcheck(idx)
if idx==-1 print('Wrong index, must be ', IDXS) return end
if global.('tt' .. idx) == nil print('Timetable is missing, not creating web interface') return end
if global.('ttweb' .. idx) != nil print('ttweb' .. idx,'already exists, not creating web') return end
global.('ttweb' .. idx) = TimetableWeb(idx)
end
def start_timetable(idx)
tt_generator(idx)
web_generator(idx)
end
start_timetable(1)
global.start_timetable = start_timetable
end