Skip to content

Commit 7fe7f52

Browse files
handy-manny-sweJohan Stenmark
andauthored
Added auto off timer feature - Garage door support (#33)
* Added auto off timer feature Co-authored-by: Johan Stenmark <johanstenmark@Johans-och-Maries-iMac.local>
1 parent 8af263c commit 7fe7f52

File tree

5 files changed

+131
-17
lines changed

5 files changed

+131
-17
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ binlibs/
22
build/
33
build_*/
44
deps/
5+
.DS_Store

fs/index.html

Lines changed: 82 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<!DOCTYPE html>
22
<html lang="en">
33
<head>
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
45
<script src="axios.min.js.gz"></script>
56
<style>
67
html, body { height: 100vh; padding: 0; margin: 0;}
@@ -79,9 +80,17 @@ <h1 class="" id="sw1_name">SW1 Name</h1>
7980
</select>
8081
</div>
8182
<div class="form-control">
82-
<label>Persist state:</label>
83+
<label for="sw1_persist">Persist state:</label>
8384
<input type="checkbox" id="sw1_persist">
8485
</div>
86+
<div class="form-control">
87+
<label for="sw1_auto_off">Auto off:</label>
88+
<input type="checkbox" id="sw1_auto_off">
89+
</div>
90+
<div>
91+
<label for="sw1_auto_off_delay">Auto off delay:</label>
92+
<input type="text" id="sw1_auto_off_delay" placeholder="HH:mm:ss" required pattern="(0*([0-9]|1[0-9]|2[0-3])):(0*([0-9]|[1-4][0-9]|5[0-9])):(0*([0-9]|[1-4][0-9]|5[0-9]))">
93+
</div>
8594
<div class="form-control">
8695
<label></label>
8796
<button class="btn" id="sw1_save_btn">
@@ -121,9 +130,17 @@ <h1 class="" id="sw2_name">SW2 Name</h1>
121130
</select>
122131
</div>
123132
<div class="form-control">
124-
<label>Persist state:</label>
133+
<label for="sw2_persist">Persist state:</label>
125134
<input type="checkbox" id="sw2_persist">
126135
</div>
136+
<div class="form-control">
137+
<label for="sw2_auto_off">Auto off:</label>
138+
<input type="checkbox" id="sw2_auto_off">
139+
</div>
140+
<div>
141+
<label for="sw2_auto_off_delay">Auto off delay:</label>
142+
<input type="text" id="sw2_auto_off_delay" placeholder="HH:mm:ss" required pattern="(0*([0-9]|1[0-9]|2[0-3])):(0*([0-9]|[1-4][0-9]|5[0-9])):(0*([0-9]|[1-4][0-9]|5[0-9]))">
143+
</div>
127144
<div class="form-control">
128145
<label></label>
129146
<button class="btn" id="sw2_save_btn">
@@ -344,7 +361,7 @@ <h1 class="">Firmware</h1>
344361
);
345362
}
346363

