diff --git a/README.md b/README.md index 6b300d613..42fbc0dfc 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ If you like **Sonoff-Tasmota**, give it a star, or fork it and contribute! ### Development: [![Build Status](https://img.shields.io/travis/arendst/Sonoff-Tasmota.svg)](https://travis-ci.org/arendst/Sonoff-Tasmota) -Current version is **5.12.0l** - See [sonoff/_releasenotes.ino](https://github.com/arendst/Sonoff-Tasmota/blob/development/sonoff/_releasenotes.ino) for change information. +Current version is **5.12.0m** - See [sonoff/_releasenotes.ino](https://github.com/arendst/Sonoff-Tasmota/blob/development/sonoff/_releasenotes.ino) for change information. ### Quick install diff --git a/sonoff/_releasenotes.ino b/sonoff/_releasenotes.ino index 229aae0b4..ea8d97155 100644 --- a/sonoff/_releasenotes.ino +++ b/sonoff/_releasenotes.ino @@ -1,4 +1,9 @@ -/* 5.12.0l +/* 5.12.0m + * Reinit timers to accomodate random window (#2447) + * Add random window to timers (#2447) + * Add optional KNX IP Protocol Support (#2402) + * + * 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) diff --git a/sonoff/i18n.h b/sonoff/i18n.h index ce984f1e4..bee43f02d 100644 --- a/sonoff/i18n.h +++ b/sonoff/i18n.h @@ -361,6 +361,7 @@ #define D_JSON_TIMER_ARM "Arm" #define D_JSON_TIMER_MODE "Mode" #define D_JSON_TIMER_TIME "Time" + #define D_JSON_TIMER_WINDOW "Window" #define D_JSON_TIMER_DAYS "Days" #define D_JSON_TIMER_REPEAT "Repeat" #define D_JSON_TIMER_OUTPUT "Output" diff --git a/sonoff/settings.h b/sonoff/settings.h index f3281aae2..806bcdc02 100644 --- a/sonoff/settings.h +++ b/sonoff/settings.h @@ -92,14 +92,14 @@ typedef union { typedef union { uint32_t data; struct { - uint32_t time : 11; // bits 0 - 10 = minutes in a day - uint32_t mode : 5; // bits 11 - 15 = timer modes - Scheduler, Sunrise, Sunset - 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, Blink - uint32_t repeat : 1; // bit 29 - uint32_t arm : 1; // bit 30 - uint32_t spare : 1; // bit 31 + uint32_t time : 11; // bits 0 - 10 = minutes in a day + uint32_t window : 4; // bits 11 - 14 = minutes random window + uint32_t repeat : 1; // bit 15 + 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, Blink or Rule + uint32_t mode : 2; // bits 29 - 30 = timer modes - Scheduler, Sunrise, Sunset + uint32_t arm : 1; // bit 31 }; } Timer; @@ -258,11 +258,11 @@ struct SYSCFG { byte knx_GA_param[MAX_KNX_GA]; // 6E2 Type of Input (relay changed, button pressed, sensor read <-teleperiod) byte knx_CB_param[MAX_KNX_CB]; // 6EC Type of Output (set relay, toggle relay, reply sensor value) - byte free_6b8[10]; // 6F6 + byte free_6f6[266]; // 6F6 - char rules[MAX_RULE_SIZE]; // 700 uses 512 bytes in v5.12.0l + char rules[MAX_RULE_SIZE]; // 800 uses 512 bytes in v5.12.0m - // 900 - FFF free locations + // A00 - FFF free locations } Settings; struct RTCMEM { diff --git a/sonoff/settings.ino b/sonoff/settings.ino index c9b41d946..9419548c8 100644 --- a/sonoff/settings.ino +++ b/sonoff/settings.ino @@ -920,21 +920,25 @@ void SettingsDelta() Settings.sbaudrate = SOFT_BAUDRATE / 1200; Settings.serial_delimiter = 0xff; } - if (Settings.version < 0x050C0009) { - memset(&Settings.timer, 0x00, sizeof(Timer) * MAX_TIMERS); - } +// if (Settings.version < 0x050C0009) { +// memset(&Settings.timer, 0x00, sizeof(Timer) * MAX_TIMERS); +// } if (Settings.version < 0x050C000A) { Settings.latitude = (int)((double)LATITUDE * 1000000); Settings.longitude = (int)((double)LONGITUDE * 1000000); } if (Settings.version < 0x050C000B) { - memset(&Settings.free_6b8, 0x00, sizeof(Settings.free_6b8)); memset(&Settings.rules, 0x00, sizeof(Settings.rules)); } + if (Settings.version < 0x050C000D) { + memmove(Settings.rules, Settings.rules -256, sizeof(Settings.rules)); // move rules up by 256 bytes + memset(&Settings.timer, 0x00, sizeof(Timer) * MAX_TIMERS); // Reset timers as layout has changed from v5.12.0i + Settings.knx_GA_registered = 0; + Settings.knx_CB_registered = 0; + memset(&Settings.knx_physsical_addr, 0x00, 0x800 - 0x6b8); // Reset until 0x800 for future use + } Settings.version = VERSION; SettingsSave(1); } } - - diff --git a/sonoff/sonoff.ino b/sonoff/sonoff.ino index 2e20c6ae7..f937cc517 100644 --- a/sonoff/sonoff.ino +++ b/sonoff/sonoff.ino @@ -25,7 +25,7 @@ - Select IDE Tools - Flash Size: "1M (no SPIFFS)" ====================================================*/ -#define VERSION 0x050C000C // 5.12.0l +#define VERSION 0x050C000D // 5.12.0m // Location specific includes #include // Arduino_Esp8266 version information (ARDUINO_ESP8266_RELEASE and ARDUINO_ESP8266_RELEASE_2_3_0) diff --git a/sonoff/xdrv_09_timers.ino b/sonoff/xdrv_09_timers.ino index f8efae266..4b216b676 100644 --- a/sonoff/xdrv_09_timers.ino +++ b/sonoff/xdrv_09_timers.ino @@ -27,6 +27,7 @@ * 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 @@ -46,6 +47,7 @@ const char kTimerCommands[] PROGMEM = D_CMND_TIMER "|" D_CMND_TIMERS ; uint16_t timer_last_minute = 60; +int8_t timer_window[MAX_TIMERS] = { 0 }; #ifdef USE_SUNRISE /*********************************************************************************************\ @@ -242,12 +244,26 @@ uint16_t GetSunMinutes(byte dawn) /*******************************************************************************************/ +void TimerSetRandomWindow(byte 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() +{ + for (byte i = 0; i < MAX_TIMERS; i++) { TimerSetRandomWindow(i); } +} + void TimerEverySecond() { if (RtcTime.valid) { + if (!RtcTime.hour && !RtcTime.minute && !RtcTime.second) { TimerSetRandomWindows(); } // Midnight if (RtcTime.minute != timer_last_minute) { // Execute every minute only once timer_last_minute = RtcTime.minute; - uint16_t time = (RtcTime.hour *60) + RtcTime.minute; + int16_t time = (RtcTime.hour *60) + RtcTime.minute; uint8_t days = 1 << (RtcTime.day_of_week -1); for (byte i = 0; i < MAX_TIMERS; i++) { @@ -261,6 +277,9 @@ void TimerEverySecond() } #endif if (xtimer.arm) { + set_time += timer_window[i]; // Add random time offset + if (set_time < 0) { set_time == 0; } // Stay today; + if (set_time > 1439) { set_time == 1439; } if (time == set_time) { if (xtimer.days & days) { Settings.timer[i].arm = xtimer.repeat; @@ -294,11 +313,11 @@ void PrepShowTimer(uint8_t index) 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, xtimer.arm, xtimer.mode, hour, xtimer.time % 60, days, xtimer.repeat, xtimer.device +1, xtimer.power); + 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_WINDOW "\":%d,\"" D_JSON_TIMER_DAYS "\":\"%s\",\"" D_JSON_TIMER_REPEAT "\":%d,\"" D_JSON_TIMER_OUTPUT "\":%d,\"" D_JSON_TIMER_ACTION "\":%d}"), + mqtt_data, index, xtimer.arm, xtimer.mode, hour, xtimer.time % 60, xtimer.window, 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, xtimer.arm, xtimer.time / 60, xtimer.time % 60, days, xtimer.repeat, xtimer.device +1, xtimer.power); + 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_WINDOW "\":%d,\"" D_JSON_TIMER_DAYS "\":\"%s\",\"" D_JSON_TIMER_REPEAT "\":%d,\"" D_JSON_TIMER_OUTPUT "\":%d,\"" D_JSON_TIMER_ACTION "\":%d}"), + mqtt_data, index, xtimer.arm, xtimer.time / 60, xtimer.time % 60, xtimer.window, days, xtimer.repeat, xtimer.device +1, xtimer.power); #endif // USE_SUNRISE } @@ -365,6 +384,10 @@ boolean TimerCommand() } 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; @@ -413,9 +436,9 @@ boolean TimerCommand() } else { snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s,"), mqtt_data); } - jsflg = 1; + jsflg++; PrepShowTimer(i +1); - if ((strlen(mqtt_data) > (LOGSZ - TOPSZ - 20)) || (i == MAX_TIMERS -1)) { + if (jsflg > 3) { snprintf_P(mqtt_data, sizeof(mqtt_data), PSTR("%s}}"), mqtt_data); MqttPublishPrefixTopic_P(RESULT_OR_STAT, PSTR(D_CMND_TIMERS)); jsflg = 0; @@ -463,53 +486,57 @@ const char HTTP_TIMER_SCRIPT[] PROGMEM = "q.appendChild(o);" "}" #ifdef USE_SUNRISE - "function gt(){" // Set hours and minutas according to mode + "function gt(){" // Set hours and minutes according to mode "var m,p,q;" "m=qs('input[name=\"rd\"]:checked').value;" // Get mode - "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 + "p=pt[ct]&0x7FF;" // Get time + "if(m==0){" // Time is set + "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 + "so(0);" // Schedule time, hide offset span + "}" "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 + "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 "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;';}" + "o=qs('#ho');" + "e=o.childElementCount;" + "if(b==1){" + "qs('#dr').disabled='';" + "if(e>12){for(i=12;i<=23;i++){o.removeChild(o.lastElementChild);}}" // Create offset hours select options + "}else{" + "qs('#dr').disabled='disabled';" + "if(e<23){for(i=12;i<=23;i++){ce(i,o);}}" // Create hours select options + "}" "}" #endif "function st(){" // Save parameters to hidden area "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 + "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<<11);" // Get mode + "s|=(qs('input[name=\"rd\"]:checked').value<<29);" // 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 - - "if(m==0){s|=((qs('#ho').selectedIndex*60)+qs('#mi').selectedIndex)&0x7FF;}" // Get time + "l=((qs('#ho').selectedIndex*60)+qs('#mi').selectedIndex)&0x7FF;" + "if(m==0){s|=l;}" // 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 + "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 "}" @@ -522,33 +549,31 @@ const char HTTP_TIMER_SCRIPT[] PROGMEM = "e.style.cssText=\"background-color:#fff;color:#000;font-weight:bold;\";" // Change style to tab/button used to open content "s=pt[ct];" // Get parameters from array #ifdef USE_SUNRISE - "p=(s>>11)&3;eb('b'+p).checked=1;" // Set mode + "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 "p=(s>>23)&0xF;qs('#d1').value=p+1;" // Set device "p=(s>>27)&3;eb('p1').value=p;" // Set power - "p=(s>>29)&1;eb('r0').checked=p;" // Set repeat - "p=(s>>30)&1;eb('a0').checked=p;" // Set arm + "p=(s>>15)&1;eb('r0').checked=p;" // Set repeat + "p=(s>>31)&1;eb('a0').checked=p;" // Set arm "}" "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<" STR(MAX_TIMERS) ";i++){b='';if(0==i){b=\" id='dP'\";}s+=\"\"}" "eb('bt').innerHTML=s;" // Create tabs +#ifdef USE_SUNRISE // NEW: Create offset options (+/- up to 11h, 59m) + "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 - -#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('#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<}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)+\"\"}" @@ -558,7 +583,7 @@ const char HTTP_TIMER_SCRIPT[] PROGMEM = const char HTTP_TIMER_STYLE[] PROGMEM = ".tl{float:left;border-radius:0;border:1px solid #fff;padding:1px;width:6.25%;}" #ifdef USE_SUNRISE - "input[type='radio']{width:13px;height:34px;margin-top:-1px;margin-right:8px;vertical-align:middle;}" + "input[type='radio']{width:13px;height:24px;margin-top:-1px;margin-right:8px;vertical-align:middle;}" #endif ""; const char HTTP_FORM_TIMER[] PROGMEM = @@ -586,27 +611,20 @@ const char HTTP_FORM_TIMER1[] PROGMEM = "
" #ifdef USE_SUNRISE "
" - "" D_TIMER_TIME " " - "" - " " D_HOUR_MINUTE_SEPARATOR " " - "
" - "" D_SUNRISE "
" - "" D_SUNSET "
" - "
" + "" D_TIMER_TIME "
" + "" D_SUNRISE " (}8)
" + "" D_SUNSET " (}9)
" "
" + "" + " " #else "" D_TIMER_TIME " " +#endif // USE_SUNRISE "" " " D_HOUR_MINUTE_SEPARATOR " " "" -#endif // USE_SUNRISE + " +/- " + "" "

