-
Notifications
You must be signed in to change notification settings - Fork 0
/
scheduler.go
430 lines (405 loc) · 10.2 KB
/
scheduler.go
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
package database
import (
"database/sql"
)
type Schedule struct {
Id uint `json:"id"`
Owner string `json:"owner"`
Data ScheduleData `json:"data"`
}
type ScheduleData struct {
Name string `json:"name"`
Hour uint `json:"hour"`
Minute uint `json:"minute"`
TargetMode ScheduleTargetMode `json:"targetMode"` // Specifies which actions are taken when the schedule is executed
HomescriptCode string `json:"homescriptCode"` // Is read when using the `code` mode of the schedule
HomescriptTargetId string `json:"homescriptTargetId"` // Is required when using the `hms` mode of the schedule
SwitchJobs []ScheduleSwitchJobData `json:"switchJobs"`
}
// Specifies which action will be performed as a target
type ScheduleTargetMode string
const (
ScheduleTargetModeCode ScheduleTargetMode = "code" // Will execute Homescript code as a target
ScheduleTargetModeSwitches ScheduleTargetMode = "switches" // Will perform a series of power actions as a target
ScheduleTargetModeHMS ScheduleTargetMode = "hms" // Will execute a Homescript by its id as a target
)
// Creates a new table containing the schedules for the normal scheduler jobs
func createScheduleTable() error {
if _, err := db.Exec(`
CREATE TABLE
IF NOT EXISTS
schedule(
Id INT AUTO_INCREMENT,
Owner VARCHAR(20),
Name VARCHAR(30),
Hour INT,
Minute INT,
TargetMode ENUM (
'switches',
'code',
'hms'
),
HomescriptCode TEXT,
HomescriptTargetId VARCHAR(30),
PRIMARY KEY (Id),
FOREIGN KEY (Owner)
REFERENCES user(Username)
)
`); err != nil {
log.Error("Failed to create schedule table: executing query failed: ", err.Error())
return err
}
return nil
}
// Creates a new schedule which represents a job of the scheduler
func CreateNewSchedule(
owner string,
data ScheduleData,
) (uint, error) {
query, err := db.Prepare(`
INSERT INTO
schedule(
Id,
Owner,
Name,
Hour,
Minute,
TargetMode,
HomescriptCode,
HomescriptTargetId
)
VALUES(DEFAULT, ?, ?, ?, ?, ?, ?, ?)
`)
if err != nil {
log.Error("Failed to create new schedule: preparing query failed: ", err.Error())
return 0, err
}
defer query.Close()
res, err := query.Exec(
owner,
data.Name,
data.Hour,
data.Minute,
data.TargetMode,
data.HomescriptCode,
data.HomescriptTargetId,
)
if err != nil {
log.Error("Failed to create new schedule: executing query failed: ", err.Error())
return 0, err
}
newId, err := res.LastInsertId()
if err != nil {
log.Error("Failed to create new schedule: retrieving last inserted id failed: ", err.Error())
return 0, err
}
// Create the schedule's switch jobs
for _, switchJob := range data.SwitchJobs {
if _, err := CreateNewScheduleSwitch(
uint(newId),
switchJob.SwitchId,
switchJob.PowerOn,
); err != nil {
log.Error("Failed to create new schedule: could not create switch job: ", err.Error())
return 0, err
}
}
return uint(newId), nil
}
// Returns a schedule struct which matches the given id
// If the id does not match a struct, a `false`` is returned
func GetScheduleById(id uint) (Schedule, bool, error) {
query, err := db.Prepare(`
SELECT
Id,
Name,
Owner,
Hour,
Minute,
TargetMode,
HomescriptCode,
HomescriptTargetId
FROM schedule
WHERE Id=?
`)
if err != nil {
log.Error("Failed to get schedule by id: preparing query failed: ", err.Error())
return Schedule{}, false, err
}
defer query.Close()
var schedule Schedule
if err := query.QueryRow(id).Scan(
&schedule.Id,
&schedule.Data.Name,
&schedule.Owner,
&schedule.Data.Hour,
&schedule.Data.Minute,
&schedule.Data.TargetMode,
&schedule.Data.HomescriptCode,
&schedule.Data.HomescriptTargetId,
); err != nil {
if err == sql.ErrNoRows {
return Schedule{}, false, nil
}
log.Error("Failed to get schedule by id: executing query failed: ", err.Error())
return Schedule{}, false, err
}
// Obtain this schedule's switch jobs
switches, err := ListSwitchesOfSchedule(schedule.Id)
if err != nil {
return Schedule{}, false, err
}
schedule.Data.SwitchJobs = switches
return schedule, true, nil
}
// Returns a list containing schedules of a given user
func GetUserSchedules(username string) ([]Schedule, error) {
query, err := db.Prepare(`
SELECT
Id,
Name,
Owner,
Hour,
Minute,
TargetMode,
HomescriptCode,
HomescriptTargetId
FROM schedule
WHERE Owner=?
`)
if err != nil {
log.Error("Failed to list user schedules: preparing query failed: ", err.Error())
return nil, err
}
defer query.Close()
res, err := query.Query(username)
if err != nil {
log.Error("Failed to list user schedules: executing query failed: ", err.Error())
return nil, err
}
defer res.Close()
// Obtain schedule switches
switches, err := ListUserScheduleSwitches(username)
if err != nil {
return nil, err
}
schedules := make([]Schedule, 0)
for res.Next() {
var schedule Schedule
if err := res.Scan(
&schedule.Id,
&schedule.Data.Name,
&schedule.Owner,
&schedule.Data.Hour,
&schedule.Data.Minute,
&schedule.Data.TargetMode,
&schedule.Data.HomescriptCode,
&schedule.Data.HomescriptTargetId,
); err != nil {
log.Error("Failed to list user schedules: scanning results of query failed: ", err.Error())
return nil, err
}
// Append the schedule's switches to the data
schedule.Data.SwitchJobs = make([]ScheduleSwitchJobData, 0)
for _, swItem := range switches {
if swItem.ScheduleId == schedule.Id {
schedule.Data.SwitchJobs = append(schedule.Data.SwitchJobs, swItem.Data)
}
}
// Append the row to the list
schedules = append(schedules, schedule)
}
return schedules, nil
}
// Returns a list of schedules of all users, used for activating schedules at the start of the server
func GetSchedules() ([]Schedule, error) {
query, err := db.Prepare(`
SELECT
Id,
Name,
Owner,
Hour,
Minute,
TargetMode,
HomescriptCode,
HomescriptTargetId
FROM schedule
`)
if err != nil {
log.Error("Failed to list schedules: preparing query failed: ", err.Error())
return nil, err
}
defer query.Close()
res, err := query.Query()
if err != nil {
log.Error("Failed to list schedules: executing query failed: ", err.Error())
return nil, err
}
// Obtain all schedule switch jobs
switches, err := ListAllScheduleSwitches()
if err != nil {
return nil, err
}
defer res.Close()
schedules := make([]Schedule, 0)
for res.Next() {
var schedule Schedule
if err := res.Scan(
&schedule.Id,
&schedule.Data.Name,
&schedule.Owner,
&schedule.Data.Hour,
&schedule.Data.Minute,
&schedule.Data.TargetMode,
&schedule.Data.HomescriptCode,
&schedule.Data.HomescriptTargetId,
); err != nil {
log.Error("Failed to list schedules: scanning results of query failed: ", err.Error())
return nil, err
}
// Append all needed switch jobs to this row
for _, switchJob := range switches {
schedule.Data.SwitchJobs = append(schedule.Data.SwitchJobs, switchJob.Data)
}
// Append the row to the results
schedules = append(schedules, schedule)
}
return schedules, nil
}
// Modifies the metadata of a given schedule
// Does not validate the provided metadata
func ModifySchedule(id uint, newData ScheduleData) error {
query, err := db.Prepare(`
UPDATE schedule
SET
Name=?,
Hour=?,
Minute=?,
TargetMode=?,
HomescriptCode=?,
HomescriptTargetId=?
WHERE Id=?
`)
if err != nil {
log.Error("Failed to modify schedule: preparing query failed: ", err.Error())
return err
}
defer query.Close()
if _, err := query.Exec(
newData.Name,
newData.Hour,
newData.Minute,
newData.TargetMode,
newData.HomescriptCode,
newData.HomescriptTargetId,
id,
); err != nil {
log.Error("Failed to modify schedule: executing query failed: ", err.Error())
return err
}
// Perform switch diff operations
oldSwitches, err := ListSwitchesOfSchedule(id)
if err != nil {
return err
}
add, del := getSwitchDiff(
oldSwitches,
newData.SwitchJobs,
)
// Remove all unused switches
for _, swDel := range del {
if err := DeleteSwitchFromSchedule(
swDel.SwitchId,
id,
); err != nil {
return err
}
}
// Add all missing switches
for _, swAdd := range add {
if _, err := CreateNewScheduleSwitch(
id,
swAdd.SwitchId,
swAdd.PowerOn,
); err != nil {
return err
}
}
return nil
}
// Compares two slices which contain schedule switch jobs
// Outputs two slices which determine which actions have to be taken to transform the old state into the new state
// This function is used in schedule modification
func getSwitchDiff(
oldSwitches []ScheduleSwitchJobData,
newSwitches []ScheduleSwitchJobData,
) (
add []ScheduleSwitchJobData,
del []ScheduleSwitchJobData,
) {
// Determine deletions
for _, swOld := range oldSwitches {
exists := false
for _, swNew := range newSwitches {
if swNew.SwitchId == swOld.SwitchId && swNew.PowerOn == swOld.PowerOn {
exists = true
break
}
}
if !exists {
del = append(del, swOld)
}
}
// Determine addition
for _, swNew := range newSwitches {
exists := false
for _, swOld := range oldSwitches {
if swOld.SwitchId == swNew.SwitchId && swOld.PowerOn == swNew.PowerOn {
exists = true
break
}
}
if !exists {
add = append(add, swNew)
}
}
return add, del
}
// Deletes a schedule item given its Id
// Deletes all switch jobs first
// Does not validate the validity of the provided Id
func DeleteScheduleById(id uint) error {
// Delete all switch jobs first
if err := DeleteAllSwitchesFromSchedule(id); err != nil {
return err
}
// Delete the actual schedule
query, err := db.Prepare(`
DELETE FROM
schedule
WHERE Id=?
`)
if err != nil {
log.Error("Failed to delete schedule by id: preparing query failed: ", err.Error())
return err
}
defer query.Close()
if _, err := query.Exec(id); err != nil {
log.Error("Failed to delete schedule by id: executing query failed: ", err.Error())
return err
}
return nil
}
// Deletes all schedules from a given user
func DeleteAllSchedulesFromUser(username string) error {
schedules, err := GetUserSchedules(username)
if err != nil {
return err
}
for _, schedule := range schedules {
if err := DeleteScheduleById(schedule.Id); err != nil {
return err
}
}
return nil
}