/
schedule.py
382 lines (320 loc) · 13.7 KB
/
schedule.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
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
"""
-*- test-case-name: PyHouse.src.Modules.Scheduling.test.test_schedule -*-
@name: PyHouse/src/Modules/Scheduling/schedule.py
@author: D. Brian Kimmel
@contact: D.BrianKimmel@gmail.com
@copyright: (c) 2013-2018 by D. Brian Kimmel
@license: MIT License
@note: Created on Apr 8, 2013
@summary: Schedule events
Handle the home automation system schedule for a house.
The schedule is at the Core of PyHouse.
Lighting events, entertainment events, etc. for one house are triggered by the schedule and are run by twisted.
Read/reread the schedule file at:
1. Start up
2. Midnight
3. After each set of scheduled events.
Controls:
Communication
Entertainment
HVAC
Irrigation
Lighting
Pool
Security
UPNP
Operation:
Iterate thru the schedule tree and create a list of schedule events.
Select the next event(s) from now, there may be more than one event scheduled for the same time.
Create a twisted timer that goes off when the scheduled time arrives.
We only create one timer (ATM) so that we do not have to cancel timers when the schedule is edited.
"""
__updated__ = '2018-03-26'
# Import system type stuff
import datetime
import dateutil.parser as dparser
# sudo pip install hexdump aniso8601
import aniso8601
# Import PyMh files
from Modules.Housing.Hvac.hvac_actions import API as hvacActionsAPI
from Modules.Housing.Irrigation.irrigation_action import API as irrigationActionsAPI
from Modules.Housing.Lighting.lighting_actions import API as lightActionsAPI
from Modules.Housing.Scheduling.schedule_xml import Xml as scheduleXml
from Modules.Housing.Scheduling import sunrisesunset
from Modules.Computer import logging_pyh as Logger
LOG = Logger.getLogger('PyHouse.Schedule ')
SECONDS_IN_MINUTE = 60
SECONDS_IN_HOUR = SECONDS_IN_MINUTE * 60 # 3600
SECONDS_IN_DAY = SECONDS_IN_HOUR * 24 # 86400
SECONDS_IN_WEEK = SECONDS_IN_DAY * 7 # 604800
INITIAL_DELAY = 5 # Must be from 5 to 30 seconds.
PAUSE_DELAY = 5
def to_minutes(p_datetime):
return (p_datetime.hour * 60 + p_datetime.minute)
def _get_schedule_timefield(p_schedule_obj):
"""
"""
l_timefield = p_schedule_obj.Time.lower()
try:
# l_time = dparser.parse(l_timefield, fuzzy=True) # Worked ok in Python2
l_time = aniso8601.parse_time(l_timefield)
except ValueError:
l_time = datetime.time(0)
return l_time, l_timefield
class RiseSet(object):
def __init__(self):
self.SunRise = None
self.SunSet = None
class SchedTime(object):
"""
Get the when scheduled time. It may be from about a minute to about 1 week.
If the schedule is not active return a None
This class deals with extracting information from the time and DOW fields of a schedule.
DOW mon=1, tue=2, wed=4, thu=8, fri=16, sat=32, sun=64
weekday mon=0, tue=1, wed=2, thu=3, fri=4, sat=5, sun=6
The time field may be:
HH:MM or HH:MM:SS
sunrise/sunset/dawn/dusk +/- offset HH:MM:SS or HH:MM
"""
@staticmethod
def _extract_days(p_schedule_obj, p_now):
""" Get the number of days until the next DOW in the schedule.
DOW mon=1, tue=2, wed=4, thu=8, fri=16, sat=32, sun=64
weekday() mon=0, tue=1, wed=2, thu=3, fri=4, sat=5, sun=6
@param p_schedule_obj: is the schedule object we are working on
@param p_now: is a datetime.datetime.now()
@return: the number of days till the next DOW - 0..6, 10 if never
"""
l_dow = p_schedule_obj.DOW
l_now_day = p_now.weekday()
l_day = 2 ** l_now_day
l_is_in_dow = (l_dow & l_day) != 0
if l_is_in_dow:
return 0
l_days = 1
for _l_ix in range(0, 7):
l_now_day = (l_now_day + 1) % 7
l_day = 2 ** l_now_day
l_is_in_dow = (l_dow & l_day) != 0
if l_is_in_dow:
return l_days
l_days += 1
return 10
@staticmethod
def _extract_schedule_time(p_schedule_obj, p_rise_set):
""" Find the number of minutes from midnight until the schedule time for action.
Possible valid formats are:
hh:mm:ss
hh:mm
sunrise
sunrise + hh:mm
sunrise - hh:mm
@return: the number of minutes
"""
l_timefield = p_schedule_obj.Time.lower()
if 'dawn' in l_timefield:
# print('Dawn - {}'.format(l_timefield))
l_base = to_minutes(p_rise_set.Dawn)
l_timefield = l_timefield[4:]
elif 'sunrise' in l_timefield:
# print('SunRise - {}'.format(l_timefield))
l_base = to_minutes(p_rise_set.SunRise)
l_timefield = l_timefield[7:]
elif 'noon' in l_timefield:
# print('Noon - {}'.format(l_timefield))
l_base = to_minutes(p_rise_set.Noon)
l_timefield = l_timefield[4:]
elif 'sunset' in l_timefield:
# print('SunSet - {}'.format(l_timefield))
l_base = to_minutes(p_rise_set.SunSet)
l_timefield = l_timefield[6:]
elif 'dusk' in l_timefield:
# print('Dusk - {}'.format(l_timefield))
l_base = to_minutes(p_rise_set.Dusk)
l_timefield = l_timefield[4:]
else:
l_base = 0
l_timefield = l_timefield.strip()
# print('==time== - {}'.format(l_timefield))
l_subflag = False
if '-' in l_timefield:
# print(" found - ")
l_subflag = True
l_timefield = l_timefield[1:]
elif '+' in l_timefield:
# print(" found + ")
l_subflag = False
l_timefield = l_timefield[1:]
l_timefield = l_timefield.strip()
try:
# l_time = dparser.parse(l_timefield, fuzzy=True)
l_time = aniso8601.parse_time(l_timefield)
# print('Parsable time field "{}" = "{}"'.format(l_timefield, l_time))
except ValueError:
# print('Unparsable time field "{}"'.format(l_timefield))
l_time = datetime.time(0)
l_offset = to_minutes(l_time)
#
#
if l_subflag:
l_minutes = l_base - l_offset
else:
l_minutes = l_base + l_offset
#
return l_minutes
@staticmethod
def extract_time_to_go(_p_pyhouse_obj, p_schedule_obj, p_now, p_rise_set):
""" Compute the seconds to go from now to the next scheduled time.
@param p_pyhouse_obj: Not used yet
@param p_schedule_obj: is the schedule object we are working on.
@param p_now: is the datetime for now.
@param p_rise_set: is the sunrise/sunset structure.
@return: The number of seconds from now to the scheduled time.
"""
l_dow_mins = SchedTime._extract_days(p_schedule_obj, p_now) * 24 * 60
l_sched_mins = SchedTime._extract_schedule_time(p_schedule_obj, p_rise_set)
l_sched_secs = 60 * (l_dow_mins + l_sched_mins)
l_now_secs = to_minutes(p_now) * 60
l_seconds = l_sched_secs - l_now_secs
if l_seconds < 0:
l_seconds += SECONDS_IN_DAY
return l_seconds
class ScheduleExecution(object):
@staticmethod
def dispatch_one_schedule(p_pyhouse_obj, p_schedule_obj):
"""
Send information to one device to execute a schedule.
"""
if p_schedule_obj.ScheduleType == 'Lighting':
LOG.info('Execute_one_schedule type = Lighting')
lightActionsAPI.DoSchedule(p_pyhouse_obj, p_schedule_obj)
#
elif p_schedule_obj.ScheduleType == 'Hvac':
LOG.info('Execute_one_schedule type = Hvac')
hvacActionsAPI.DoSchedule(p_pyhouse_obj, p_schedule_obj)
#
elif p_schedule_obj.ScheduleType == 'Irrigation':
LOG.info('Execute_one_schedule type = Hvac')
irrigationActionsAPI.DoSchedule(p_pyhouse_obj, p_schedule_obj)
#
elif p_schedule_obj.ScheduleType == 'TeStInG14159': # To allow a path for unit tests
LOG.info('Execute_one_schedule type = Testing')
# irrigationActionsAPI.DoSchedule(p_pyhouse_obj, p_schedule_obj)
#
else:
LOG.error('Unknown schedule type: {}'.format(p_schedule_obj.ScheduleType))
irrigationActionsAPI.DoSchedule(p_pyhouse_obj, p_schedule_obj)
@staticmethod
def execute_schedules_list(p_pyhouse_obj, p_key_list = []):
""" The timer calls this when a list of schedules is due to be executed.
For each Schedule in the list, call the dispatcher for that type of schedule.
Delay before generating the next schedule to avoid a race condition
that duplicates an event if it completes before the clock goes to the next second.
@param p_key_list: a list of schedule keys in the next time schedule to be executed.
"""
LOG.info("About to execute - Schedules:{}".format(p_key_list))
for l_slot in range(len(p_key_list)):
l_schedule_obj = p_pyhouse_obj.House.Schedules[p_key_list[l_slot]]
ScheduleExecution.dispatch_one_schedule(p_pyhouse_obj, l_schedule_obj)
Utility.schedule_next_event(p_pyhouse_obj)
class Utility(object):
"""
"""
@staticmethod
def _setup_components(p_pyhouse_obj):
# p_pyhouse_obj.House.Schedules = {}
pass
@staticmethod
def fetch_sunrise_set(p_pyhouse_obj):
l_riseset = p_pyhouse_obj.House.Location.RiseSet # RiseSetData()
LOG.info('Got Sunrise: {}; Sunset: {}'.format(l_riseset.SunRise, l_riseset.SunSet))
# p_pyhouse_obj.APIs.Computer.MqttAPI.MqttPublish('schedule/sunrise_set', l_riseset)
return l_riseset
@staticmethod
def find_next_scheduled_events(p_pyhouse_obj, p_now):
""" Go thru all the schedules and find the next schedule list to run.
Note that there may be several scheduled events for that time
@param p_now: is a datetime of now()
"""
l_schedule_key_list = []
l_min_seconds = SECONDS_IN_WEEK
l_riseset = Utility.fetch_sunrise_set(p_pyhouse_obj)
for l_key, l_schedule_obj in p_pyhouse_obj.House.Schedules.items():
if not l_schedule_obj.Active:
continue
l_seconds = SchedTime.extract_time_to_go(p_pyhouse_obj, l_schedule_obj, p_now, l_riseset)
if l_seconds < 30:
continue
if l_min_seconds == l_seconds: # Add to lists for the given time.
l_schedule_key_list.append(l_key)
elif l_seconds < l_min_seconds: # earlier schedule - start new list
l_min_seconds = l_seconds
l_schedule_key_list = []
l_schedule_key_list.append(l_key)
l_debug_msg = "Delaying {} for list {}".format(l_min_seconds, l_schedule_key_list)
LOG.info("find_next_scheduled_events complete. {}".format(l_debug_msg))
return l_min_seconds, l_schedule_key_list
@staticmethod
def run_after_delay(p_pyhouse_obj, p_delay, p_list):
l_runID = p_pyhouse_obj.Twisted.Reactor.callLater(p_delay, ScheduleExecution.execute_schedules_list, p_pyhouse_obj, p_list)
l_datetime = datetime.datetime.fromtimestamp(l_runID.getTime())
LOG.info('Scheduled {} after delay of {} - Time: {}'.format(p_list, p_delay, l_datetime))
return l_runID
@staticmethod
def schedule_next_event(p_pyhouse_obj, p_delay = 0):
""" Find the list of schedules to run, call the timer to run at the time in the schedules.
@param p_pyhouse_obj: is the grand repository of information
@param p_delay: is the (forced) delay time for the timer.
"""
l_delay, l_list = Utility.find_next_scheduled_events(p_pyhouse_obj, datetime.datetime.now())
if p_delay != 0:
l_delay = p_delay
Utility.run_after_delay(p_pyhouse_obj, l_delay, l_list)
class Timers(object):
"""
"""
def __init__(self, p_pyhouse_obj):
self.m_pyhouse_obj = p_pyhouse_obj
self.m_timers = {}
self.m_count = 0
def set_one(self, p_pyhouse_obj, p_delay, p_list):
l_callback = ScheduleExecution.execute_schedules_list
l_runID = p_pyhouse_obj.Twisted.Reactor.callLater(p_delay, l_callback, p_pyhouse_obj, p_list)
l_datetime = datetime.datetime.fromtimestamp(l_runID.getTime())
LOG.info('Scheduled {} after delay of {} - Time: {}'.format(p_list, p_delay, l_datetime))
return l_runID
class API(object):
m_pyhouse_obj = None
def __init__(self, p_pyhouse_obj):
self.m_pyhouse_obj = p_pyhouse_obj
Utility._setup_components(p_pyhouse_obj)
LOG.info("Initialized.")
def LoadXml(self, p_pyhouse_obj):
""" Load the Schedule xml info.
"""
p_pyhouse_obj.House.Schedules = {}
l_schedules = scheduleXml.read_schedules_xml(p_pyhouse_obj)
p_pyhouse_obj.House.Schedules = l_schedules
LOG.info('Loaded {} Schedules XML'.format(len(l_schedules)))
return l_schedules # for testing
def Start(self):
"""
Extracts all from XML so an update will write correct info back out to the XML file.
"""
sunrisesunset.API(self.m_pyhouse_obj).Start()
self.RestartSchedule()
LOG.info("Started.")
def Stop(self):
"""Stop everything.
"""
LOG.info("Stopped.")
def SaveXml(self, p_xml):
l_xml, l_count = scheduleXml.write_schedules_xml(self.m_pyhouse_obj.House.Schedules)
p_xml.append(l_xml)
LOG.info('Saved {} Schedules XML.'.format(l_count))
return l_xml # for testing
def RestartSchedule(self):
""" Anything that alters the schedules should call this to cause the new schedules to take effect.
"""
self.m_pyhouse_obj.Twisted.Reactor.callLater(INITIAL_DELAY, Utility.schedule_next_event, self.m_pyhouse_obj)
# ## END DBK