/*
xdrv_09_timers.ino - timer support for Tasmota
Copyright (C) 2020 Theo Arends
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 3 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, see .
*/
#ifdef USE_TIMERS
/*********************************************************************************************\
* Timers
*
* Arm a timer using one or all of the following JSON values:
* {"Arm":1,"Mode":0,"Time":"09:23","Window":0,"Days":"--TW--S","Repeat":1,"Output":1,"Action":1}
*
* Arm 0 = Off, 1 = On
* Mode 0 = Schedule, 1 = Sunrise, 2 = Sunset
* Time hours:minutes
* Window minutes (0..15)
* Days 7 day character mask starting with Sunday (SMTWTFS). 0 or - = Off, any other value = On
* Repeat 0 = Execute once, 1 = Execute again
* Output 1..16
* Action 0 = Off, 1 = On, 2 = Toggle, 3 = Blink or Rule if USE_RULES enabled
*
* Window allows Time offset for +/- 15 minutes max as long as Time is not within Window from 00:00
\*********************************************************************************************/
#define XDRV_09 9
const char kTimerCommands[] PROGMEM = "|" // No prefix
D_CMND_TIMER "|" D_CMND_TIMERS
#ifdef USE_SUNRISE
"|" D_CMND_LATITUDE "|" D_CMND_LONGITUDE
#endif
;
void (* const TimerCommand[])(void) PROGMEM = {
&CmndTimer, &CmndTimers
#ifdef USE_SUNRISE
, &CmndLatitude, &CmndLongitude
#endif
};
uint16_t timer_last_minute = 60;
int8_t timer_window[MAX_TIMERS] = { 0 };
#ifdef USE_SUNRISE
/*********************************************************************************************\
* Sunrise and sunset (+13k code)
*
* https://forum.arduino.cc/index.php?topic=218280.0
* Source: C-Programm von http://lexikon.astronomie.info/zeitgleichung/neu.html
* Rewrite for Arduino by 'jurs' for German Arduino forum
\*********************************************************************************************/
const float pi2 = TWO_PI;
const float pi = PI;
const float RAD = DEG_TO_RAD;
float JulianischesDatum(void)
{
// Gregorianischer Kalender
int Gregor;
int Jahr = RtcTime.year;
int Monat = RtcTime.month;
int Tag = RtcTime.day_of_month;
if (Monat <= 2) {
Monat += 12;
Jahr -= 1;
}
Gregor = (Jahr / 400) - (Jahr / 100) + (Jahr / 4); // Gregorianischer Kalender
return 2400000.5f + 365.0f*Jahr - 679004.0f + Gregor + (int)(30.6001f * (Monat +1)) + Tag + 0.5f;
}
float InPi(float x)
{
int n = (int)(x / pi2);
x = x - n*pi2;
if (x < 0) x += pi2;
return x;
}
float eps(float T)
{
// Neigung der Erdachse
return RAD * (23.43929111f + (-46.8150f*T - 0.00059f*T*T + 0.001813f*T*T*T)/3600.0f);
}
float BerechneZeitgleichung(float *DK,float T)
{
float RA_Mittel = 18.71506921f + 2400.0513369f*T +(2.5862e-5f - 1.72e-9f*T)*T*T;
float M = InPi(pi2 * (0.993133f + 99.997361f*T));
float L = InPi(pi2 * (0.7859453f + M/pi2 + (6893.0f*sinf(M)+72.0f*sinf(2.0f*M)+6191.2f*T) / 1296.0e3f));
float e = eps(T);
float RA = atanf(tanf(L)*cosf(e));
if (RA < 0.0) RA += pi;
if (L > pi) RA += pi;
RA = 24.0*RA/pi2;
*DK = asinf(sinf(e)*sinf(L));
// Damit 0<=RA_Mittel<24
RA_Mittel = 24.0f * InPi(pi2*RA_Mittel/24.0f)/pi2;
float dRA = RA_Mittel - RA;
if (dRA < -12.0f) dRA += 24.0f;
if (dRA > 12.0f) dRA -= 24.0f;
dRA = dRA * 1.0027379f;
return dRA;
}
void DuskTillDawn(uint8_t *hour_up,uint8_t *minute_up, uint8_t *hour_down, uint8_t *minute_down)
{
float JD2000 = 2451545.0f;
float JD = JulianischesDatum();
float T = (JD - JD2000) / 36525.0f;
float DK;
/*
h (D) = -0.8333 normaler SA & SU-Gang
h (D) = -6.0 civile Dämmerung
h (D) = -12.0 nautische Dämmerung
h (D) = -18.0 astronomische Dämmerung
*/
// double h = -50/60.0*RAD;
float h = SUNRISE_DAWN_ANGLE *RAD;
float B = (((float)Settings.latitude)/1000000) * RAD; // geographische Breite
float GeographischeLaenge = ((float)Settings.longitude)/1000000;
// double Zeitzone = 0; //Weltzeit
// double Zeitzone = 1; //Winterzeit
// double Zeitzone = 2.0; //Sommerzeit
float Zeitzone = ((float)Rtc.time_timezone) / 60;
float Zeitgleichung = BerechneZeitgleichung(&DK, T);
float Zeitdifferenz = 12.0f*acosf((sinf(h) - sinf(B)*sinf(DK)) / (cosf(B)*cosf(DK)))/pi;
float AufgangOrtszeit = 12.0f - Zeitdifferenz - Zeitgleichung;
float UntergangOrtszeit = 12.0f + Zeitdifferenz - Zeitgleichung;
float AufgangWeltzeit = AufgangOrtszeit - GeographischeLaenge / 15.0f;
float UntergangWeltzeit = UntergangOrtszeit - GeographischeLaenge / 15.0f;
float Aufgang = AufgangWeltzeit + Zeitzone; // In Stunden
if (Aufgang < 0.0f) {
Aufgang += 24.0f;
} else {
if (Aufgang >= 24.0f) Aufgang -= 24.0f;
}
float Untergang = UntergangWeltzeit + Zeitzone;
if (Untergang < 0.0f) {
Untergang += 24.0f;
} else {
if (Untergang >= 24.0f) Untergang -= 24.0f;
}
int AufgangMinuten = (int)(60.0f*(Aufgang - (int)Aufgang)+0.5f);
int AufgangStunden = (int)Aufgang;
if (AufgangMinuten >= 60.0f) {
AufgangMinuten -= 60.0f;
AufgangStunden++;
} else {
if (AufgangMinuten < 0.0f) {
AufgangMinuten += 60.0f;
AufgangStunden--;
if (AufgangStunden < 0.0f) AufgangStunden += 24.0f;
}
}
int UntergangMinuten = (int)(60.0f*(Untergang - (int)Untergang)+0.5f);
int UntergangStunden = (int)Untergang;
if (UntergangMinuten >= 60.0f) {
UntergangMinuten -= 60.0f;
UntergangStunden++;
} else {
if (UntergangMinuten<0) {
UntergangMinuten += 60.0f;
UntergangStunden--;
if (UntergangStunden < 0.0f) UntergangStunden += 24.0f;
}
}
*hour_up = AufgangStunden;
*minute_up = AufgangMinuten;
*hour_down = UntergangStunden;
*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 > 719) {
// 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 |= (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 |= (stored.days >> 6);
}
}
duskdawn->time = timeBuffer;
}
String GetSun(uint32_t dawn)
{
char stime[6];
uint8_t hour[2];
uint8_t minute[2];
DuskTillDawn(&hour[0], &minute[0], &hour[1], &minute[1]);
dawn &= 1;
snprintf_P(stime, sizeof(stime), PSTR("%02d:%02d"), hour[dawn], minute[dawn]);
return String(stime);
}
uint16_t SunMinutes(uint32_t dawn)
{
uint8_t hour[2];
uint8_t minute[2];
DuskTillDawn(&hour[0], &minute[0], &hour[1], &minute[1]);
dawn &= 1;
return (hour[dawn] *60) + minute[dawn];
}
#endif // USE_SUNRISE
/*******************************************************************************************/
void TimerSetRandomWindow(uint32_t index)
{
timer_window[index] = 0;
if (Settings.timer[index].window) {
timer_window[index] = (random(0, (Settings.timer[index].window << 1) +1)) - Settings.timer[index].window; // -15 .. 15
}
}
void TimerSetRandomWindows(void)
{
for (uint32_t i = 0; i < MAX_TIMERS; i++) { TimerSetRandomWindow(i); }
}
void TimerEverySecond(void)
{
if (RtcTime.valid) {
if (!RtcTime.hour && !RtcTime.minute && !RtcTime.second) { TimerSetRandomWindows(); } // Midnight
if (Settings.flag3.timers_enable && // CMND_TIMERS
(uptime > 60) && (RtcTime.minute != timer_last_minute)) { // Execute from one minute after restart every minute only once
timer_last_minute = RtcTime.minute;
int32_t time = (RtcTime.hour *60) + RtcTime.minute;
uint8_t days = 1 << (RtcTime.day_of_week -1);
for (uint32_t 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
Timer xtimer = Settings.timer[i];
#ifdef USE_SUNRISE
if ((1 == xtimer.mode) || (2 == xtimer.mode)) { // Sunrise or Sunset
ApplyTimerOffsets(&xtimer);
}
#endif
if (xtimer.arm) {
int32_t set_time = xtimer.time + timer_window[i]; // Add random time offset
if (set_time < 0) {
set_time = abs(timer_window[i]); // After midnight and within negative window so stay today but allow positive randomness;
}
if (set_time > 1439) {
set_time = xtimer.time - abs(timer_window[i]); // Before midnight and within positive window so stay today but allow negative randomness;
}
if (set_time > 1439) { set_time = 1439; } // Stay today
DEBUG_DRIVER_LOG(PSTR("TIM: Timer %d, Time %d, Window %d, SetTime %d"), i +1, xtimer.time, timer_window[i], set_time);
if (time == set_time) {
if (xtimer.days & days) {
Settings.timer[i].arm = xtimer.repeat;
#if defined(USE_RULES) || defined(USE_SCRIPT)
if (POWER_BLINK == xtimer.power) { // Blink becomes Rule disregarding device and allowing use of Backlog commands
Response_P(PSTR("{\"Clock\":{\"Timer\":%d}}"), i +1);
XdrvRulesProcess();
} else
#endif // USE_RULES
if (devices_present) { ExecuteCommandPower(xtimer.device +1, xtimer.power, SRC_TIMER); }
}
}
}
}
}
}
}
void PrepShowTimer(uint32_t index)
{
Timer xtimer = Settings.timer[index -1];
char days[8] = { 0 };
for (uint32_t i = 0; i < 7; i++) {
uint8_t mask = 1 << i;
snprintf(days, sizeof(days), "%s%d", days, ((xtimer.days & mask) > 0));
}
char soutput[80];
soutput[0] = '\0';
if (devices_present) {
snprintf_P(soutput, sizeof(soutput), PSTR(",\"" D_JSON_TIMER_OUTPUT "\":%d"), xtimer.device +1);
}
#ifdef USE_SUNRISE
char sign[2] = { 0 };
int16_t hour = xtimer.time / 60;
if ((1 == xtimer.mode) || (2 == xtimer.mode)) { // Sunrise or Sunset
if (hour > 11) {
hour -= 12;
sign[0] = '-';
}
}
ResponseAppend_P(PSTR("\"" D_CMND_TIMER "%d\":{\"" D_JSON_TIMER_ARM "\":%d,\"" D_JSON_TIMER_MODE "\":%d,\"" D_JSON_TIMER_TIME "\":\"%s%02d:%02d\",\"" D_JSON_TIMER_WINDOW "\":%d,\"" D_JSON_TIMER_DAYS "\":\"%s\",\"" D_JSON_TIMER_REPEAT "\":%d%s,\"" D_JSON_TIMER_ACTION "\":%d}"),
index, xtimer.arm, xtimer.mode, sign, hour, xtimer.time % 60, xtimer.window, days, xtimer.repeat, soutput, xtimer.power);
#else
ResponseAppend_P(PSTR("\"" D_CMND_TIMER "%d\":{\"" D_JSON_TIMER_ARM "\":%d,\"" D_JSON_TIMER_TIME "\":\"%02d:%02d\",\"" D_JSON_TIMER_WINDOW "\":%d,\"" D_JSON_TIMER_DAYS "\":\"%s\",\"" D_JSON_TIMER_REPEAT "\":%d%s,\"" D_JSON_TIMER_ACTION "\":%d}"),
index, xtimer.arm, xtimer.time / 60, xtimer.time % 60, xtimer.window, days, xtimer.repeat, soutput, xtimer.power);
#endif // USE_SUNRISE
}
/*********************************************************************************************\
* Commands
\*********************************************************************************************/
void CmndTimer(void)
{
uint32_t index = XdrvMailbox.index;
if ((index > 0) && (index <= MAX_TIMERS)) {
uint32_t error = 0;
if (XdrvMailbox.data_len) {
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= MAX_TIMERS)) {
if (XdrvMailbox.payload == 0) {
Settings.timer[index -1].data = 0; // Clear timer
} else {
Settings.timer[index -1].data = Settings.timer[XdrvMailbox.payload -1].data; // Copy timer
}
} else {
//#ifndef USE_RULES
#if defined(USE_RULES)==0 && defined(USE_SCRIPT)==0
if (devices_present) {
#endif
char dataBufUc[XdrvMailbox.data_len + 1];
UpperCase(dataBufUc, XdrvMailbox.data);
StaticJsonBuffer<256> jsonBuffer;
JsonObject& root = jsonBuffer.parseObject(dataBufUc);
if (!root.success()) {
Response_P(PSTR("{\"" D_CMND_TIMER "%d\":\"" D_JSON_INVALID_JSON "\"}"), index); // JSON decode failed
error = 1;
}
else {
char parm_uc[10];
index--;
if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_ARM))].success()) {
Settings.timer[index].arm = (root[parm_uc] != 0);
}
#ifdef USE_SUNRISE
if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_MODE))].success()) {
Settings.timer[index].mode = (uint8_t)root[parm_uc] & 0x03;
}
#endif
if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_TIME))].success()) {
uint16_t itime = 0;
int8_t value = 0;
uint8_t sign = 0;
char time_str[10];
strlcpy(time_str, root[parm_uc], sizeof(time_str));
const char *substr = strtok(time_str, ":");
if (substr != nullptr) {
if (strchr(substr, '-')) {
sign = 1;
substr++;
}
value = atoi(substr);
if (sign) { 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(nullptr, ":");
if (substr != nullptr) {
value = atoi(substr);
if (value < 0) { value = 0; }
if (value > 59) { value = 59; }
itime += value;
}
}
Settings.timer[index].time = itime;
}
if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_WINDOW))].success()) {
Settings.timer[index].window = (uint8_t)root[parm_uc] & 0x0F;
TimerSetRandomWindow(index);
}
if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_DAYS))].success()) {
// SMTWTFS = 1234567 = 0011001 = 00TW00S = --TW--S
Settings.timer[index].days = 0;
const char *tday = root[parm_uc];
uint8_t i = 0;
char ch = *tday++;
while ((ch != '\0') && (i < 7)) {
if (ch == '-') { ch = '0'; }
uint8_t mask = 1 << i++;
Settings.timer[index].days |= (ch == '0') ? 0 : mask;
ch = *tday++;
}
}
if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_REPEAT))].success()) {
Settings.timer[index].repeat = (root[parm_uc] != 0);
}
if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_OUTPUT))].success()) {
uint8_t device = ((uint8_t)root[parm_uc] -1) & 0x0F;
Settings.timer[index].device = (device < devices_present) ? device : 0;
}
if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_ACTION))].success()) {
uint8_t action = (uint8_t)root[parm_uc] & 0x03;
Settings.timer[index].power = (devices_present) ? action : 3; // If no devices than only allow rules
}
index++;
}
//#ifndef USE_RULES
#if defined(USE_RULES)==0 && defined(USE_SCRIPT)==0
} else {
Response_P(PSTR("{\"" D_CMND_TIMER "%d\":\"" D_JSON_TIMER_NO_DEVICE "\"}"), index); // No outputs defined so nothing to control
error = 1;
}
#endif
}
}
if (!error) {
Response_P(PSTR("{"));
PrepShowTimer(index);
ResponseJsonEnd();
}
}
}
void CmndTimers(void)
{
if (XdrvMailbox.data_len) {
if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) {
Settings.flag3.timers_enable = XdrvMailbox.payload; // CMND_TIMERS
}
if (XdrvMailbox.payload == 2) {
Settings.flag3.timers_enable = !Settings.flag3.timers_enable; // CMND_TIMERS
}
}
ResponseCmndStateText(Settings.flag3.timers_enable); // CMND_TIMERS
MqttPublishPrefixTopic_P(RESULT_OR_STAT, XdrvMailbox.command);
uint32_t jsflg = 0;
uint32_t lines = 1;
for (uint32_t i = 0; i < MAX_TIMERS; i++) {
if (!jsflg) {
Response_P(PSTR("{\"" D_CMND_TIMERS "%d\":{"), lines++);
} else {
ResponseAppend_P(PSTR(","));
}
jsflg++;
PrepShowTimer(i +1);
if (jsflg > 3) {
ResponseJsonEndEnd();
MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_TIMERS));
jsflg = 0;
}
}
mqtt_data[0] = '\0';
}
#ifdef USE_SUNRISE
void CmndLongitude(void)
{
if (XdrvMailbox.data_len) {
Settings.longitude = (int)(CharToFloat(XdrvMailbox.data) *1000000);
}
ResponseCmndFloat((float)(Settings.longitude) /1000000, 6);
}
void CmndLatitude(void)
{
if (XdrvMailbox.data_len) {
Settings.latitude = (int)(CharToFloat(XdrvMailbox.data) *1000000);
}
ResponseCmndFloat((float)(Settings.latitude) /1000000, 6);
}
#endif // USE_SUNRISE
/*********************************************************************************************\
* Presentation
\*********************************************************************************************/
#ifdef USE_WEBSERVER
#ifdef USE_TIMERS_WEB
#define WEB_HANDLE_TIMER "tm"
const char S_CONFIGURE_TIMER[] PROGMEM = D_CONFIGURE_TIMER;
const char HTTP_BTN_MENU_TIMER[] PROGMEM =
"
";
const char HTTP_TIMER_SCRIPT1[] PROGMEM =
"var pt=[],ct=99;"
"function ce(i,q){" // Create select option
"var o=document.createElement('option');"
"o.textContent=i;"
"q.appendChild(o);"
"}";
#ifdef USE_SUNRISE
const char HTTP_TIMER_SCRIPT2[] PROGMEM =
"function gt(){" // Set hours and minutes according to mode
"var m,p,q;"
"m=qs('input[name=\"rd\"]:checked').value;" // Get mode
"p=pt[ct]&0x7FF;" // Get time
"if(m==0){" // Time is set
"so(0);" // Hide offset span and allow Hour 00..23
"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
"so(1);" // Show offset span and allow Hour 00..11
"q=Math.floor(p/60);" // Parse hours
"if(q>=12){q-=12;qs('#dr').selectedIndex=1;}" // Negative offset
"else{qs('#dr').selectedIndex=0;}"
"if(q<10){q='0'+q;}qs('#ho').value=q;" // Set offset hours
"q=p%%60;if(q<10){q='0'+q;}qs('#mi').value=q;" // Set offset minutes
"}"
"}"
"function so(b){" // Hide or show offset items
"o=qs('#ho');"
"e=o.childElementCount;"
"if(b==1){"
"qs('#dr').style.visibility='';"
"if(e>12){for(i=12;i<=23;i++){o.removeChild(o.lastElementChild);}}" // Create offset hours select options
"}else{"
"qs('#dr').style.visibility='hidden';"
"if(e<23){for(i=12;i<=23;i++){ce(i,o);}}" // Create hours select options
"}"
"}";
#endif
const char HTTP_TIMER_SCRIPT3[] PROGMEM =
"function st(){" // Save parameters to hidden area
"var i,l,m,n,p,s;"
"m=0;s=0;"
"n=1<<31;if(eb('a0').checked){s|=n;}" // Get arm
"n=1<<15;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<<29);" // Get mode
#endif
"if(%d>0){"
"i=qs('#d1').selectedIndex;if(i>=0){s|=(i<<23);}" // Get output
"s|=(qs('#p1').selectedIndex<<27);" // Get action
"}else{"
"s|=3<<27;" // Get action (rule)
"}"
"l=((qs('#ho').selectedIndex*60)+qs('#mi').selectedIndex)&0x7FF;"
"if(m==0){s|=l;}" // Get time
#ifdef USE_SUNRISE
"if((m==1)||(m==2)){"
"if(qs('#dr').selectedIndex>0){if(l>0){l+=720;}}" // If negative offset and delta-time > 0, add 12h to given offset time
"s|=l&0x7FF;" // Save offset instead of time
"}"
#endif
"s|=((qs('#mw').selectedIndex)&0x0F)<<11;" // Get window minutes
"pt[ct]=s;"
"eb('t0').value=pt.join();" // Save parameters from array to hidden area
"}";
const char HTTP_TIMER_SCRIPT4[] PROGMEM =
"function ot(t,e){" // Select tab and update elements
"var i,n,o,p,q,s;"
"if(ct<99){st();}" // Save changes
"ct=t;"
"o=document.getElementsByClassName('tl');" // Restore style to all tabs/buttons
"for(i=0;i>29)&3;eb('b'+p).checked=1;" // Set mode
"gt();" // Set hours and minutes according to mode
#else
"p=s&0x7FF;" // Get time
"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
#endif
"q=(s>>11)&0xF;if(q<10){q='0'+q;}qs('#mw').value=q;" // Set window minutes
"for(i=0;i<7;i++){p=(s>>(16+i))&1;eb('w'+i).checked=p;}" // Set weekdays
"if(%d>0){"
"p=(s>>23)&0xF;qs('#d1').value=p+1;" // Set output
"p=(s>>27)&3;qs('#p1').selectedIndex=p;" // Set action
"}"
"p=(s>>15)&1;eb('r0').checked=p;" // Set repeat
"p=(s>>31)&1;eb('a0').checked=p;" // Set arm
"}";
const char HTTP_TIMER_SCRIPT5[] PROGMEM =
"function it(){" // Initialize elements and select first tab
"var b,i,o,s;"
"pt=eb('t0').value.split(',').map(Number);" // Get parameters from hidden area to array
"s='';"
"for(i=0;i<%d;i++){"
"b='';"
"if(0==i){b=\" id='dP'\";}"
"s+=\"\""
"}"
"eb('bt').innerHTML=s;" // Create tabs
"if(%d>0){" // Create Output and Action drop down boxes
"eb('oa').innerHTML=\"" D_TIMER_OUTPUT " " D_TIMER_ACTION " \";"
"o=qs('#p1');ce('" D_OFF "',o);ce('" D_ON "',o);ce('" D_TOGGLE "',o);" // Create offset direction select options
#if defined(USE_RULES) || defined(USE_SCRIPT)
"ce('" D_RULE "',o);"
#else
"ce('" D_BLINK "',o);"
#endif
"}else{"
"eb('oa').innerHTML=\"" D_TIMER_ACTION " " D_RULE "\";" // No outputs but rule is allowed
"}";
const char HTTP_TIMER_SCRIPT6[] PROGMEM =
#ifdef USE_SUNRISE
"o=qs('#dr');ce('+',o);ce('-',o);" // Create offset direction select options
#endif
"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
"o=qs('#mw');for(i=0;i<=15;i++){ce((i<10)?('0'+i):i,o);}" // Create window minutes select options
"o=qs('#d1');for(i=0;i<%d;i++){ce(i+1,o);}" // Create outputs
"var a='" D_DAY3LIST "';"
// "s='';for(i=0;i<7;i++){s+=\"\"+a.substring(i*3,(i*3)+3)+\" \"}"
// "s='';for(i=0;i<7;i++){s+=\" \"}"
"s='';for(i=0;i<7;i++){s+=\" \"}"
"eb('ds').innerHTML=s;" // Create weekdays
"eb('dP').click();" // Get the element with id='dP' and click on it
"}"
"wl(it);";
const char HTTP_TIMER_STYLE[] PROGMEM =
".tl{float:left;border-radius:0;border:1px solid #%06x;padding:1px;width:6.25%%;}"; // COLOR_FORM, Border color needs to be the same as Fieldset background color from HTTP_HEAD_STYLE1 (transparent won't work)
const char HTTP_FORM_TIMER1[] PROGMEM =
"