" "
"; const char HTTP_FORM_TIMER2[] PROGMEM = @@ -632,14 +650,12 @@ void HandleTimerConfiguration() if (i > 0) { page += F(","); } page += String(Settings.timer[i].data); } -#ifdef USE_SUNRISE - page += F(","); page += String(GetSunMinutes(0)); // Add Sunrise - page += F(","); page += String(GetSunMinutes(1)); // Add Sunset -#endif // USE_SUNRISE page += FPSTR(HTTP_FORM_TIMER1); page.replace(F("}1"), String(devices_present)); #ifdef USE_SUNRISE - page.replace(F("299"), String(180 + (strlen(D_TIMER_TIME) *10))); // Fix string length to keep radios centered + page.replace(F("}8"), GetSun(0)); // Add Sunrise + page.replace(F("}9"), GetSun(1)); // Add Sunset + page.replace(F("299"), String(100 + (strlen(D_SUNSET) *12))); // Fix string length to keep radios centered #endif // USE_SUNRISE page += FPSTR(HTTP_FORM_END); page.replace(F("type='submit'"), FPSTR(HTTP_FORM_TIMER2)); @@ -659,7 +675,11 @@ void TimerSaveSettings() for (byte i = 0; i < MAX_TIMERS; i++) { timer.data = strtol(p, &p, 10); p++; // Skip comma - if (timer.time < 1440) { Settings.timer[i].data = timer.data; } + 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%s0x%08X"), log_data, (i > 0)?",":"", Settings.timer[i].data); } AddLog(LOG_LEVEL_DEBUG); @@ -678,6 +698,9 @@ boolean Xdrv09(byte function) boolean result = false; switch (function) { + case FUNC_INIT: + TimerSetRandomWindows(); + break; case FUNC_EVERY_SECOND: TimerEverySecond(); break; diff --git a/sonoff/xdrv_11_knx.ino b/sonoff/xdrv_11_knx.ino new file mode 100644 index 000000000..734c22c54 --- /dev/null +++ b/sonoff/xdrv_11_knx.ino @@ -0,0 +1,885 @@ +/* + xdrv_11_knx.ino - KNX IP Protocol support for Sonoff-Tasmota + + Copyright (C) 2018 Adrian Scillato (https://github.com/ascillato) + + 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_KNX +/*********************************************************************************************\ + * KNX support + * + * Using libraries: + * ESP KNX IP library (async-udp branch) (https://github.com/envy/esp-knx-ip/tree/async-udp) + * ESPAsyncUDP library (https://github.com/me-no-dev/ESPAsyncUDP) + +Constants in sonoff.h +----------------------- + +#define MAX_KNX_GA 10 Max number of KNX Group Addresses to read that can be set +#define MAX_KNX_CB 10 Max number of KNX Group Addresses to write that can be set + If you change MAX_KNX_CB you also have to change on the esp-knx-ip.h file the following: + #define MAX_CALLBACK_ASSIGNMENTS 10 + #define MAX_CALLBACKS 10 + Both to MAX_KNX_CB + +Variables in settings.h +----------------------- + +bool Settings.flag.knx_enabled Enable/Disable KNX Protocol +uint16_t Settings.knx_physsical_addr Physical KNX address of this device +byte Settings.knx_GA_registered Number of group address to read +byte Settings.knx_CB_registered Number of group address to write +uint16_t Settings.knx_GA_addr[MAX_KNX_GA] Group address to read +uint16_t Settings.knx_CB_addr[MAX_KNX_CB] Group address to write +byte Settings.knx_GA_param[MAX_KNX_GA] Type of Input (relay changed, button pressed, sensor read) +byte Settings.knx_CB_param[MAX_KNX_CB] Type of Output (set relay, toggle relay, reply sensor value) + +\*********************************************************************************************/ + +#include + +//void KNX_CB_Action(message_t const &msg, void *arg); // Define function (action callback) to be called by the KNX_IP Library + // when an action is requested by another KNX Device + +address_t KNX_physs_addr; // Physical KNX address of this device +address_t KNX_addr; // KNX Address converter variable + +#define KNX_Empty 255 +#define KNX_TEMPERATURE 17 +#define KNX_HUMIDITY 18 +#define KNX_MAX_device_param 18 + +float last_temp; +float last_hum; + +typedef struct __device_parameters +{ + byte type; // PARAMETER_ID. Used as type of GA = relay, button, sensor, etc, (INPUTS) + // used when an action on device triggers a MSG to send on KNX + // Needed because this is the value that the ESP_KNX_IP library will pass as parameter + // to identify the action to perform when a MSG is received + + bool show; // HARDWARE related. to identify if the parameter exists on the device. + + bool last_state; // LAST_STATE of relays + + callback_id_t CB_id; // ACTION_ID. To store the ID value of Registered_CB to the library. + // The ESP_KNX_IP requires to register the callbacks, and then, to assign an address to the registered callback + // So CB_id is needed to store the ID of the callback to then, assign multiple addresses to the same ID (callback) + // It is used as type of CB = set relay, toggle relay, reply sensor, etc, (OUTPUTS) + // used when a MSG receive KNX triggers an action on the device + // - Multiples address to the same callback (i.e. Set Relay 1 Status) are used on scenes for example +} device_parameters_t; + +// device parameters (information that can be sent) +device_parameters_t device_param[] = { + { 1, false, false, KNX_Empty }, // device_param[ 0] = Relay 1 + { 2, false, false, KNX_Empty }, // device_param[ 1] = Relay 2 + { 3, false, false, KNX_Empty }, // device_param[ 2] = Relay 3 + { 4, false, false, KNX_Empty }, // device_param[ 3] = Relay 4 + { 5, false, false, KNX_Empty }, // device_param[ 4] = Relay 5 + { 6, false, false, KNX_Empty }, // device_param[ 5] = Relay 6 + { 7, false, false, KNX_Empty }, // device_param[ 6] = Relay 7 + { 8, false, false, KNX_Empty }, // device_param[ 7] = Relay 8 + { 9, false, false, KNX_Empty }, // device_param[ 8] = Button 1 + { 10, false, false, KNX_Empty }, // device_param[ 9] = Button 2 + { 11, false, false, KNX_Empty }, // device_param[10] = Button 3 + { 12, false, false, KNX_Empty }, // device_param[11] = Button 4 + { 13, false, false, KNX_Empty }, // device_param[12] = Button 5 + { 14, false, false, KNX_Empty }, // device_param[13] = Button 6 + { 15, false, false, KNX_Empty }, // device_param[14] = Button 7 + { 16, false, false, KNX_Empty }, // device_param[15] = Button 8 + { KNX_TEMPERATURE, false, false, KNX_Empty }, // device_param[16] = Temperature + { KNX_HUMIDITY , false, false, KNX_Empty }, // device_param[17] = humidity + { KNX_Empty, false, false, KNX_Empty} +}; + +// device parameters (information that can be sent) +const char * device_param_ga[] = { + D_SENSOR_RELAY " 1", // Relay 1 + D_SENSOR_RELAY " 2", // Relay 2 + D_SENSOR_RELAY " 3", // Relay 3 + D_SENSOR_RELAY " 4", // Relay 4 + D_SENSOR_RELAY " 5", // Relay 5 + D_SENSOR_RELAY " 6", // Relay 6 + D_SENSOR_RELAY " 7", // Relay 7 + D_SENSOR_RELAY " 8", // Relay 8 + D_SENSOR_BUTTON " 1", // Button 1 + D_SENSOR_BUTTON " 2", // Button 2 + D_SENSOR_BUTTON " 3", // Button 3 + D_SENSOR_BUTTON " 4", // Button 4 + D_SENSOR_BUTTON " 5", // Button 5 + D_SENSOR_BUTTON " 6", // Button 6 + D_SENSOR_BUTTON " 7", // Button 7 + D_SENSOR_BUTTON " 8", // Button 8 + D_TEMPERATURE , // Temperature + D_HUMIDITY , // Humidity + nullptr +}; + +// device actions (posible actions to be performed on the device) +const char *device_param_cb[] = { + D_SENSOR_RELAY " 1", // Set Relay 1 (1-On or 0-OFF) + D_SENSOR_RELAY " 2", + D_SENSOR_RELAY " 3", + D_SENSOR_RELAY " 4", + D_SENSOR_RELAY " 5", + D_SENSOR_RELAY " 6", + D_SENSOR_RELAY " 7", + D_SENSOR_RELAY " 8", + D_SENSOR_RELAY " 1 " D_BUTTON_TOGGLE, // Relay 1 Toggle (1 or 0 will toggle) + D_SENSOR_RELAY " 2 " D_BUTTON_TOGGLE, + D_SENSOR_RELAY " 3 " D_BUTTON_TOGGLE, + D_SENSOR_RELAY " 4 " D_BUTTON_TOGGLE, + D_SENSOR_RELAY " 5 " D_BUTTON_TOGGLE, + D_SENSOR_RELAY " 6 " D_BUTTON_TOGGLE, + D_SENSOR_RELAY " 7 " D_BUTTON_TOGGLE, + D_SENSOR_RELAY " 8 " D_BUTTON_TOGGLE, + D_REPLY " " D_TEMPERATURE, // Reply Temperature + D_REPLY " " D_HUMIDITY, // Reply Humidity + nullptr +}; + + +byte KNX_GA_Search( byte param, byte start = 0 ) +{ + for (byte i = start; i < Settings.knx_GA_registered; ++i) + { + if ( Settings.knx_GA_param[i] == param ) + { + if ( Settings.knx_GA_addr[i] != 0 ) // Relay has group address set? GA=0/0/0 can not be used as KNX address, so it is used here as a: not set value + { + if ( i >= start ) { return i; } + } + } + } + return KNX_Empty; +} + + +byte KNX_CB_Search( byte param, byte start = 0 ) +{ + for (byte i = start; i < Settings.knx_CB_registered; ++i) + { + if ( Settings.knx_CB_param[i] == param ) + { + if ( Settings.knx_CB_addr[i] != 0 ) + { + if ( i >= start ) { return i; } + } + } + } + return KNX_Empty; +} + + +void KNX_ADD_GA( byte GAop, byte GA_FNUM, byte GA_AREA, byte GA_FDEF ) +{ + // Check if all GA were assigned. If yes-> return + if ( Settings.knx_GA_registered >= MAX_KNX_GA ) { return; } + if ( GA_FNUM == 0 && GA_AREA == 0 && GA_FDEF == 0 ) { return; } + + // Assign a GA to that address + Settings.knx_GA_param[Settings.knx_GA_registered] = GAop; + KNX_addr.ga.area = GA_FNUM; + KNX_addr.ga.line = GA_AREA; + KNX_addr.ga.member = GA_FDEF; + Settings.knx_GA_addr[Settings.knx_GA_registered] = KNX_addr.value; + + Settings.knx_GA_registered++; + + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX D_ADD " GA #%d: %s " D_TO " %d/%d/%d"), + Settings.knx_GA_registered, + device_param_ga[GAop-1], + GA_FNUM, GA_AREA, GA_FDEF ); + AddLog(LOG_LEVEL_DEBUG); +} + + +void KNX_DEL_GA( byte GAnum ) +{ + + byte dest_offset = 0; + byte src_offset = 0; + byte len = 0; + + // Delete GA + Settings.knx_GA_param[GAnum-1] = 0; + + if (GAnum == 1) + { + // start of array, so delete first entry + src_offset = 1; + // Settings.knx_GA_registered will be 1 in case of only one entry + // Settings.knx_GA_registered will be 2 in case of two entries, etc.. + // so only copy anything, if there is it at least more then one element + len = (Settings.knx_GA_registered - 1); + } + else if (GAnum == Settings.knx_GA_registered) + { + // last element, don't do anything, simply decrement counter + } + else + { + // somewhere in the middle + // need to calc offsets + + // skip all prev elements + dest_offset = GAnum -1 ; // GAnum -1 is equal to how many element are in front of it + src_offset = dest_offset + 1; // start after the current element + len = (Settings.knx_GA_registered - GAnum); + } + + if (len > 0) + { + memmove(Settings.knx_GA_param + dest_offset, Settings.knx_GA_param + src_offset, len * sizeof(byte)); + memmove(Settings.knx_GA_addr + dest_offset, Settings.knx_GA_addr + src_offset, len * sizeof(uint16_t)); + } + + Settings.knx_GA_registered--; + + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX D_DELETE " GA #%d"), + GAnum ); + AddLog(LOG_LEVEL_DEBUG); +} + + +void KNX_ADD_CB( byte CBop, byte CB_FNUM, byte CB_AREA, byte CB_FDEF ) +{ + // Check if all callbacks were assigned. If yes-> return + if ( Settings.knx_CB_registered >= MAX_KNX_CB ) { return; } + if ( CB_FNUM == 0 && CB_AREA == 0 && CB_FDEF == 0 ) { return; } + + // Check if a CB for CBop was registered on the ESP-KNX-IP Library + if ( device_param[CBop-1].CB_id == KNX_Empty ) + { + // if no, register the CB for CBop + device_param[CBop-1].CB_id = knx.callback_register("", KNX_CB_Action, &device_param[CBop-1]); + // KNX IP Library requires a parameter + // to identify which action was requested on the KNX network + // to be performed on this device (set relay, etc.) + // Is going to be used device_param[j].type that stores the type number (1: relay 1, etc) + } + // Assign a callback to CB address + Settings.knx_CB_param[Settings.knx_CB_registered] = CBop; + KNX_addr.ga.area = CB_FNUM; + KNX_addr.ga.line = CB_AREA; + KNX_addr.ga.member = CB_FDEF; + Settings.knx_CB_addr[Settings.knx_CB_registered] = KNX_addr.value; + + knx.callback_assign( device_param[CBop-1].CB_id, KNX_addr ); + + Settings.knx_CB_registered++; + + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX D_ADD " CB #%d: %d/%d/%d " D_TO " %s"), + Settings.knx_CB_registered, + CB_FNUM, CB_AREA, CB_FDEF, + device_param_cb[CBop-1] ); + AddLog(LOG_LEVEL_DEBUG); +} + + +void KNX_DEL_CB( byte CBnum ) +{ + byte oldparam = Settings.knx_CB_param[CBnum-1]; + byte dest_offset = 0; + byte src_offset = 0; + byte len = 0; + + // Delete assigment + knx.callback_unassign(CBnum-1); + Settings.knx_CB_param[CBnum-1] = 0; + + if (CBnum == 1) + { + // start of array, so delete first entry + src_offset = 1; + // Settings.knx_CB_registered will be 1 in case of only one entry + // Settings.knx_CB_registered will be 2 in case of two entries, etc.. + // so only copy anything, if there is it at least more then one element + len = (Settings.knx_CB_registered - 1); + } + else if (CBnum == Settings.knx_CB_registered) + { + // last element, don't do anything, simply decrement counter + } + else + { + // somewhere in the middle + // need to calc offsets + + // skip all prev elements + dest_offset = CBnum -1 ; // GAnum -1 is equal to how many element are in front of it + src_offset = dest_offset + 1; // start after the current element + len = (Settings.knx_CB_registered - CBnum); + } + + if (len > 0) + { + memmove(Settings.knx_CB_param + dest_offset, Settings.knx_CB_param + src_offset, len * sizeof(byte)); + memmove(Settings.knx_CB_addr + dest_offset, Settings.knx_CB_addr + src_offset, len * sizeof(uint16_t)); + } + + Settings.knx_CB_registered--; + + // Check if there is no other assigment to that callback. If there is not. delete that callback register + if ( KNX_CB_Search( oldparam ) == KNX_Empty ) { + knx.callback_deregister( device_param[oldparam-1].CB_id ); + device_param[oldparam-1].CB_id = KNX_Empty; + } + + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX D_DELETE " CB #%d"), CBnum ); + AddLog(LOG_LEVEL_DEBUG); +} + + +bool KNX_CONFIG_NOT_MATCH() +{ + for (int i = 0; i < KNX_MAX_device_param; ++i) + { + if ( !device_param[i].show ) { // device has this parameter ? + // if not, search for all registered group address to this parameter for deletion + if ( KNX_GA_Search(i+1) != KNX_Empty ) { return true; } + if ( (i < 8) || (i > 15) ) // check relays and sensors (i from 8 to 16 are toggle relays parameters) + { + if ( KNX_CB_Search(i+1) != KNX_Empty ) { return true; } + if ( KNX_CB_Search(i+8) != KNX_Empty ) { return true; } + } + } + } + return false; +} + + +void KNXStart() +{ + knx.start(nullptr); + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX D_START)); + AddLog(LOG_LEVEL_DEBUG); +} + + +void KNX_INIT() +{ + // Check for incompatible config + if (Settings.knx_GA_registered > MAX_KNX_GA) { Settings.knx_GA_registered = MAX_KNX_GA; } + if (Settings.knx_CB_registered > MAX_KNX_CB) { Settings.knx_CB_registered = MAX_KNX_CB; } + + // Set Physical KNX Address of the device + KNX_physs_addr.value = Settings.knx_physsical_addr; + knx.physical_address_set( KNX_physs_addr ); + + // Read Configuration + // Check which relays, buttons and sensors where configured for this device + // and activate options according to the hardware + for (int i = GPIO_REL1; i < GPIO_REL8 + 1; ++i) + { + if (GetUsedInModule(i, my_module.gp.io)) { device_param[i - GPIO_REL1].show = true; } + } + for (int i = GPIO_REL1_INV; i < GPIO_REL8_INV + 1; ++i) + { + if (GetUsedInModule(i, my_module.gp.io)) { device_param[i - GPIO_REL1_INV].show = true; } + } + for (int i = GPIO_SWT1; i < GPIO_SWT4 + 1; ++i) + { + if (GetUsedInModule(i, my_module.gp.io)) { device_param[i - GPIO_SWT1 + 8].show = true; } + } + for (int i = GPIO_KEY1; i < GPIO_KEY4 + 1; ++i) + { + if (GetUsedInModule(i, my_module.gp.io)) { device_param[i - GPIO_KEY1 + 8].show = true; } + } + if (GetUsedInModule(GPIO_DHT11, my_module.gp.io)) { device_param[KNX_TEMPERATURE-1].show = true; } + if (GetUsedInModule(GPIO_DHT22, my_module.gp.io)) { device_param[KNX_TEMPERATURE-1].show = true; } + if (GetUsedInModule(GPIO_SI7021, my_module.gp.io)) { device_param[KNX_TEMPERATURE-1].show = true; } + if (GetUsedInModule(GPIO_DHT11, my_module.gp.io)) { device_param[KNX_HUMIDITY-1].show = true; } + if (GetUsedInModule(GPIO_DHT22, my_module.gp.io)) { device_param[KNX_HUMIDITY-1].show = true; } + if (GetUsedInModule(GPIO_SI7021, my_module.gp.io)) { device_param[KNX_HUMIDITY-1].show = true; } + + // Delete from KNX settings all configuration is not anymore related to this device + if (KNX_CONFIG_NOT_MATCH()) { + Settings.knx_GA_registered = 0; + Settings.knx_CB_registered = 0; + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX D_DELETE " " D_KNX_PARAMETERS )); + AddLog(LOG_LEVEL_DEBUG); + } + + // Register Group Addresses to listen to + // Search on the settings if there is a group address set for receive KNX messages for the type: device_param[j].type + // If there is, register the group address on the KNX_IP Library to Receive data for Executing Callbacks + byte j; + for (byte i = 0; i < Settings.knx_CB_registered; ++i) + { + j = Settings.knx_CB_param[i]; + if ( j > 0 ) + { + device_param[j-1].CB_id = knx.callback_register("", KNX_CB_Action, &device_param[j-1]); // KNX IP Library requires a parameter + // to identify which action was requested on the KNX network + // to be performed on this device (set relay, etc.) + // Is going to be used device_param[j].type that stores the type number (1: relay 1, etc) + KNX_addr.value = Settings.knx_CB_addr[i]; + knx.callback_assign( device_param[j-1].CB_id, KNX_addr ); + } + } +} + + +void KNX_CB_Action(message_t const &msg, void *arg) +{ + device_parameters_t *chan = (device_parameters_t *)arg; + + if (!(Settings.flag.knx_enabled)) { return; } + + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX D_RECEIVED_FROM " %d.%d.%d " D_COMMAND " %s: %d " D_TO " %s"), + msg.received_on.ga.area, msg.received_on.ga.line, msg.received_on.ga.member, + (msg.ct == KNX_CT_WRITE) ? D_KNX_COMMAND_WRITE : (msg.ct == KNX_CT_READ) ? D_KNX_COMMAND_READ : D_KNX_COMMAND_OTHER, + msg.data[0], + device_param_cb[(chan->type)-1]); + AddLog(LOG_LEVEL_INFO); + + switch (msg.ct) + { + case KNX_CT_WRITE: + if (chan->type < 9) // Set Relays + { + ExecuteCommandPower(chan->type, msg.data[0]); + } + else if (chan->type < 17) // Toggle Relays + { + ExecuteCommandPower((chan->type) -8, 2); + } + break; + case KNX_CT_READ: + if (chan->type < 9) // reply Relays status + { + knx.answer_1bit(msg.received_on, chan->last_state); + } + else if (chan->type = KNX_TEMPERATURE) // Reply Temperature + { + knx.answer_2byte_float(msg.received_on, last_temp); + } + else if (chan->type = KNX_HUMIDITY) // Reply Humidity + { + knx.answer_2byte_float(msg.received_on, last_hum); + } + break; + } +} + + +void KnxUpdatePowerState(byte device, power_t state) +{ + if (!(Settings.flag.knx_enabled)) { return; } + + device_param[device -1].last_state = bitRead(state, device -1); // power state (on/off) + + // Search all the registered GA that has that output (variable: device) as parameter + byte i = KNX_GA_Search(device); + while ( i != KNX_Empty ) { + KNX_addr.value = Settings.knx_GA_addr[i]; + knx.write_1bit(KNX_addr, device_param[device -1].last_state); + + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX "%s = %d " D_SENT_TO " %d.%d.%d"), + device_param_ga[device -1], device_param[device -1].last_state, + KNX_addr.ga.area, KNX_addr.ga.line, KNX_addr.ga.member); + AddLog(LOG_LEVEL_INFO); + + i = KNX_GA_Search(device, i + 1); + } +} + + +void KnxSendButtonPower(byte key, byte device, byte state) +{ +// key 0 = button_topic +// key 1 = switch_topic +// state 0 = off +// state 1 = on +// state 2 = toggle +// state 3 = hold +// state 9 = clear retain flag + if (!(Settings.flag.knx_enabled)) { return; } +// if (key) +// { + +// Search all the registered GA that has that output (variable: device) as parameter + byte i = KNX_GA_Search(device + 8); + while ( i != KNX_Empty ) { + KNX_addr.value = Settings.knx_GA_addr[i]; + knx.write_1bit(KNX_addr, !(state == 0)); + + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX "%s = %d " D_SENT_TO " %d.%d.%d"), + device_param_ga[device + 8], !(state == 0), + KNX_addr.ga.area, KNX_addr.ga.line, KNX_addr.ga.member); + AddLog(LOG_LEVEL_INFO); + + i = KNX_GA_Search(device + 8, i + 1); + } +// } +} + + +void KnxSensor(byte sensor_type, float value) +{ + if (sensor_type == KNX_TEMPERATURE) + { + last_temp = value; + } else if (sensor_type == KNX_HUMIDITY) + { + last_hum = value; + } + + if (!(Settings.flag.knx_enabled)) { return; } + + byte i = KNX_GA_Search(sensor_type); + while ( i != KNX_Empty ) { + KNX_addr.value = Settings.knx_GA_addr[i]; + knx.write_2byte_float(KNX_addr, value); + + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX "%s " D_SENT_TO " %d.%d.%d "), + device_param_ga[sensor_type], + KNX_addr.ga.area, KNX_addr.ga.line, KNX_addr.ga.member); + AddLog(LOG_LEVEL_INFO); + + i = KNX_GA_Search(sensor_type, i+1); + } +} + + +/*********************************************************************************************\ + * Presentation +\*********************************************************************************************/ + +#ifdef USE_WEBSERVER +const char S_CONFIGURE_KNX[] PROGMEM = D_CONFIGURE_KNX; + +const char HTTP_FORM_KNX[] PROGMEM = + "
 " D_KNX_PARAMETERS " 
" + "
" + "" D_KNX_PHYSICAL_ADDRESS " " + " . " + " . " + "" + "

" D_KNX_PHYSICAL_ADDRESS_NOTE "

" + "" D_KNX_ENABLE "

" + + "
" + "" D_KNX_GROUP_ADDRESS_TO_WRITE "
" + + " / " + " / " + " "; + +const char HTTP_FORM_KNX_ADD_BTN[] PROGMEM = + "

" + ""; + +const char HTTP_FORM_KNX_ADD_TABLE_ROW[] PROGMEM = + "" + ""; + +const char HTTP_FORM_KNX3[] PROGMEM = + "
{optex} -> GAfnum / GAarea / GAfdef

" + "
" + "" D_KNX_GROUP_ADDRESS_TO_READ "
"; + +const char HTTP_FORM_KNX4[] PROGMEM = + "-> -> "); + page += FPSTR(HTTP_FORM_KNX_GA); + page.replace(F("GAfnum"), F("GA_FNUM")); + page.replace(F("GAarea"), F("GA_AREA")); + page.replace(F("GAfdef"), F("GA_FDEF")); + page.replace(F("GAfnum"), F("GA_FNUM")); + page.replace(F("GAarea"), F("GA_AREA")); + page.replace(F("GAfdef"), F("GA_FDEF")); + page += FPSTR(HTTP_FORM_KNX_ADD_BTN); + page.replace(F("{btnval}"), String(1)); + if (Settings.knx_GA_registered < MAX_KNX_GA) { + page.replace(F("btndis"), F(" ")); + } + else + { + page.replace(F("btndis"), F("disabled")); + } + page.replace(F("fncbtnadd"), F("GAwarning")); + for (byte i = 0; i < Settings.knx_GA_registered ; ++i) + { + if ( Settings.knx_GA_param[i] ) + { + page += FPSTR(HTTP_FORM_KNX_ADD_TABLE_ROW); + page.replace(F("{opval}"), String(i+1)); + page.replace(F("{optex}"), String(device_param_ga[Settings.knx_GA_param[i]-1])); + KNX_addr.value = Settings.knx_GA_addr[i]; + page.replace(F("GAfnum"), String(KNX_addr.ga.area)); + page.replace(F("GAarea"), String(KNX_addr.ga.line)); + page.replace(F("GAfdef"), String(KNX_addr.ga.member)); + } + } + page += FPSTR(HTTP_FORM_KNX3); + page += FPSTR(HTTP_FORM_KNX_GA); + page.replace(F("GAfnum"), F("CB_FNUM")); + page.replace(F("GAarea"), F("CB_AREA")); + page.replace(F("GAfdef"), F("CB_FDEF")); + page.replace(F("GAfnum"), F("CB_FNUM")); + page.replace(F("GAarea"), F("CB_AREA")); + page.replace(F("GAfdef"), F("CB_FDEF")); + page += FPSTR(HTTP_FORM_KNX4); + for (byte i = 0; i < KNX_MAX_device_param ; i++) + { + if ( device_param[i].show ) + { + page += FPSTR(HTTP_FORM_KNX_OPT); + page.replace(F("{vop}"), String(device_param[i].type)); + page.replace(F("{nop}"), String(device_param_cb[i])); + } + } + page += F(" "); + page += FPSTR(HTTP_FORM_KNX_ADD_BTN); + page.replace(F("{btnval}"), String(2)); + if (Settings.knx_CB_registered < MAX_KNX_CB) { + page.replace(F("btndis"), F(" ")); + } + else + { + page.replace(F("btndis"), F("disabled")); + } + page.replace(F("fncbtnadd"), F("CBwarning")); + for (byte i = 0; i < Settings.knx_CB_registered ; ++i) + { + if ( Settings.knx_CB_param[i] ) + { + page += FPSTR(HTTP_FORM_KNX_ADD_TABLE_ROW2); + page.replace(F("{opval}"), String(i+1)); + page.replace(F("{optex}"), String(device_param_cb[Settings.knx_CB_param[i]-1])); + KNX_addr.value = Settings.knx_CB_addr[i]; + page.replace(F("GAfnum"), String(KNX_addr.ga.area)); + page.replace(F("GAarea"), String(KNX_addr.ga.line)); + page.replace(F("GAfdef"), String(KNX_addr.ga.member)); + } + } + page += F("
"); + page += F("
"); + page += FPSTR(HTTP_BTN_CONF); + + page.replace( F(""), + F("function GAwarning()" + "{" + "var GA_FNUM = document.getElementById('GA_FNUM');" + "var GA_AREA = document.getElementById('GA_AREA');" + "var GA_FDEF = document.getElementById('GA_FDEF');" + "if ( GA_FNUM != null && GA_FNUM.value == '0' && GA_AREA.value == '0' && GA_FDEF.value == '0' ) {" + "alert('" D_KNX_WARNING "');" + "}" + "}" + "function CBwarning()" + "{" + "var CB_FNUM = document.getElementById('CB_FNUM');" + "var CB_AREA = document.getElementById('CB_AREA');" + "var CB_FDEF = document.getElementById('CB_FDEF');" + "if ( CB_FNUM != null && CB_FNUM.value == '0' && CB_AREA.value == '0' && CB_FDEF.value == '0' ) {" + "alert('" D_KNX_WARNING "');" + "}" + "}" + "") ); + ShowPage(page); + } + +} + + +void KNX_Save_Settings() +{ + String stmp; + address_t KNX_addr; + byte i; + + Settings.flag.knx_enabled = WebServer->hasArg("b1"); + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX D_ENABLED ": %d "), + Settings.flag.knx_enabled); + AddLog(LOG_LEVEL_DEBUG); + + stmp = WebServer->arg("area"); + KNX_addr.pa.area = stmp.toInt(); + stmp = WebServer->arg("line"); + KNX_addr.pa.line = stmp.toInt(); + stmp = WebServer->arg("member"); + KNX_addr.pa.member = stmp.toInt(); + Settings.knx_physsical_addr = KNX_addr.value; + knx.physical_address_set( KNX_addr ); // Set Physical KNX Address of the device + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX D_KNX_PHYSICAL_ADDRESS ": %d.%d.%d "), + KNX_addr.pa.area, KNX_addr.pa.line, KNX_addr.pa.member ); + AddLog(LOG_LEVEL_DEBUG); + + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX "GA: %d"), + Settings.knx_GA_registered ); + AddLog(LOG_LEVEL_DEBUG); + for (i = 0; i < Settings.knx_GA_registered ; ++i) + { + KNX_addr.value = Settings.knx_GA_addr[i]; + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX "GA #%d: %s " D_TO " %d/%d/%d"), + i+1, device_param_ga[Settings.knx_GA_param[i]-1], + KNX_addr.ga.area, KNX_addr.ga.line, KNX_addr.ga.member ); + AddLog(LOG_LEVEL_DEBUG); + } + + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX "CB: %d"), + Settings.knx_CB_registered ); + AddLog(LOG_LEVEL_DEBUG); + for (i = 0; i < Settings.knx_CB_registered ; ++i) + { + KNX_addr.value = Settings.knx_CB_addr[i]; + snprintf_P(log_data, sizeof(log_data), PSTR(D_LOG_KNX "CB #%d: %d/%d/%d " D_TO " %s"), + i+1, + KNX_addr.ga.area, KNX_addr.ga.line, KNX_addr.ga.member, + device_param_cb[Settings.knx_CB_param[i]-1] ); + AddLog(LOG_LEVEL_DEBUG); + } +} + +#endif // USE_WEBSERVER + + +/*********************************************************************************************\ + * Interface +\*********************************************************************************************/ + +#define XDRV_11 + +boolean Xdrv11(byte function) +{ + boolean result = false; + switch (function) { + case FUNC_INIT: + KNX_INIT(); + break; + case FUNC_LOOP: + knx.loop(); // Process knx events + // It is not used by the actual config of asyncUDP branch of ESP-KNX-IP Library, + // but is left here for compatibility with upcoming features of ESP-KNX-IP Library + break; +// case FUNC_COMMAND: +// result = KNXCommand(); +// break; +// case FUNC_SET_POWER: +// break; + } + return result; +} + +#endif // USE_KNX