-
-
Notifications
You must be signed in to change notification settings - Fork 156
/
sip.py
337 lines (303 loc) · 13.8 KB
/
sip.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
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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# standard library imports
import ast
# from calendar import timegm
from datetime import date
import i18n
import json
import math # check for infinity: math.isinf(math.inf)
import os
import subprocess
import sys
from threading import Thread
import time
sip_path = os.path.dirname(os.path.abspath(__file__))
os.chdir(sip_path)
# local module imports
import gv
from gpio_pins import set_output
from helpers import (
check_rain,
get_rpi_revision,
jsave,
log_run,
plugin_adjustment,
prog_match,
report_new_day,
report_station_completed,
report_running_program_change,
report_rain_delay_change,
schedule_stations,
station_names,
stop_onrain,
restart,
convert_temp,
temp_string,
days_since_epoch
)
from ReverseProxied import ReverseProxied
from urls import urls # Provides access to URLs for UI pages
import web # the Web.py module. See webpy.org (Enables the Python SIP web interface)
sys.path.append("./plugins")
gv.restarted = 1
def timing_loop():
""" ***** Main timing algorithm. Runs in a separate thread.***** """
print(_("Starting timing loop") + "\n")
prior_min = 0
while True: # infinite loop
cur_ord = (
date.today().toordinal() # day of year
)
if cur_ord > gv.day_ord:
gv.day_ord = cur_ord
report_new_day()
gv.dse = days_since_epoch()
gv.now = round(time.time()) # Current time as seconds since the epoch, in UTC. Updated once per second.
lt = time.localtime(gv.now) # Current time as time struct.
if gv.sd["mas"]: # If master is defined
masid = gv.sd["mas"] -1 # master station index
else:
masid = None
if (gv.sd["en"]
and not gv.sd["mm"]
and (not gv.sd["bsy"] or not gv.sd["seq"])
):
this_min = (lt.tm_hour * 60) + lt.tm_min
if this_min != prior_min: # only check programs once a minute
prior_min = this_min
for i, p in enumerate(gv.pd): # get both index and prog item
start_triggers = [ p["start_min"] ]
if p["cycle_min"] != 0:
recurring_start = p["start_min"]
while recurring_start < p["stop_min"]:
recurring_start += p["cycle_min"]
start_triggers.append(recurring_start)
if this_min in start_triggers:
if prog_match(p) and any(p["duration_sec"]):
# check each station per boards listed in program up to number of boards in Options
for b in range(len(p["station_mask"])): # len is number of bytes
for s in range(8):
sid = b * 8 + s # station index
if (gv.srvals[sid]
and gv.sd["seq"]
):
continue # skip if currently on and sequential mode
if sid == masid:
continue # skip, this is master station
# station duration conditionally scaled by "water level"
if gv.sd["iw"][b] & 1 << s: # If ignore water level.
duration_adj = 1.0
if gv.sd["idd"]: # If individual duration per station.
duration = p["duration_sec"][sid]
else:
duration = p["duration_sec"][0]
else:
duration_adj = (
gv.sd["wl"] / 100.0
) * plugin_adjustment()
if gv.sd["idd"]:
duration = (
p["duration_sec"][sid] * duration_adj
)
else:
duration = p["duration_sec"][0] * duration_adj
duration = round(duration) # convert to int
if (
p["station_mask"][b] & 1 << s # if this station is scheduled in this program
and duration # station has a duration
):
gv.rs[sid][2] = duration
gv.rs[sid][3] = i + 1 # program number for scheduling
gv.ps[sid][0] = i + 1 # program number for display
gv.ps[sid][1] = duration
schedule_stations(p["station_mask"]) # -> helpers, turns on gv.sd["bsy"]
if gv.sd["bsy"]:
for b in range(gv.sd["nbrd"]): # Check each station once a second
for s in range(8):
sid = b * 8 + s # station index
if gv.srvals[sid]: # if this station is on
if gv.now >= gv.rs[sid][1]: # if time is up
gv.srvals[sid] = 0
set_output()
gv.sbits[b] &= ~(1 << s)
if sid != masid: # if not master, fill out log
gv.ps[sid] = [0, 0]
gv.lrun[0] = sid
gv.lrun[1] = gv.rs[sid][3]
gv.lrun[2] = int(gv.now - gv.rs[sid][0])
log_run()
report_station_completed(sid + 1)
gv.rs[sid] = [0, 0, 0, 0]
else: # if this station is not yet on
if (gv.now >= gv.rs[sid][0]
and gv.now < gv.rs[sid][1]
):
if sid != masid: # if not master
if (gv.sd["mo"][b] & (1 << s) # station activates master
and gv.sd["mton"] < 0 # master has a negative delay
and not gv.srvals[masid] # master is not on
):
# advance remaining stations start and stop times by master negative delay
for stn in range(sid, len(gv.rs)):
if gv.rs[stn][3]: # If station has a duration
gv.rs[stn][0] += abs(gv.sd["mton"])
gv.rs[stn][1] += abs(gv.sd["mton"])
brd = masid // 8
gv.sbits[brd] |= 1 << (masid - (brd * 8)) # start master
gv.srvals[masid] = 1
set_output()
else:
gv.srvals[sid] = 1 # station is turned on
set_output()
gv.sbits[b] |= 1 << s # Set display to on
gv.ps[sid][0] = gv.rs[sid][3]
gv.ps[sid][1] = gv.rs[sid][2]
if (gv.sd["mas"] # Master is defined
and gv.sd["mo"][b] & (1 << s) # this station activates master.
): # Master settings
if gv.sd["mton"] > 0:
gv.rs[masid][0] = gv.rs[sid][0] + gv.sd["mton"] # this is where master is scheduled
gv.rs[masid][1] = gv.rs[sid][1] + gv.sd["mtoff"]
gv.rs[masid][3] = gv.rs[sid][3]
elif sid == masid: # if this is master
gv.sbits[b] |= 1 << s
gv.srvals[sid] = 1 # this is where master is turned on
set_output()
program_running = False
pon = None
for sid in range(gv.sd["nst"]):
if gv.rs[sid][1]: # if any station is scheduled
program_running = True
pon = gv.rs[sid][3]
break
if pon != gv.pon: # Update number of running program
gv.pon = pon
report_running_program_change()
if program_running:
if (gv.sd["urs"]
and gv.sd["rs"] # Stop stations if use rain sensor and rain detected.
):
stop_onrain() # Clear schedule for stations that do not ignore rain.
for sid in range(len(gv.rs)): # loop through program schedule (gv.ps)
if gv.rs[sid][2] == 0: # skip stations with no duration
continue
if gv.srvals[sid]:
# If station is on, decrement time remaining display
if gv.ps[sid][1] > 0: # if time is left
gv.ps[sid][1] -= 1
if not program_running:
gv.srvals = [0] * (gv.sd["nst"])
set_output()
gv.rovals = [0] * gv.sd["nst"]
gv.sbits = [0] * (gv.sd["nbrd"] + 1)
gv.ps = []
for i in range(gv.sd["nst"]):
gv.ps.append([0, 0])
gv.sd["bsy"] = 0
if (gv.sd["mas"] # master is defined
and (gv.sd["mm"] or not gv.sd["seq"]) # manual or concurrent mode.
):
for b in range(gv.sd["nbrd"]): # set stop time for master
for s in range(8):
sid = b * 8 + s
if (
gv.sd["mas"] != sid + 1 # if not master
and gv.srvals[sid] # station is on
and gv.rs[sid][1]
>= gv.now # station has a stop time >= now
and gv.sd["mo"][b] & (1 << s) # station activates master
):
gv.rs[gv.sd["mas"] - 1][1] = (
gv.rs[sid][1] + gv.sd["mtoff"]
) # set to future...
break # first found will do
else: # Not busy
if gv.pon != None:
gv.pon = None
gv.rn = 0
report_running_program_change()
if gv.sd["urs"]:
check_rain() # in helpers.py
if gv.sd["rd"] and gv.now >= gv.sd["rdst"]: # Check if rain delay time is up
gv.sd["rd"] = 0
gv.sd["rdst"] = 0 # Rain delay stop time
jsave(gv.sd, "sd")
report_rain_delay_change()
time.sleep(1)
#### End of timing loop ####
class SIPApp(web.application):
"""Allow program to select HTTP port."""
def run(
self, port=gv.sd["htp"], ip=gv.sd["htip"], *middleware
): # get port number from options settings
func = self.wsgifunc(*middleware)
func = ReverseProxied(func)
return web.httpserver.runsimple(func, (ip, port))
app = SIPApp(urls, globals())
web.config.debug = False # Improves page load speed
web.config._session = web.session.Session(
app, web.session.DiskStore("sessions"), initializer={"user": "anonymous"}
)
template_globals = {
"gv": gv,
"str": str,
"eval": eval,
"convert_temp": convert_temp,
"temp_string" : temp_string,
"session": web.config._session,
"json": json,
"ast": ast,
"_": _,
"i18n": i18n,
"app_path": lambda p: web.ctx.homepath + p,
"web": web,
"round": round,
"time": time,
# "timegm": timegm,
}
template_render = web.template.render(
"templates", globals=template_globals, base="base"
)
if __name__ == "__main__":
#########################################################
#### Code to import all webpages and plugin webpages ####
import plugins
try:
print(_("plugins loaded:"))
except Exception as e:
print("Import plugins error", e)
pass
for name in plugins.__all__:
print(" ", name)
gv.plugin_menu.sort(key=lambda entry: entry[0])
# Keep plugin manager at top of menu
try:
for i, item in enumerate(gv.plugin_menu):
if "/plugins" in item:
gv.plugin_menu.pop(i)
except Exception as e:
print("Creating plugins menu", e)
pass
tl = Thread(target=timing_loop)
tl.daemon = True
tl.start()
if gv.use_gpio_pins:
set_output()
app.notfound = lambda: web.seeother("/")
###########################
#### For HTTPS (SSL): ####
if gv.sd["htp"] == 443:
try:
from cheroot.server import HTTPServer
from cheroot.ssl.builtin import BuiltinSSLAdapter
HTTPServer.ssl_adapter = BuiltinSSLAdapter(
certificate='/usr/lib/ssl/certs/SIP.crt',
private_key='/usr/lib/ssl/private/SIP.key'
)
except IOError as e:
gv.sd["htp"] = int(80)
jsave(gv.sd, "sd")
print("SSL error", e)
restart()
app.run()