diff --git a/README.md b/README.md index 2f4d7ed1cdf3..e4a1e3fb61b5 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ## Sonoff-Tasmota Provide ESP8266 based Sonoff by [iTead Studio](https://www.itead.cc/) and ElectroDragon IoT Relay with Serial, Web and MQTT control allowing 'Over the Air' or OTA firmware updates using Arduino IDE. -Current version is **5.12.0h** - See [sonoff/_releasenotes.ino](https://github.com/arendst/Sonoff-Tasmota/blob/development/sonoff/_releasenotes.ino) for change information. +Current version is **5.12.0i** - See [sonoff/_releasenotes.ino](https://github.com/arendst/Sonoff-Tasmota/blob/development/sonoff/_releasenotes.ino) for change information. ### ATTENTION All versions diff --git a/sonoff/_releasenotes.ino b/sonoff/_releasenotes.ino index dc40e3a5bc71..d924953435d3 100644 --- a/sonoff/_releasenotes.ino +++ b/sonoff/_releasenotes.ino @@ -1,4 +1,7 @@ -/* 5.12.0h +/* 5.12.0i + * Add 16 timers using commands Timer and Timers (#1091) + * + * 5.12.0h * Add optional Arduino OTA support to be enabled in user_config.h (#1998) * Add support for Software Serial bridge using commands SerialDelimiter, SBaudrate and SSerialSend. Supports 8N1 and text only (#2190) * Add support for Hardware Serial bridge using commands SerialDelimiter, Baudrate and SerialSend. Supports 8N1 and text only (#2182) diff --git a/sonoff/i18n.h b/sonoff/i18n.h index 71a795297457..2b9ac09ed861 100644 --- a/sonoff/i18n.h +++ b/sonoff/i18n.h @@ -349,6 +349,16 @@ #define D_CMND_SBAUDRATE "SBaudrate" #define D_JSON_SSERIALRECEIVED "SSerialReceived" +// Commands xdrv_09_timers.ino +#define D_CMND_TIMER "Timer" + #define D_JSON_TIMER_ARM "Arm" + #define D_JSON_TIMER_TIME "Time" + #define D_JSON_TIMER_DAYS "Days" + #define D_JSON_TIMER_REPEAT "Repeat" + #define D_JSON_TIMER_DEVICE "Device" + #define D_JSON_TIMER_POWER "Power" +#define D_CMND_TIMERS "Timers" + /********************************************************************************************/ #ifndef MY_LANGUAGE diff --git a/sonoff/settings.h b/sonoff/settings.h index 936e54963d2e..173958b86022 100644 --- a/sonoff/settings.h +++ b/sonoff/settings.h @@ -89,6 +89,20 @@ typedef union { }; } SysBitfield2; +typedef union { + uint32_t data; + struct { + uint32_t time : 11; // bits 0 - 10 = minutes in a day + uint32_t mday : 5; // bits 11 - 15 = optional day in a month + uint32_t days : 7; // bits 16 - 22 = week day mask + uint32_t device : 4; // bits 23 - 26 = 16 devices + uint32_t power : 2; // bits 27 - 28 = 4 power states - Off, On, Toggle + uint32_t repeat : 1; // bit 29 + uint32_t arm : 1; // bit 30 + uint32_t spare : 1; // bit 31 + }; +} Timer; + struct SYSCFG { unsigned long cfg_holder; // 000 unsigned long save_flag; // 004 @@ -236,6 +250,11 @@ struct SYSCFG { uint16_t pulse_counter_debounce; // 5D2 uint8_t rf_code[17][9]; // 5D4 + byte free_66d[3]; // 66D + + Timer timer[MAX_TIMERS]; // 670 + + // 6B0 - FFF free locations } Settings; struct RTCMEM { diff --git a/sonoff/settings.ino b/sonoff/settings.ino index ac706f4aa9f6..d17f33d357c2 100644 --- a/sonoff/settings.ino +++ b/sonoff/settings.ino @@ -910,6 +910,9 @@ void SettingsDelta() Settings.sbaudrate = SOFT_BAUDRATE / 1200; Settings.serial_delimiter = 0xff; } + if (Settings.version < 0x050C0009) { + memset(&Settings.timer, 0x00, sizeof(Timer) * MAX_TIMERS); + } Settings.version = VERSION; SettingsSave(1); diff --git a/sonoff/sonoff.h b/sonoff/sonoff.h index 58079e2df991..5511856c49c8 100644 --- a/sonoff/sonoff.h +++ b/sonoff/sonoff.h @@ -43,6 +43,7 @@ typedef unsigned long power_t; // Power (Relay) type #define MAX_SWITCHES 4 // Max number of switches #define MAX_PWMS 5 // Max number of PWM channels #define MAX_COUNTERS 4 // Max number of counter sensors +#define MAX_TIMERS 16 // Max number of Timers #define MAX_PULSETIMERS 8 // Max number of supported pulse timers #define MAX_FRIENDLYNAMES 4 // Max number of Friendly names #define MAX_DOMOTICZ_IDX 4 // Max number of Domoticz device, key and switch indices diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino index c69a5784a78e..96ade50054fe 100644 --- a/sonoff/sonoff.ino +++ b/sonoff/sonoff.ino @@ -25,7 +25,7 @@ - Select IDE Tools - Flash Size: "1M (no SPIFFS)" ====================================================*/ -#define VERSION 0x050C0008 // 5.12.0h +#define VERSION 0x050C0009 // 5.12.0i // Location specific includes #include // Arduino_Esp8266 version information (ARDUINO_ESP8266_RELEASE and ARDUINO_ESP8266_RELEASE_2_3_0) diff --git a/sonoff/support.ino b/sonoff/support.ino index 6a1293ccf1a8..d0250690a7e3 100644 --- a/sonoff/support.ino +++ b/sonoff/support.ino @@ -241,6 +241,32 @@ char* Unescape(char* buffer, uint16_t* size) return buffer; } +char* UpperCase(char* dest, const char* source) +{ + char* write = dest; + const char* read = source; + char ch = '.'; + + while (ch != '\0') { + ch = *read++; + *write++ = toupper(ch); + } + return dest; +} + +char* UpperCase_P(char* dest, const char* source) +{ + char* write = dest; + const char* read = source; + char ch = '.'; + + while (ch != '\0') { + ch = pgm_read_byte(read++); + *write++ = toupper(ch); + } + return dest; +} + boolean ParseIp(uint32_t* addr, const char* str) { uint8_t *part = (uint8_t*)addr; diff --git a/sonoff/user_config.h b/sonoff/user_config.h index b919f677345f..5de54309a8f9 100644 --- a/sonoff/user_config.h +++ b/sonoff/user_config.h @@ -162,6 +162,8 @@ #define NTP_SERVER2 "nl.pool.ntp.org" // [NtpServer2] Select second NTP server by name or IP address (5.39.184.5) #define NTP_SERVER3 "0.nl.pool.ntp.org" // [NtpServer3] Select third NTP server by name or IP address (93.94.224.67) +#define USE_TIMERS // Add support for up to 16 timers (+2k2 code) + // -- Time - Start Daylight Saving Time and timezone offset from UTC in minutes #define TIME_DST North, Last, Sun, Mar, 2, +120 // Northern Hemisphere, Last sunday in march at 02:00 +120 minutes diff --git a/sonoff/xdrv_09_timers.ino b/sonoff/xdrv_09_timers.ino new file mode 100644 index 000000000000..350f49dfd6dd --- /dev/null +++ b/sonoff/xdrv_09_timers.ino @@ -0,0 +1,205 @@ +/* + xdrv_09_timers.ino - timer support for Sonoff-Tasmota + + Copyright (C) 2018 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,"Time":"09:23","Days":"--TW--S","Repeat":1,"Device":1,"Power":1} + * + * Arm 0 = Off, 1 = On + * Time hours:minutes + * Days 7 day character mask starting with Sunday (SMTWTFS). 0 or - = Off, any other value = On + * Repeat 0 = Execute once, 1 = Execute again + * Device 1..16 + * Power 0 = Off, 1 = On, 2 = Toggle, 3 = Blink + * +\*********************************************************************************************/ + +enum TimerCommands { CMND_TIMER, CMND_TIMERS }; +const char kTimerCommands[] PROGMEM = D_CMND_TIMER "|" D_CMND_TIMERS ; + +power_t fired = 0; + +void TimerEverySecond() +{ + if (RtcTime.valid) { + uint16_t time = (RtcTime.hour * 60) + RtcTime.minute; + uint8_t days = 1 << (RtcTime.day_of_week -1); + + for (byte i = 0; i < MAX_TIMERS; i++) { + if (Settings.timer[i].arm) { + if (time == Settings.timer[i].time) { + if (!bitRead(fired, i) && (Settings.timer[i].days & days)) { + bitSet(fired, i); + Settings.timer[i].arm = Settings.timer[i].repeat; + ExecuteCommandPower(Settings.timer[i].device +1, Settings.timer[i].power); + } + } else { + bitClear(fired, i); + } + } + } + } +} + +void PrepShowTimer(uint8_t index) +{ + char days[8] = { 0 }; + + index--; + 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_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_DEVICE "\":%d,\"" D_JSON_TIMER_POWER "\":%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); +} + +/*********************************************************************************************\ + * Commands +\*********************************************************************************************/ + +boolean TimerCommand() +{ + char command [CMDSZ]; + char dataBufUc[XdrvMailbox.data_len]; + boolean serviced = true; + uint8_t index = XdrvMailbox.index; + + UpperCase(dataBufUc, XdrvMailbox.data); + int command_code = GetCommandCode(command, sizeof(command), XdrvMailbox.topic, kTimerCommands); + if ((CMND_TIMER == command_code) && (index > 0) && (index <= MAX_TIMERS)) { + uint8_t error = 0; + + if (XdrvMailbox.data_len) { + StaticJsonBuffer<128> jsonBuffer; + JsonObject& root = jsonBuffer.parseObject(dataBufUc); + if (!root.success()) { + snprintf_P(mqtt_data, sizeof(mqtt_data), 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); + } + if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_TIME))].success()) { + uint16_t itime = 0; + uint8_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 > 23) value = 23; + itime = value * 60; + substr = strtok(NULL, ":"); + if (substr != NULL) { + value = atoi(substr); + if (value > 59) value = 59; + itime += value; + } + } + Settings.timer[index].time = itime; + } + 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]; + char ch = '.'; + + uint8_t i = 0; + while ((ch != '\0') && (i < 7)) { + ch = *tday++; + if (ch == '-') ch = '0'; + uint8_t mask = 1 << i++; + Settings.timer[index].days |= (ch == '0') ? 0 : mask; + } + } + 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_DEVICE))].success()) { + Settings.timer[index].device = ((uint8_t)root[parm_uc] -1) & 0x0F; + } + if (root[UpperCase_P(parm_uc, PSTR(D_JSON_TIMER_POWER))].success()) { + Settings.timer[index].power = (uint8_t)root[parm_uc] & 0x03; + } + if (Settings.timer[index].arm) bitClear(fired, index); + + index++; + } + } + if (!error) { + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{")); + PrepShowTimer(index); + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s}"), mqtt_data); + } + } + else if (CMND_TIMERS == command_code) { + byte jsflg = 0; + byte lines = 1; + for (byte i = 0; i < MAX_TIMERS; i++) { + if (!jsflg) { + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("{\"" D_CMND_TIMERS "%d\":{"), lines++); + } else { + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s,"), mqtt_data); + } + jsflg = 1; + PrepShowTimer(i +1); + if ((strlen(mqtt_data) > (LOGSZ - TOPSZ)) || (i == MAX_TIMERS -1)) { + snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s}}"), mqtt_data); + MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_TIMERS)); + jsflg = 0; + } + } + mqtt_data[0] = '\0'; + } + else serviced = false; + + return serviced; +} + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +#define XDRV_09 + +boolean Xdrv09(byte function) +{ + boolean result = false; + + switch (function) { + case FUNC_EVERY_SECOND: + TimerEverySecond(); + break; + case FUNC_COMMAND: + result = TimerCommand(); + break; + } + return result; +} + +#endif // USE_TIMERS \ No newline at end of file