-
Notifications
You must be signed in to change notification settings - Fork 0
/
housesprinkler_state.c
376 lines (325 loc) · 12.5 KB
/
housesprinkler_state.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
/* 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_state.c - Backup and restore the sprinkler state.
*
* The sprinkler state covers data items that the program needs to
* function as expected when it restarts, such as:
* - Scheduling on/off.
* - Watering index enabled/disabled.
* - When each schedule was last activated (used for interval calculation).
* - etc.
*
* These items are not considered part of the configuration, for one of the
* following reasons:
* - It should only impact one sprinkler instance, not all of them, or
* - This is data generated by the application, not entered by the user, or
* - This is a separate item that should not cause a complete reconfiguration.
*
* This version uses both the local backup file _and_ the data from the
* depot repository. The later has priority. This scheme has two benefits:
* - seamless transition from ocal storage only to deport repositories.
* - keep working even if the depot is not accessible.
*
* SYNOPSYS:
*
* void housesprinkler_state_share (int on);
*
* Turn the depot mechanism on and off. The intent is to turn it
* on only when the sprinkler system is on. That does mean that turning
* the sprinkler system off will not be recorded in the depot's
* repository. The logic is to record which instance is on, not that
* a specific instance is off (there can be multiple instances off,
* and that does not identify which one is on).
*
* void housesprinkler_state_listen (BackupListener *listener);
*
* Listen to external changes to the state backup. Such changes typically
* come from the depot repository.
*
* void housesprinkler_state_register (BackupWorker *worker);
*
* Register a worker function to export a module's internal state to JSON.
* Worker functions are called when the state must be saved.
*
* Modules that need to backup data must use this to register a worker
* function that exports the module's internal state to a JSON structure
* that will be saved to disk (local or depot repository)..
*
* long housesprinkler_state_get (const char *path);
* const char *housesprinkler_state_get_string (const char *path);
*
* Retrieve items from the state backup file. This backup file contains
* saved live values that can be changed from the user interface and must
* survive a program restart. Supported data types are boolean, integer and
* string (for now). A boolean is reported as an integer (0 or 1).
*
* void housesprinkler_state_changed (void);
*
* Report that the internal state has changed.
*
* Note that saving the backup data is asynchronous: the client indicates
* that the data has changed, but saving the data will be decided later.
* The reason for this is that multiple clients might change their data
* at around the same time, but we do not want to save each time: it is
* better to delay and do the save only once.
*
* void housesprinkler_state_periodic (time_t now);
*
* Background state activity (mostly: save data when changed).
*/
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <echttp_json.h>
#include "houselog.h"
#include "housedepositor.h"
#include "housesprinkler.h"
#include "housesprinkler_state.h"
#define DEBUG if (sprinkler_isdebug()) printf
static ParserToken *BackupParsed = 0;
static int BackupTokenAllocated = 0;
static int BackupTokenCount = 0;
static char *BackupInText = 0;
static const char *BackupFile = "/etc/house/sprinklerbkp.json";
static const char FactoryBackupFile[] =
"/usr/local/share/house/public/sprinkler/backup.json";
static time_t StateDataHasChanged = 0;
static int ShareStateData = 1;
static int StateFileEnabled = 1;
static char *BackupOutBuffer = 0;
static int BackupOutBufferSize = 0;
// The backup mechanism relies on collaboration from the modules that
// need to backup data: only these modules know what data is to be saved.
//
static BackupListener **BackupRegisteredListener = 0;
static int BackupListenerSize = 0;
static int BackupListenerCount = 0;
void housesprinkler_state_listen (BackupListener *listener) {
int i;
if (! listener) return; // Just a check against gross error.
for (i = 0; i < BackupListenerCount; ++i) {
if (BackupRegisteredListener[i] == listener)
return; // Already registered.
}
if (BackupListenerCount >= BackupListenerSize) {
BackupListenerSize += 16;
BackupRegisteredListener =
realloc (BackupRegisteredListener,
sizeof(BackupListener *) * BackupListenerSize);
}
BackupRegisteredListener[BackupListenerCount++] = listener;
}
static BackupWorker **BackupRegisteredWorker = 0;
static int BackupWorkerSize = 0;
static int BackupWorkerCount = 0;
void housesprinkler_state_register (BackupWorker *worker) {
int i;
if (! worker) return; // Just a check against gross error.
for (i = 0; i < BackupWorkerCount; ++i) {
if (BackupRegisteredWorker[i] == worker) return; // Already registered.
}
if (BackupWorkerCount >= BackupWorkerSize) {
BackupWorkerSize += 16;
BackupRegisteredWorker =
realloc (BackupRegisteredWorker,
sizeof(BackupWorker *) * BackupWorkerSize);
}
BackupRegisteredWorker[BackupWorkerCount++] = worker;
}
static void housesprinkler_state_clear (void) {
if (BackupInText) {
echttp_parser_free (BackupInText);
BackupInText = 0;
}
BackupTokenCount = 0;
}
static const char *housesprinkler_state_new (char *data) {
const char *error;
BackupInText = data;
BackupTokenCount = echttp_json_estimate(BackupInText);
if (BackupTokenCount > BackupTokenAllocated) {
BackupTokenAllocated = BackupTokenCount+64;
if (BackupParsed) free (BackupParsed);
BackupParsed = calloc (BackupTokenAllocated, sizeof(ParserToken));
}
error = echttp_json_parse (BackupInText, BackupParsed, &BackupTokenCount);
if (error) {
DEBUG ("Backup config parsing error: %s\n", error);
housesprinkler_state_clear ();
} else {
DEBUG ("Planned %d, read %d items of backup config\n", BackupTokenAllocated, BackupTokenCount);
}
return error;
}
static int housesprinkler_state_save (int size) {
if (!StateFileEnabled) return size; // No error.
int fd = open (BackupFile, O_WRONLY|O_TRUNC|O_CREAT, 0777);
if (fd < 0) {
DEBUG ("Cannot open %s\n", BackupFile);
return 0; // Failure.
}
int written = write (fd, BackupOutBuffer, size);
if (written < 0) {
DEBUG ("Cannot write to %s\n", BackupFile);
} else {
DEBUG ("Wrote %d characters to %s\n", written, BackupFile);
}
close(fd);
return written;
}
static void housesprinkler_state_listener (const char *name, time_t timestamp,
const char *data, int length) {
houselog_event ("SYSTEM", "STATE", "LOAD", "FROM DEPOT %s", name);
const char *error = housesprinkler_state_new (echttp_parser_string(data));
if (error) {
houselog_event ("SYSTEM", "STATE", "ERROR", "%s", error);
return;
}
// We need to make a copy because we do not control the lifetime of
// the caller's data buffer.
//
if (BackupOutBufferSize < length) {
if (BackupOutBuffer) free (BackupOutBuffer);
BackupOutBuffer = strdup (data);
BackupOutBufferSize = length;
} else {
snprintf (BackupOutBuffer, BackupOutBufferSize, "%s", data);
}
housesprinkler_state_save (length); // Best effort only, ignore errors.
int i;
for (i = 0; i < BackupListenerCount; ++i) {
BackupRegisteredListener[i] ();
}
}
const void housesprinkler_state_load (int argc, const char **argv) {
char *newconfig;
int i;
for (i = 1; i < argc; ++i) {
if (echttp_option_match ("-backup=", argv[i], &BackupFile)) continue;
if (echttp_option_present ("-no-local-storage", argv[i])) {
StateFileEnabled = 0;
continue;
}
}
housesprinkler_state_clear ();
housedepositor_subscribe ("state", "sprinkler.json",
housesprinkler_state_listener);
if (!StateFileEnabled) return;
const char *name = BackupFile;
DEBUG ("Loading backup from %s\n", name);
newconfig = echttp_parser_load (name);
if (!newconfig) {
name = FactoryBackupFile;
DEBUG ("Loading backup from %s\n", name);
newconfig = echttp_parser_load (name);
StateDataHasChanged = time(0); // Force creation of the backup file.
}
if (newconfig) {
const char *error;
houselog_event ("SYSTEM", "STATE", "LOAD", "FILE %s", name);
housesprinkler_state_new (newconfig);
}
}
void housesprinkler_state_share (int on) {
ShareStateData = on;
}
const char *housesprinkler_state_get_string (const char *path) {
if (BackupTokenCount <= 0) return 0;
int i = echttp_json_search(BackupParsed, path);
if (i < 0) return 0;
switch (BackupParsed[i].type) {
case PARSER_STRING: return BackupParsed[i].value.string;
}
return 0;
}
long housesprinkler_state_get (const char *path) {
if (BackupTokenCount <= 0) return 0;
// Support boolean and integer, all converted to integer.
// Anything else: return 0
//
long value = 0;
int i = echttp_json_search(BackupParsed, path);
if (i < 0) return 0;
switch (BackupParsed[i].type) {
case PARSER_BOOL: value = BackupParsed[i].value.bool; break;
case PARSER_INTEGER: value = BackupParsed[i].value.integer; break;
}
return value;
}
void housesprinkler_state_changed (void) {
if (!StateDataHasChanged) {
DEBUG("State data has changed.\n");
StateDataHasChanged = time(0);
}
}
static int housesprinkler_state_format (void) {
int cursor = 0;
int i;
if (!BackupOutBuffer) {
BackupOutBufferSize = 1024;
BackupOutBuffer = malloc(BackupOutBufferSize);
}
DEBUG("Saving backup data to %s\n", BackupFile);
cursor = snprintf (BackupOutBuffer, BackupOutBufferSize,
"{\"host\":\"%s\"", sprinkler_host());
for (i = 0; i < BackupWorkerCount; ++i) {
cursor += snprintf (BackupOutBuffer+cursor, BackupOutBufferSize-cursor, ",");
if (cursor >= BackupOutBufferSize) goto retry;
cursor += BackupRegisteredWorker[i] (BackupOutBuffer+cursor, BackupOutBufferSize-cursor);
if (cursor >= BackupOutBufferSize) goto retry;
}
cursor += snprintf (BackupOutBuffer+cursor, BackupOutBufferSize-cursor, "}");
if (cursor >= BackupOutBufferSize) goto retry;
return cursor;
retry:
DEBUG ("Backup failed: buffer too small\n");
houselog_trace (HOUSE_WARNING, "STATE",
"BUFFER TOO SMALL (NEED %d bytes)", cursor);
BackupOutBufferSize += 1024;
free (BackupOutBuffer);
BackupOutBuffer = malloc(BackupOutBufferSize);
return housesprinkler_state_format ();
}
void housesprinkler_state_periodic (time_t now) {
static time_t LastCall = 0;
if (now == LastCall) return; // Run the logic once per second.
LastCall = now;
if (StateDataHasChanged) {
if (StateDataHasChanged < now - 10) {
StateDataHasChanged = 0;
return; // We tried 10 times, no point to try again.
}
if (StateDataHasChanged < now) {
int size = housesprinkler_state_format();
if (ShareStateData) {
houselog_event ("SYSTEM", "STATE", "SAVE", "TO DEPOT sprinkler.json");
housedepositor_put ("state", "sprinkler.json", BackupOutBuffer, size);
}
if (housesprinkler_state_save (size) == size)
StateDataHasChanged = 0;
}
}
}