-
Notifications
You must be signed in to change notification settings - Fork 0
/
housesprinkler_zone.c
455 lines (408 loc) · 15.6 KB
/
housesprinkler_zone.c
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
/* housesprinkler - A simple home web server for sprinkler control
*
* Copyright 2020, Pascal Martin
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*
* housesprinkler_zone.c - Control the watering zones.
*
* SYNOPSYS:
*
* This module handles watering zones, including:
* - Load the servers and zones configuration.
* - Run periodic discoveries to find which server controls each zone.
* - Run a queue of zone activation.
*
* The queue starts one zone at a time. If a pulse/pause duration was
* defined, the zone is started only for the duration of the pulse. If
* there is still some activation time left, the zone is scheduled for
* re-activation pulse + pause seconds later.
*
* The goal of the pulse/pause mechanism is to avoid runoffs: the sprinklers
* typically deliver water faster than the ground can absorb. After some
* time poodles start to form. If there is a slope, a runoff may occur and
* the water is lost. So the goal is to run the sprinklers for a limited
* amount of time, and stop before a poodle or runoff has occurred.
* The pause time is meant to let the water be absorbed, after which the
* same sprinklers can be run again. This repeats until the whole requested
* watering time has been delivered.
*
* Another zone can be started while the previous zone is paused. The
* queue mechanism selects the first entry with the lowest start time,
* alternating through all the zones present in the queue, and no time
* is wasted doing no watering.
*
* A zone is removed from the queue once its last pulse has been completed.
*
* void housesprinkler_zone_refresh (void);
*
* This function must be called each time the configuration changes.
*
* void housesprinkler_zone_activate (const char *name,
* int pulse, const char *context);
*
* Activate one zone for the duration set by pulse. If the zone is already
* present in the watering queue, this pulse's amount is added to the
* remaining runtime. The context is typically the name of the schedule,
* or 0 for manual activation.
*
* void housesprinkler_zone_stop (void);
*
* Stop all active zones.
*
* void housesprinkler_zone_periodic (time_t now);
*
* The periodic function that runs the zones, one by one.
*
* int housesprinkler_zone_idle (void);
*
* Return true if at least one zone is active, false otherwise.
*
* int housesprinkler_zone_status (char *buffer, int size);
*
* A function that populates a complete status in JSON.
*
*/
#include <string.h>
#include <stdlib.h>
#include <echttp.h>
#include <echttp_json.h>
#include "houselog.h"
#include "housediscover.h"
#include "housesprinkler.h"
#include "housesprinkler_time.h"
#include "housesprinkler_zone.h"
#include "housesprinkler_feed.h"
#include "housesprinkler_config.h"
#include "housesprinkler_control.h"
#define DEBUG if (sprinkler_isdebug()) printf
typedef struct {
const char *name;
const char *feed;
int hydrate;
int pulse;
int pause;
char manual;
char status;
} SprinklerZone;
static SprinklerZone *Zones = 0;
static int ZonesCount = 0;
static time_t ZonesBusy = 0; // Do not schedule while a zone is running.
static time_t PulseEnd = 0;
static SprinklerZone *ZoneActive = 0; // One zone active at a time.
typedef struct {
int zone;
int hydrate;
int runtime;
time_t nexton;
char context[32];
} SprinklerQueue;
static SprinklerQueue *Queue = 0;
static int QueueNext = 0;
static int ZoneIndexValvePause = 1; // An optional pause for indexing valves.
void housesprinkler_zone_refresh (void) {
int i;
int count;
int content;
char path[128];
int list[256];
// Reload all zones.
//
if (Zones) free (Zones);
Zones = 0;
ZoneActive = 0;
ZonesBusy = 0;
PulseEnd = 0;
ZonesCount = 0;
content = housesprinkler_config_array (0, ".zones");
if (content > 0) {
ZonesCount = housesprinkler_config_array_length (content);
if (ZonesCount > 0) {
Zones = calloc (ZonesCount, sizeof(SprinklerZone));
DEBUG ("Loading %d zones\n", ZonesCount);
}
}
for (i = 0; i < ZonesCount; ++i) {
snprintf (path, sizeof(path), "[%d]", i);
int zone = housesprinkler_config_object (content, path);
if (zone > 0) {
Zones[i].name = housesprinkler_config_string (zone, ".name");
Zones[i].feed = housesprinkler_config_string (zone, ".feed");
Zones[i].hydrate = housesprinkler_config_integer (zone, ".hydrate");
Zones[i].pulse = housesprinkler_config_integer (zone, ".pulse");
Zones[i].pause = housesprinkler_config_integer (zone, ".pause");
Zones[i].manual = housesprinkler_config_boolean (zone, ".manual");
Zones[i].status = 'i';
housesprinkler_control_declare (Zones[i].name, "ZONE");
DEBUG ("\tZone %s (hydrate=%d, pulse=%d, pause=%d, manual=%s)\n",
Zones[i].name, Zones[i].hydrate, Zones[i].pulse, Zones[i].pause,
Zones[i].manual?"true":"false");
}
}
// We support at max 1 activation per zone..
// (If the same zone is activated more than once, the runtimes simply
// accumulate.)
//
if (Queue) free (Queue);
Queue = 0;
QueueNext = 0;
if (ZonesCount)
Queue = calloc (ZonesCount, sizeof(SprinklerQueue));
}
static int housesprinkler_zone_search (const char *name) {
int i;
for (i = 0; i < ZonesCount; ++i) {
if (!strcmp (name, Zones[i].name)) return i;
}
return -1;
}
void housesprinkler_zone_activate (const char *name,
int pulse, const char *context) {
int zone = housesprinkler_zone_search (name);
if (zone >= 0) {
int i;
time_t now = sprinkler_schedulingtime(time(0));
if (Zones[zone].manual && context) {
houselog_event ("ZONE", Zones[zone].name, "IGNORE", "MANUAL MODE ONLY");
return;
}
houselog_trace (HOUSE_INFO, name,
"queued (%s) for a %d seconds pulse",
context?"scheduled":"manually", pulse);
for (i = 0; i < QueueNext; ++i) {
if (Queue[i].zone == zone) {
// This zone was already queued. Add this pulse
// to the total remaining runtime.
Queue[i].runtime += pulse;
if (Queue[i].nexton == 0) Queue[i].nexton = now;
return;
}
}
if (QueueNext < ZonesCount) {
// This zone was not queued yet: create a new entry.
Queue[QueueNext].zone = zone;
Queue[QueueNext].hydrate = Zones[zone].hydrate;
Queue[QueueNext].runtime = pulse;
if (context)
snprintf (Queue[QueueNext].context, sizeof(Queue[0].context),
"%s", context);
else
Queue[QueueNext].context[0] = 0;
Queue[QueueNext].nexton = now;
DEBUG ("Activated zone %s for %d seconds (%s, queue entry %d)\n",
name, pulse, context?context:"manual", QueueNext);
QueueNext += 1;
}
}
}
void housesprinkler_zone_stop (void) {
int i;
time_t now = time(0);
DEBUG ("%ld: Stop all zones\n", now);
houselog_event ("ZONE", "ALL", "STOP", "MANUAL");
for (i = 0; i < QueueNext; ++i) {
Queue[i].hydrate = 0;
Queue[i].runtime = 0;
Queue[i].nexton = 0;
}
QueueNext = 0;
ZonesBusy = 0;
PulseEnd = 0;
}
static int housesprinkler_zone_elapsed (int queued) {
int zone = Queue[queued].zone;
int soaks = Queue[queued].runtime / Zones[zone].pulse;
if (Queue[queued].runtime % Zones[zone].pulse == 0) soaks -= 1;
return Queue[queued].runtime + (Zones[zone].pause * soaks);
}
static void housesprinkler_zone_schedule (time_t now) {
int i;
// Prune the queue once there is no time left and the zone has completed
// its pulse (including the pause period).
//
while (QueueNext > 0 &&
Queue[QueueNext-1].runtime == 0 &&
Queue[QueueNext-1].nexton < now) {
Queue[QueueNext-1].nexton = 0;
QueueNext -= 1;
DEBUG ("%ld: Prune queue entry %d\n", now, QueueNext);
}
if (now <= ZonesBusy) return;
if (ZoneActive) {
if (ZonesBusy == 0) {
// Clear sign that a stop was requested: cancel the zone.
housesprinkler_control_cancel (ZoneActive->name);
}
if (ZoneActive->status == 'a') ZoneActive->status = 'i';
ZoneActive = 0;
PulseEnd = 0;
}
// Search for the next zone to be started.
// Because nexttime is initialized to current time, only zones that
// have exhausted their pulse and pause period are considered here.
// So this loop searches for a zone that meet two conditions: ready
// to start, and the "oldest" to be so. This is done to maximize
// the soak time, beyond the minimum as configured.
// If there are multiple zones of the same "age", then the one with
// the longest elapsed runtime is selected: this is done to prioritize
// the longest running zones, especially when the program starts,
// because these long running zones are on the critical path and
// define when the program will end.
//
int remaining = 0;
int nextzone = -1;
time_t nexttime = now + 1;
for (i = 0; i < QueueNext; ++i) {
if (Queue[i].runtime == 0) continue;
if (Queue[i].context[0]) {
// Activate a zone that is part of a program only at the start of
// the minute.
// The reason for doing so it to make it easier to calculate
// water usage: we can sample water flow sensor on a minute basis.
// We don't do this synchronization for manual controls.
// We accept to be late by one second, as this is the time
// precision used by the periodic mechanism anyway.
//
if (now % 60 > 1) continue;
}
time_t nexton = Queue[i].nexton;
if (nexton > 0) {
if (nexton > nexttime) continue;
int elapsed = housesprinkler_zone_elapsed(i);
DEBUG ("queue %s has elapse time %d\n", Zones[Queue[i].zone].name, elapsed);
if (nexton < nexttime) {
nextzone = i;
nexttime = nexton;
remaining = elapsed;
} else if (elapsed > remaining) {
nextzone = i;
remaining = elapsed;
}
}
}
if (nextzone >= 0) {
int zone = Queue[nextzone].zone;
int pulse = 0;
if (Queue[nextzone].context[0] == 0) {
// This is a manual zone control: just use the runtime as provided
// by the user without any adjustment or cycle.
//
pulse = Queue[nextzone].runtime;
Queue[nextzone].runtime = 0;
Queue[nextzone].hydrate = 0;
Queue[nextzone].nexton = now + pulse;
} else {
// This zone control is part of a program: apply adjustments
// and follow the configured cycle.
//
pulse = Zones[zone].pulse;
if (Queue[nextzone].hydrate > 0) {
// The first pulse is meant to hydrate the soil (clay).
pulse = Queue[nextzone].hydrate;
Queue[nextzone].hydrate = 0; // Don't do it again.
}
if (pulse == 0 || Queue[nextzone].runtime <= pulse) {
pulse = Queue[nextzone].runtime;
Queue[nextzone].runtime = 0;
} else {
Queue[nextzone].runtime -= pulse;
}
// Always waits until the end of the pause, even if this is the
// last pulse: if the same zone is activated again, we don't want
// to ever skip the pause.
//
Queue[nextzone].nexton = now + pulse + Zones[zone].pause;
}
if (Zones[zone].feed) {
housesprinkler_feed_activate
(Zones[zone].feed, pulse, Queue[nextzone].context);
}
if (housesprinkler_control_start
(Zones[zone].name, pulse, Queue[nextzone].context)) {
// Schedule the next zone after the pulse and the optional index
// valve pause have been exhausted.
ZonesBusy = now + pulse + ZoneIndexValvePause;
ZoneActive = Zones + zone;
PulseEnd = now + pulse;
ZoneActive->status = 'a';
}
}
}
void housesprinkler_zone_periodic (time_t now) {
if (!ZonesCount) return;
if (now) housesprinkler_zone_schedule (now);
}
int housesprinkler_zone_idle (void) {
if (!QueueNext) return 1; // Nothing in the queue: most common case.
// There is something in the queue, but this might be a leftover
// pause. The system is active only if one zone is active, or if
// there are other zones to be activated later. If it is only
// waiting for the pause periods to complete, it is already idle.
// This avoids declaring a program as "complete" only 30 minutes
// or so after the last watering.
//
int i;
time_t now = sprinkler_schedulingtime(time(0));
if (PulseEnd >= now) return 0; // One zone is active.
for (i = 0; i < QueueNext; ++i) {
if (Queue[i].runtime > 0) return 0; // One zone will be active.
}
return 1;
}
int housesprinkler_zone_status (char *buffer, int size) {
int i;
int cursor;
const char *prefix = "";
cursor = snprintf (buffer+cursor, size-cursor, "\"zones\":[");
if (cursor >= size) goto overflow;
prefix = "";
for (i = 0; i < ZonesCount; ++i) {
int state = housesprinkler_control_state (Zones[i].name);
if ((state != 'e') && (state != 'u')) state = Zones[i].status;
cursor += snprintf (buffer+cursor, size-cursor, "%s[\"%s\",\"%c\"]",
prefix, Zones[i].name, state);
if (cursor >= size) goto overflow;
prefix = ",";
}
cursor += snprintf (buffer+cursor, size-cursor, "],\"queue\":[");
if (cursor >= size) goto overflow;
prefix = "";
for (i = 0; i < QueueNext; ++i) {
if (Queue[i].runtime > 0) {
cursor += snprintf (buffer+cursor, size-cursor,
"%s[\"%s\",%d]",
prefix,
Zones[Queue[i].zone].name,
Queue[i].runtime);
if (cursor >= size) goto overflow;
prefix = ",";
}
}
cursor += snprintf (buffer+cursor, size-cursor, "]");
if (cursor >= size) goto overflow;
if (ZoneActive) {
cursor += snprintf (buffer+cursor, size-cursor,
",\"active\":\"%s\"", ZoneActive->name);
if (cursor >= size) goto overflow;
}
return cursor;
overflow:
houselog_trace (HOUSE_FAILURE, "STATUS",
"BUFFER TOO SMALL (NEED %d bytes)", cursor);
buffer[0] = 0;
return 0;
}