diff --git a/sonoff/_releasenotes.ino b/sonoff/_releasenotes.ino
index 0860a476d..44ca02078 100644
--- a/sonoff/_releasenotes.ino
+++ b/sonoff/_releasenotes.ino
@@ -1,6 +1,7 @@
/* 5.12.0l
* Release rules up to 511 characters
* Prepare for feature release - call on translators to update their language files
+ * Add timer sunrise and sunset offset (#2378)
*
* 5.12.0k
* Prepare for simple rules of up to 255 characters by enlarging Settings area to now 2048 bytes
diff --git a/sonoff/xdrv_09_timers.ino b/sonoff/xdrv_09_timers.ino
index e864e0062..c30cd153d 100644
--- a/sonoff/xdrv_09_timers.ino
+++ b/sonoff/xdrv_09_timers.ino
@@ -175,6 +175,43 @@ void DuskTillDawn(uint8_t *hour_up,uint8_t *minute_up, uint8_t *hour_down, uint8
*minute_down = UntergangMinuten;
}
+void ApplyTimerOffsets(Timer *duskdawn)
+{
+ uint8_t hour[2];
+ uint8_t minute[2];
+ Timer stored = (Timer)*duskdawn;
+
+ // replace hours, minutes by sunrise
+ DuskTillDawn(&hour[0], &minute[0], &hour[1], &minute[1]);
+ uint8_t mode = (duskdawn->mode -1) &1;
+ duskdawn->time = (hour[mode] *60) + minute[mode];
+
+ // apply offsets, check for over- and underflows
+ uint16_t timeBuffer;
+ if ((uint16_t)stored.time > 720) {
+ // negative offset, time after 12:00
+ timeBuffer = (uint16_t)stored.time - 720;
+ // check for underflow
+ if (timeBuffer > (uint16_t)duskdawn->time) {
+ timeBuffer = 1440 - (timeBuffer - (uint16_t)duskdawn->time);
+ duskdawn->days = duskdawn->days >> 1;
+ duskdawn->days = duskdawn->days |= (stored.days << 6);
+ } else {
+ timeBuffer = (uint16_t)duskdawn->time - timeBuffer;
+ }
+ } else {
+ // positive offset
+ timeBuffer = (uint16_t)duskdawn->time + (uint16_t)stored.time;
+ // check for overflow
+ if (timeBuffer > 1440) {
+ timeBuffer -= 1440;
+ duskdawn->days = duskdawn->days << 1;
+ duskdawn->days = duskdawn->days |= (stored.days >> 6);
+ }
+ }
+ duskdawn->time = timeBuffer;
+}
+
String GetSun(byte dawn)
{
char stime[6];
@@ -212,23 +249,25 @@ void TimerEverySecond()
for (byte i = 0; i < MAX_TIMERS; i++) {
if (Settings.timer[i].device >= devices_present) Settings.timer[i].data = 0; // Reset timer due to change in devices present
- uint16_t set_time = Settings.timer[i].time;
+ Timer xtimer = Settings.timer[i];
+ uint16_t set_time = xtimer.time;
#ifdef USE_SUNRISE
- if ((1 == Settings.timer[i].mode) || (2 == Settings.timer[i].mode)) { // Sunrise or Sunset
- set_time = GetSunMinutes(Settings.timer[i].mode -1);
+ if ((1 == xtimer.mode) || (2 == xtimer.mode)) { // Sunrise or Sunset
+ ApplyTimerOffsets(&xtimer);
+ set_time = xtimer.time;
}
#endif
- if (Settings.timer[i].arm) {
+ if (xtimer.arm) {
if (time == set_time) {
- if (Settings.timer[i].days & days) {
- Settings.timer[i].arm = Settings.timer[i].repeat;
+ if (xtimer.days & days) {
+ Settings.timer[i].arm = xtimer.repeat;
#ifdef USE_RULES
- if (3 == Settings.timer[i].power) { // Blink becomes Rule disregarding device and allowing use of Backlog commands
+ if (3 == xtimer.power) { // Blink becomes Rule disregarding device and allowing use of Backlog commands
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"Clock\":{\"Timer\":%d}}"), i +1);
RulesProcess();
} else
#endif // USE_RULES
- ExecuteCommandPower(Settings.timer[i].device +1, Settings.timer[i].power);
+ ExecuteCommandPower(xtimer.device +1, xtimer.power);
}
}
}
@@ -241,17 +280,22 @@ void PrepShowTimer(uint8_t index)
{
char days[8] = { 0 };
- index--;
+ Timer xtimer = Settings.timer[index -1];
+
for (byte i = 0; i < 7; i++) {
uint8_t mask = 1 << i;
- snprintf(days, sizeof(days), "%s%d", days, ((Settings.timer[index].days & mask) > 0));
+ snprintf(days, sizeof(days), "%s%d", days, ((xtimer.days & mask) > 0));
}
#ifdef USE_SUNRISE
+ int16_t hour = xtimer.time / 60;
+ if ((1 == xtimer.mode) || (2 == xtimer.mode)) { // Sunrise or Sunset
+ if (hour > 11) hour = (hour -12) * -1;
+ }
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s\"" D_CMND_TIMER "%d\":{\"" D_JSON_TIMER_ARM "\":%d,\"" D_JSON_TIMER_MODE "\":%d,\"" D_JSON_TIMER_TIME "\":\"%02d:%02d\",\"" D_JSON_TIMER_DAYS "\":\"%s\",\"" D_JSON_TIMER_REPEAT "\":%d,\"" D_JSON_TIMER_OUTPUT "\":%d,\"" D_JSON_TIMER_ACTION "\":%d}"),
- mqtt_data, index +1, Settings.timer[index].arm, Settings.timer[index].mode, Settings.timer[index].time / 60, Settings.timer[index].time % 60, days, Settings.timer[index].repeat, Settings.timer[index].device +1, Settings.timer[index].power);
+ mqtt_data, index, xtimer.arm, xtimer.mode, hour, xtimer.time % 60, days, xtimer.repeat, xtimer.device +1, xtimer.power);
#else
snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s\"" D_CMND_TIMER "%d\":{\"" D_JSON_TIMER_ARM "\":%d,\"" D_JSON_TIMER_TIME "\":\"%02d:%02d\",\"" D_JSON_TIMER_DAYS "\":\"%s\",\"" D_JSON_TIMER_REPEAT "\":%d,\"" D_JSON_TIMER_OUTPUT "\":%d,\"" D_JSON_TIMER_ACTION "\":%d}"),
- mqtt_data, index +1, Settings.timer[index].arm, Settings.timer[index].time / 60, Settings.timer[index].time % 60, days, Settings.timer[index].repeat, Settings.timer[index].device +1, Settings.timer[index].power);
+ mqtt_data, index, xtimer.arm, xtimer.time / 60, xtimer.time % 60, days, xtimer.repeat, xtimer.device +1, xtimer.power);
#endif // USE_SUNRISE
}
@@ -298,18 +342,20 @@ boolean TimerCommand()
#endif
if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_TIME))].success()) {
uint16_t itime = 0;
- uint8_t value = 0;
+ int8_t value = 0;
char time_str[10];
snprintf(time_str, sizeof(time_str), root[parm_uc]);
const char *substr = strtok(time_str, ":");
if (substr != NULL) {
value = atoi(substr);
+ if (value < 0) value = abs(value) +12; // Allow entering timer offset from -11:59 to -00:01 converted to 12:01 to 23:59
if (value > 23) value = 23;
itime = value * 60;
substr = strtok(NULL, ":");
if (substr != NULL) {
value = atoi(substr);
+ if (value < 0) value = 0;
if (value > 59) value = 59;
itime += value;
}
@@ -417,25 +463,50 @@ const char HTTP_TIMER_SCRIPT[] PROGMEM =
"function gt(){" // Set hours and minutas according to mode
"var m,p,q;"
"m=qs('input[name=\"rd\"]:checked').value;" // Get mode
- "if(m==0){p=pt[ct]&0x7FF;}" // Schedule time
+ "if(m==0){p=pt[ct]&0x7FF;so(0);}" // Schedule time, hide offset span
"if(m==1){p=pt[" STR(MAX_TIMERS) "];}" // Sunrise
"if(m==2){p=pt[" STR(MAX_TIMERS +1) "];}" // Sunset
"q=Math.floor(p/60);if(q<10){q='0'+q;}qs('#ho').value=q;" // Set hours
"q=p%60;if(q<10){q='0'+q;}qs('#mi').value=q;" // Set minutes
+ "if((m==1)||(m==2)){" // Sunrise or sunset is set
+ "p=pt[ct]&0x7FF;" // Load stored time for offset calculation
+ "q=Math.floor(p/60);" // Parse hours
+ "if(q>=12){q-=12;qs('#odr').selectedIndex=1;}" // Negative offset
+ "else{qs('#odr').selectedIndex=0;}"
+ "if(q<10){q='0'+q;}qs('#oho').value=q;" // Set offset hours
+ "q=p%60;if(q<10){q='0'+q;}qs('#omi').value=q;" // Set offset minutes
+ "so(1);" // Show offset span
+ "}"
+ "}"
+ "function so(b){" // Hide or show offset items
+ "if(b==1){qs('#ofs').style='';}"
+ "else{qs('#ofs').style='display:none;';}"
"}"
#endif
"function st(){" // Save parameters to hidden area
- "var i,n,p,s;"
- "s=0;"
+ "var i,l,m,n,p,s;"
+ "m=0;s=0;"
"n=1<<30;if(eb('a0').checked){s|=n;}" // Get arm
"n=1<<29;if(eb('r0').checked){s|=n;}" // Get repeat
"for(i=0;i<7;i++){n=1<<(16+i);if(eb('w'+i).checked){s|=n;}}" // Get weekdays
#ifdef USE_SUNRISE
+ "m=qs('input[name=\"rd\"]:checked').value;" // Check mode
"s|=(qs('input[name=\"rd\"]:checked').value<<11);" // Get mode
#endif
"s|=(eb('p1').value<<27);" // Get power
"s|=(qs('#d1').selectedIndex<<23);" // Get device
- "s|=((qs('#ho').selectedIndex*60)+qs('#mi').selectedIndex)&0x7FF;" // Get time
+
+// "s|=((qs('#ho').selectedIndex*60)+qs('#mi').selectedIndex)&0x7FF;" // Get time
+
+ "if(m==0){s|=((qs('#ho').selectedIndex*60)+qs('#mi').selectedIndex)&0x7FF;}" // Get time
+#ifdef USE_SUNRISE
+ "if((m==1)||(m==2)){"
+ "l=((qs('#oho').selectedIndex*60)+qs('#omi').selectedIndex);" // Buffer offset time
+ "if(qs('#odr').selectedIndex>0){l+=720;}" // If negative offset, add 12h to given offset time
+ "s|=l&0x7FF;" // Save offset instead of time
+ "}"
+#endif
+
"pt[ct]=s;"
"eb('t0').value=pt.join();" // Save parameters from array to hidden area
"}"
@@ -468,6 +539,13 @@ const char HTTP_TIMER_SCRIPT[] PROGMEM =
"eb('bt').innerHTML=s;" // Create tabs
"o=qs('#ho');for(i=0;i<=23;i++){ce((i<10)?('0'+i):i,o);}" // Create hours select options
"o=qs('#mi');for(i=0;i<=59;i++){ce((i<10)?('0'+i):i,o);}" // Create minutes select options
+
+#ifdef USE_SUNRISE // NEW: Create offset options (+/- up to 11h, 59m)
+ "o=qs('#odr');ce('+',o);ce('-',o);" // Create offset direction select options
+ "o=qs('#oho');for(i=0;i<=11;i++){ce((i<10)?('0'+i):i,o);}" // Create offset hours select options
+ "o=qs('#omi');for(i=0;i<=59;i++){ce((i<10)?('0'+i):i,o);}" // Create offset minutes select options
+#endif
+
"o=qs('#d1');for(i=0;i<}1;i++){ce(i+1,o);}" // Create devices
"var a='" D_DAY3LIST "';"
"s='';for(i=0;i<7;i++){s+=\"\"+a.substring(i*3,(i*3)+3)+\"\"}"
@@ -503,15 +581,22 @@ const char HTTP_FORM_TIMER1[] PROGMEM =
"" D_TIMER_REPEAT ""
"
"
"