The "+/-" chooser is only needed for sunrise/sunset. Otherwise it's disabled currently. This works. But it's a little confusing. Especially, if you first had "-" there (for sunset) and then switched to normal "time", then the "-" is still there, but you can't change it, because it's disabled. It looks better, if one uses .style.visibility to hide the element. It doesn't change the layout, just the element isn't shown.
794 lines
30 KiB
C++
794 lines
30 KiB
C++
/*
|
|
xdrv_09_timers.ino - timer support for Tasmota
|
|
|
|
Copyright (C) 2019 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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#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];
|
|
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 =
|
|
"<p><form action='" WEB_HANDLE_TIMER "' method='get'><button>" D_CONFIGURE_TIMER "</button></form></p>";
|
|
|
|
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){l+=720;}" // If negative offset, 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<o.length;i++){o[i].style.cssText=\"background:#%06x;color:#%06x;font-weight:normal;\"}" // COLOR_TIMER_TAB_BACKGROUND, COLOR_TIMER_TAB_TEXT
|
|
"e.style.cssText=\"background:#%06x;color:#%06x;font-weight:bold;\";" // COLOR_FORM, COLOR_TEXT, Change style to tab/button used to open content
|
|
"s=pt[ct];" // Get parameters from array
|
|
#ifdef USE_SUNRISE
|
|
"p=(s>>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+=\"<button type='button' class='tl' onclick='ot(\"+i+\",this)'\"+b+\">\"+(i+1)+\"</button>\""
|
|
"}"
|
|
"eb('bt').innerHTML=s;" // Create tabs
|
|
"if(%d>0){" // Create Output and Action drop down boxes
|
|
"eb('oa').innerHTML=\"<b>" D_TIMER_OUTPUT "</b> <span><select style='width:60px;' id='d1'></select></span> <b>" D_TIMER_ACTION "</b> <select style='width:99px;' id='p1'></select>\";"
|
|
"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=\"<b>" D_TIMER_ACTION "</b> " 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+=\"<input id='w\"+i+\"' type='checkbox'><b>\"+a.substring(i*3,(i*3)+3)+\"</b> \"}"
|
|
"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 =
|
|
"<fieldset style='min-width:470px;text-align:center;'>"
|
|
"<legend style='text-align:left;'><b> " D_TIMER_PARAMETERS " </b></legend>"
|
|
"<form method='post' action='" WEB_HANDLE_TIMER "' onsubmit='return st();'>"
|
|
"<br><input id='e0' type='checkbox'%s><b>" D_TIMER_ENABLE "</b><br><br><hr>"
|
|
"<input id='t0' value='";
|
|
const char HTTP_FORM_TIMER2[] PROGMEM =
|
|
"' hidden><div id='bt'></div><br><br><br>"
|
|
"<div id='oa' name='oa'></div><br>"
|
|
"<div>"
|
|
"<input id='a0' type='checkbox'><b>" D_TIMER_ARM "</b> "
|
|
"<input id='r0' type='checkbox'><b>" D_TIMER_REPEAT "</b>"
|
|
"</div><br>"
|
|
"<div>";
|
|
#ifdef USE_SUNRISE
|
|
const char HTTP_FORM_TIMER3[] PROGMEM =
|
|
"<fieldset style='width:%dpx;margin:auto;text-align:left;border:0;'>"
|
|
"<input id='b0' name='rd' type='radio' value='0' onclick='gt();'><b>" D_TIMER_TIME "</b><br>"
|
|
"<input id='b1' name='rd' type='radio' value='1' onclick='gt();'><b>" D_SUNRISE "</b> (%s)<br>"
|
|
"<input id='b2' name='rd' type='radio' value='2' onclick='gt();'><b>" D_SUNSET "</b> (%s)<br>"
|
|
"</fieldset>"
|
|
"<p></p>"
|
|
"<span><select style='width:46px;' id='dr'></select></span>"
|
|
" ";
|
|
#else
|
|
const char HTTP_FORM_TIMER3[] PROGMEM =
|
|
"<b>" D_TIMER_TIME "</b> ";
|
|
#endif // USE_SUNRISE
|
|
const char HTTP_FORM_TIMER4[] PROGMEM =
|
|
"<span><select style='width:60px;' id='ho'></select></span>"
|
|
" " D_HOUR_MINUTE_SEPARATOR " "
|
|
"<span><select style='width:60px;' id='mi'></select></span>"
|
|
" <b>+/-</b> "
|
|
"<span><select style='width:60px;' id='mw'></select></span>"
|
|
"</div><br>"
|
|
"<div id='ds' name='ds'></div>";
|
|
|
|
void HandleTimerConfiguration(void)
|
|
{
|
|
if (!HttpCheckPriviledgedAccess()) { return; }
|
|
|
|
AddLog_P(LOG_LEVEL_DEBUG, S_LOG_HTTP, S_CONFIGURE_TIMER);
|
|
|
|
if (WebServer->hasArg("save")) {
|
|
TimerSaveSettings();
|
|
HandleConfiguration();
|
|
return;
|
|
}
|
|
|
|
WSContentStart_P(S_CONFIGURE_TIMER);
|
|
WSContentSend_P(HTTP_TIMER_SCRIPT1);
|
|
#ifdef USE_SUNRISE
|
|
WSContentSend_P(HTTP_TIMER_SCRIPT2);
|
|
#endif // USE_SUNRISE
|
|
WSContentSend_P(HTTP_TIMER_SCRIPT3, devices_present);
|
|
WSContentSend_P(HTTP_TIMER_SCRIPT4, WebColor(COL_TIMER_TAB_BACKGROUND), WebColor(COL_TIMER_TAB_TEXT), WebColor(COL_FORM), WebColor(COL_TEXT), devices_present);
|
|
WSContentSend_P(HTTP_TIMER_SCRIPT5, MAX_TIMERS, devices_present);
|
|
WSContentSend_P(HTTP_TIMER_SCRIPT6, devices_present);
|
|
WSContentSendStyle_P(HTTP_TIMER_STYLE, WebColor(COL_FORM));
|
|
WSContentSend_P(HTTP_FORM_TIMER1, (Settings.flag3.timers_enable) ? " checked" : ""); // CMND_TIMERS
|
|
for (uint32_t i = 0; i < MAX_TIMERS; i++) {
|
|
WSContentSend_P(PSTR("%s%u"), (i > 0) ? "," : "", Settings.timer[i].data);
|
|
}
|
|
WSContentSend_P(HTTP_FORM_TIMER2);
|
|
#ifdef USE_SUNRISE
|
|
WSContentSend_P(HTTP_FORM_TIMER3, 100 + (strlen(D_SUNSET) *12), GetSun(0).c_str(), GetSun(1).c_str());
|
|
#else
|
|
WSContentSend_P(HTTP_FORM_TIMER3);
|
|
#endif // USE_SUNRISE
|
|
WSContentSend_P(HTTP_FORM_TIMER4);
|
|
WSContentSend_P(HTTP_FORM_END);
|
|
WSContentSpaceButton(BUTTON_CONFIGURATION);
|
|
WSContentStop();
|
|
}
|
|
|
|
void TimerSaveSettings(void)
|
|
{
|
|
char tmp[MAX_TIMERS *12]; // Need space for MAX_TIMERS x 10 digit numbers separated by a comma
|
|
Timer timer;
|
|
|
|
Settings.flag3.timers_enable = WebServer->hasArg("e0"); // CMND_TIMERS
|
|
WebGetArg("t0", tmp, sizeof(tmp));
|
|
char *p = tmp;
|
|
snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_MQTT D_CMND_TIMERS " %d"), Settings.flag3.timers_enable); // CMND_TIMERS
|
|
for (uint32_t i = 0; i < MAX_TIMERS; i++) {
|
|
timer.data = strtol(p, &p, 10);
|
|
p++; // Skip comma
|
|
if (timer.time < 1440) {
|
|
bool flag = (timer.window != Settings.timer[i].window);
|
|
Settings.timer[i].data = timer.data;
|
|
if (flag) TimerSetRandomWindow(i);
|
|
}
|
|
snprintf_P(log_data, sizeof(log_data), PSTR("%s,0x%08X"), log_data, Settings.timer[i].data);
|
|
}
|
|
AddLog(LOG_LEVEL_DEBUG);
|
|
}
|
|
#endif // USE_TIMERS_WEB
|
|
#endif // USE_WEBSERVER
|
|
|
|
/*********************************************************************************************\
|
|
* Interface
|
|
\*********************************************************************************************/
|
|
|
|
bool Xdrv09(uint8_t function)
|
|
{
|
|
bool result = false;
|
|
|
|
switch (function) {
|
|
case FUNC_PRE_INIT:
|
|
TimerSetRandomWindows();
|
|
break;
|
|
#ifdef USE_WEBSERVER
|
|
#ifdef USE_TIMERS_WEB
|
|
case FUNC_WEB_ADD_BUTTON:
|
|
#if defined(USE_RULES) || defined(USE_SCRIPT)
|
|
WSContentSend_P(HTTP_BTN_MENU_TIMER);
|
|
#else
|
|
if (devices_present) { WSContentSend_P(HTTP_BTN_MENU_TIMER); }
|
|
#endif // USE_RULES
|
|
break;
|
|
case FUNC_WEB_ADD_HANDLER:
|
|
WebServer->on("/" WEB_HANDLE_TIMER, HandleTimerConfiguration);
|
|
break;
|
|
#endif // USE_TIMERS_WEB
|
|
#endif // USE_WEBSERVER
|
|
case FUNC_EVERY_SECOND:
|
|
TimerEverySecond();
|
|
break;
|
|
case FUNC_COMMAND:
|
|
result = DecodeCommand(kTimerCommands, TimerCommand);
|
|
break;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
#endif // USE_TIMERS
|