347-
function sw_save_common(cfg_key, in_mode, persist, spinner) {
364+
function sw_save_common(cfg_key, in_mode, persist, auto_off, auto_off_delay, spinner) {
348365
spinner.className = "spin";
349366
var data = {
350367
config: {},
@@ -354,6 +371,8 @@ <h1 class="">Firmware</h1>
354371
data.config[cfg_key] = {
355372
in_mode: parseInt(in_mode),
356373
persist_state: persist,
374+
auto_off: auto_off,
375+
auto_off_delay: dateStringToSeconds(auto_off_delay)
357376
};
358377
axios.post(host + "/rpc/Config.Set", data).then(function(res) {
359378
}).catch(function(err) {
@@ -367,22 +386,57 @@ <h1 class="">Firmware</h1>
367386
});
368387
}
369388

389+
function isValid(swType) {
390+
var auto_off_delay = el(swType + "_auto_off_delay");
391+
var auto_off_delay_valid = auto_off_delay.checkValidity();
392+
if (!auto_off_delay_valid) {
393+
alert("Auto off delay must follow 24 hour format HH:mm:ss");
394+
return false;
395+
}
396+
return true;
397+
}
398+
399+
function dateStringToSeconds(dateString) {
400+
var dateStringParts = dateString.split(':');
401+
var seconds = parseInt(dateStringParts[0]) * 3600 + parseInt(dateStringParts[1]) * 60 + parseInt(dateStringParts[2]);
402+
return seconds;
403+
}
404+
405+
function secondsToDateString(seconds) {
406+
var date = new Date(1970, 0, 1);
407+
date.setSeconds(seconds);
408+
var dateString = twoDigitString(date.getHours()) + ":" + twoDigitString(date.getMinutes()) + ":" + twoDigitString(date.getSeconds());
409+
return dateString;
410+
}
411+
412+
function twoDigitString(num) {
413+
return num.toLocaleString(undefined, {minimumIntegerDigits: 2})
414+
}
415+
370416
el("sw1_save_btn").onclick = function() {
371-
sw_save_common(
372-
"sw1",
373-
el("sw1_in_mode").value,
374-
el("sw1_persist").checked,
375-
el("sw1_save_spinner"),
376-
);
417+
if (isValid("sw1")){
418+
sw_save_common(
419+
"sw1",
420+
el("sw1_in_mode").value,
421+
el("sw1_persist").checked,
422+
el("sw1_auto_off").checked,
423+
el("sw1_auto_off_delay").value,
424+
el("sw1_save_spinner"),
425+
);
426+
}
377427
}
378428

379429
el("sw2_save_btn").onclick = function() {
380-
sw_save_common(
381-
"sw2",
382-
el("sw2_in_mode").value,
383-
el("sw2_persist").checked,
384-
el("sw2_save_spinner"),
385-
);
430+
if (isValid("sw2")){
431+
sw_save_common(
432+
"sw2",
433+
el("sw2_in_mode").value,
434+
el("sw2_persist").checked,
435+
el("sw2_auto_off").checked,
436+
el("sw2_auto_off_delay").value,
437+
el("sw2_save_spinner"),
438+
);
439+
}
386440
}
387441

388442
el("reboot_btn").onclick = function() {
@@ -420,6 +474,9 @@ <h1 class="">Firmware</h1>
420474
el("sw1_btn_label").innerText = "Turn " + (res.data.sw1.state ? "Off" : "On");
421475
el("sw1_in_mode_" + res.data.sw1.in_mode).selected = true;
422476
el("sw1_persist").checked = res.data.sw1.persist;
477+
el("sw1_auto_off").checked = res.data.sw1.auto_off;
478+
el("sw1_auto_off_delay").disabled = !res.data.sw1.auto_off;
479+
el("sw1_auto_off_delay").value = secondsToDateString(res.data.sw1.auto_off_delay);
423480
sw1.sw_id = res.data.sw1.id;
424481
sw1.sw_state = res.data.sw1.state;
425482
sw1.style.display = "block";
@@ -434,6 +491,9 @@ <h1 class="">Firmware</h1>
434491
el("sw2_btn_label").innerText = "Turn " + (res.data.sw2.state ? "Off" : "On");
435492
el("sw2_in_mode_" + res.data.sw2.in_mode).selected = true;
436493
el("sw2_persist").checked = res.data.sw2.persist;
494+
el("sw2_auto_off").checked = res.data.sw2.auto_off;
495+
el("sw2_auto_off_delay").disabled = !res.data.sw2.auto_off;
496+
el("sw2_auto_off_delay").value = secondsToDateString(res.data.sw2.auto_off_delay);
437497
sw2.sw_id = res.data.sw2.id;
438498
sw2.sw_state = res.data.sw2.state;
439499
sw2.style.display = "block";
@@ -454,6 +514,13 @@ <h1 class="">Firmware</h1>
454514

455515
function onLoad() {
456516
getInfo();
517+
518+
el('sw1_auto_off').onchange = function() {
519+
el('sw1_auto_off_delay').disabled = !this.checked;
520+
};
521+
el('sw2_auto_off').onchange = function() {
522+
el('sw2_auto_off_delay').disabled = !this.checked;
523+
};
457524
}
458525

459526
function durationStr(d) {

mos.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ config_schema:
2929
- ["sw.in_mode", "i", 1, {"0 - Momentary, 1 - Toggle, 2 - Edge, 3 - Detached"}]
3030
- ["sw.state", "b", false, {"State of the switch"}]
3131
- ["sw.persist_state", "b", false, {"Whether state of the switch should be persisted across reboots."}]
32+
- ["sw.auto_off", "b", false, {"Whether the switch should automatically turn OFF after turning ON"}]
33+
- ["sw.auto_off_delay", "i", -1, {"Delay for automatically turning OFF, in seconds"}]
3234

3335

3436
build_vars:

src/shelly_main.c

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,14 +304,16 @@ static void shelly_get_info_handler(struct mg_rpc_request_info *ri,
304304
ri,
305305
"{id: %Q, app: %Q, host: %Q, version: %Q, fw_build: %Q, uptime: %d, "
306306
#ifdef MGOS_CONFIG_HAVE_SW1
307-
"sw1: {id: %d, name: %Q, in_mode: %d, persist: %B, state: %B"
307+
"sw1: {id: %d, name: %Q, in_mode: %d, persist: %B, state: %B, auto_off: "
308+
"%B, auto_off_delay: %d"
308309
#ifdef SHELLY_HAVE_PM
309310
", apower: %.3f, aenergy: %.3f"
310311
#endif
311312
"},"
312313
#endif
313314
#ifdef MGOS_CONFIG_HAVE_SW2
314-
"sw2: {id: %d, name: %Q, in_mode: %d, persist: %B, state: %B"
315+
"sw2: {id: %d, name: %Q, in_mode: %d, persist: %B, state: %B, auto_off: "
316+
"%B, auto_off_delay: %d"
315317
#ifdef SHELLY_HAVE_PM
316318
", apower: %.3f, aenergy: %.3f"
317319
#endif
@@ -326,6 +328,8 @@ static void shelly_get_info_handler(struct mg_rpc_request_info *ri,
326328
mgos_sys_config_get_sw1_id(), mgos_sys_config_get_sw1_name(),
327329
mgos_sys_config_get_sw1_in_mode(),
328330
mgos_sys_config_get_sw1_persist_state(), sw1.state,
331+
mgos_sys_config_get_sw1_auto_off(),
332+
mgos_sys_config_get_sw1_auto_off_delay(),
329333
#ifdef SHELLY_HAVE_PM
330334
sw1.apower, sw1.aenergy,
331335
#endif
@@ -334,6 +338,8 @@ static void shelly_get_info_handler(struct mg_rpc_request_info *ri,
334338
mgos_sys_config_get_sw2_id(), mgos_sys_config_get_sw2_name(),
335339
mgos_sys_config_get_sw2_in_mode(),
336340
mgos_sys_config_get_sw2_persist_state(), sw2.state,
341+
mgos_sys_config_get_sw2_auto_off(),
342+
mgos_sys_config_get_sw2_auto_off_delay(),
337343
#ifdef SHELLY_HAVE_PM
338344
sw2.apower, sw2.aenergy,
339345
#endif

src/shelly_sw_service.c

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ struct shelly_sw_service_ctx {
5151

5252
static struct shelly_sw_service_ctx s_ctx[NUM_SWITCHES];
5353

54+
static int s_auto_off_timer_id = MGOS_INVALID_TIMER_ID;
55+
56+
static void do_auto_off(void *arg);
57+
5458
#ifdef SHELLY_HAVE_PM
5559
static void shelly_sw_read_power(void *arg);
5660
#endif
@@ -67,6 +71,27 @@ static void do_reset(void *arg) {
6771
#endif
6872
}
6973

74+
static void handle_auto_off(struct shelly_sw_service_ctx *ctx,
75+
const char *source, bool new_state) {
76+
if (s_auto_off_timer_id != MGOS_INVALID_TIMER_ID) {
77+
// Cancel timer if state changes so that only the last timer is triggered if
78+
// state changes multiple times
79+
mgos_clear_timer(s_auto_off_timer_id);
80+
s_auto_off_timer_id = MGOS_INVALID_TIMER_ID;
81+
}
82+
83+
const struct mgos_config_sw *cfg = ctx->cfg;
84+
85+
if (!cfg->auto_off) return;
86+
87+
if (strcmp(source, "auto_off") == 0) return;
88+
89+
if (!new_state) return;
90+
91+
s_auto_off_timer_id =
92+
mgos_set_timer(cfg->auto_off_delay * 1000, 0, do_auto_off, ctx);
93+
}
94+
7095
static void shelly_sw_set_state_ctx(struct shelly_sw_service_ctx *ctx,
7196
bool new_state, const char *source) {
7297
const struct mgos_config_sw *cfg = ctx->cfg;
@@ -86,6 +111,7 @@ static void shelly_sw_set_state_ctx(struct shelly_sw_service_ctx *ctx,
86111
mgos_sys_config_save(&mgos_sys_config, false /* try_once */,
87112
NULL /* msg */);
88113
}
114+
89115
double now = mgos_uptime();
90116
if (now < 60) {
91117
if (now - ctx->last_change_ts > 10) {
@@ -100,6 +126,18 @@ static void shelly_sw_set_state_ctx(struct shelly_sw_service_ctx *ctx,
100126
mgos_set_timer(600, 0, do_reset, ctx);
101127
}
102128
}
129+
130+
handle_auto_off(ctx, source, new_state);
131+
}
132+
133+
static void do_auto_off(void *arg) {
134+
s_auto_off_timer_id = MGOS_INVALID_TIMER_ID;
135+
struct shelly_sw_service_ctx *ctx = arg;
136+
const struct mgos_config_sw *cfg = ctx->cfg;
137+
if (cfg->auto_off) {
138+
// Don't set state if auto off has been disabled during timer run
139+
shelly_sw_set_state_ctx(ctx, false, "auto_off");
140+
}
103141
}
104142

105143
bool shelly_sw_set_state(int id, bool new_state, const char *source) {

0 commit comments

Comments
 (0)