┆ %s"), device.getPower() ? PSTR(D_ON) : PSTR(D_OFF));
if (device.validDimmer() && (channels >= 1)) {
WSContentSend_P(PSTR(" 🔅 %d%%"), changeUIntScale(device.dimmer,0,254,0,100));
}
@@ -1460,6 +1524,7 @@ void ZigbeeShow(bool json)
WSContentSend_P(PSTR(" %dW"), device.mains_power);
}
}
+ WSContentSend_P(PSTR("{e}"));
}
}
From e452f85e391d5e46cdea4a7a28d50b88717dc9d7 Mon Sep 17 00:00:00 2001
From: gemu2015
Date: Thu, 17 Sep 2020 09:11:24 +0200
Subject: [PATCH 09/92] bug fixes and enhancements
---
tasmota/xdrv_10_scripter.ino | 159 ++++++++++++++++++++++++++++++-----
1 file changed, 139 insertions(+), 20 deletions(-)
diff --git a/tasmota/xdrv_10_scripter.ino b/tasmota/xdrv_10_scripter.ino
index ea8223233..32a8c2a3e 100755
--- a/tasmota/xdrv_10_scripter.ino
+++ b/tasmota/xdrv_10_scripter.ino
@@ -79,6 +79,7 @@ uint32_t DecodeLightId(uint32_t hue_id);
#undef LITTLEFS_SCRIPT_SIZE
#undef EEP_SCRIPT_SIZE
#undef USE_SCRIPT_COMPRESSION
+
#if USE_SCRIPT_FATFS==-1
#ifdef ESP32
@@ -250,9 +251,9 @@ SDClass *fsp;
#define FAT_SCRIPT_NAME "script.txt"
#endif
-#if USE_STANDARD_SPI_LIBRARY==0
-#warning ("FATFS standard spi should be used");
-#endif
+//#if USE_STANDARD_SPI_LIBRARY==0
+//#warning ("FATFS standard spi should be used");
+//#endif
#endif // USE_SCRIPT_FATFS
@@ -377,7 +378,11 @@ struct SCRIPT_MEM {
struct T_INDEX *type; // type and index pointer
struct M_FILT *mfilt;
char *glob_vnp; // var name pointer
+#ifdef SCRIPT_LARGE_VNBUFF
+ uint16_t *vnp_offset;
+#else
uint8_t *vnp_offset;
+#endif
char *glob_snp; // string vars pointer
char *scriptptr;
char *section_ptr;
@@ -511,7 +516,7 @@ char *script;
char **snp_p = snp;
uint8_t numperm = 0;
uint8_t numflt = 0;
- uint8_t count;
+ uint16_t count;
glob_script_mem.max_ssize = SCRIPT_SVARSIZE;
glob_script_mem.scriptptr = 0;
@@ -592,7 +597,7 @@ char *script;
} else {
vtypes[vars].bits.is_filter = 0;
}
- *vnp_p ++= vnames_p;
+ *vnp_p++ = vnames_p;
while (lp255) {
+ if (index > MAXVNSIZ) {
free(glob_script_mem.script_mem);
return -5;
}
@@ -760,7 +785,7 @@ char *script;
// copy string variables
char *cp1 = glob_script_mem.glob_snp;
char *sp = strings;
- for (count = 0; count=0
+ if (sel == 0) {
+ result = SD.totalBytes()/1000;
+ } else if (sel == 1) {
+ result = (SD.totalBytes() - SD.usedBytes())/1000;
+ }
+#else
+ if (sel == 0) {
+ result = FFat.totalBytes()/1000;
+ } else if (sel == 1) {
+ result = FFat.freeBytes()/1000;
+ }
+#endif // USE_SCRIPT_FATFS>=0
+#else
+ // ESP8266
+ FSInfo64 fsinfo;
+ fsp->info64(fsinfo);
+ if (sel == 0) {
+ result = fsinfo.totalBytes/1000;
+ } else if (sel == 1) {
+ result = (fsinfo.totalBytes - fsinfo.usedBytes)/1000;
+ }
+#endif // ESP32
+ return result;
+}
+
+// format number with thousand marker
+void form1000(uint32_t number, char *dp, char sc) {
+ char str[32];
+ sprintf(str, "%d", number);
+ char *sp = str;
+ uint32_t inum = strlen(sp)/3;
+ uint32_t fnum = strlen(sp)%3;
+ if (!fnum) inum--;
+ for (uint32_t count=0; count<=inum; count++) {
+ if (fnum){
+ memcpy(dp,sp,fnum);
+ dp+=fnum;
+ sp+=fnum;
+ fnum=0;
+ } else {
+ memcpy(dp,sp,3);
+ dp+=3;
+ sp+=3;
+ }
+ if (count!=inum) {
+ *dp++=sc;
+ }
+ }
+ *dp=0;
+}
+
+#endif //USE_SCRIPT_FATFS
+
#ifdef USE_SCRIPT_GLOBVARS
#define SCRIPT_UDP_BUFFER_SIZE 128
#define SCRIPT_UDP_PORT 1999
IPAddress script_udp_remote_ip;
void Script_Stop_UDP(void) {
- Script_PortUdp.flush();
- Script_PortUdp.stop();
- glob_script_mem.udp_flags.udp_connected = 0;
+ if (!glob_script_mem.udp_flags.udp_used) return;
+ if (glob_script_mem.udp_flags.udp_connected) {
+ Script_PortUdp.flush();
+ Script_PortUdp.stop();
+ glob_script_mem.udp_flags.udp_connected = 0;
+ }
}
void Script_Init_UDP() {
if (global_state.network_down) return;
+ if (!glob_script_mem.udp_flags.udp_used) return;
if (glob_script_mem.udp_flags.udp_connected) return;
if (Script_PortUdp.beginMulticast(WiFi.localIP(), IPAddress(239,255,255,250), SCRIPT_UDP_PORT)) {
@@ -898,6 +985,7 @@ void Script_Init_UDP() {
glob_script_mem.udp_flags.udp_connected = 0;
}
}
+
void Script_PollUdp(void) {
if (global_state.network_down) return;
if (!glob_script_mem.udp_flags.udp_used) return;
@@ -1546,11 +1634,18 @@ chknext:
case 'a':
#ifdef USE_ANGLE_FUNC
if (!strncmp(vname, "acos(", 5)) {
- lp=GetNumericArgument(lp + 5, OPER_EQU, &fvar, 0);
- fvar = acosf(fvar);
- lp++;
- len = 0;
- goto exit;
+ lp=GetNumericArgument(lp + 5, OPER_EQU, &fvar, 0);
+ fvar = acosf(fvar);
+ lp++;
+ len = 0;
+ goto exit;
+ }
+ if (!strncmp(vname, "abs(", 4)) {
+ lp=GetNumericArgument(lp + 4, OPER_EQU, &fvar, 0);
+ fvar = fabs(fvar);
+ lp++;
+ len = 0;
+ goto exit;
}
#endif
if (!strncmp(vname, "asc(", 4)) {
@@ -1982,6 +2077,14 @@ chknext:
len = 0;
goto exit;
}
+
+ if (!strncmp(vname, "fsi(", 4)) {
+ lp = GetNumericArgument(lp + 4, OPER_EQU, &fvar, 0);
+ fvar = get_fsinfo(fvar);
+ lp++;
+ len = 0;
+ goto exit;
+ }
#endif // USE_SCRIPT_FATFS_EXT
if (!strncmp(vname, "fl1(", 4) || !strncmp(vname, "fl2(", 4) ) {
uint8_t lknum = *(lp+2)&3;
@@ -4358,8 +4461,8 @@ const char HTTP_FORM_FILE_UPLOAD[] PROGMEM =
" "
" "
"" D_UPLOAD_STARTED " ... ";
+const char HTTP_FORM_FILE_UPGc[] PROGMEM =
+"total size: %s kB - free: %s kB ";
+
const char HTTP_FORM_SDC_DIRa[] PROGMEM =
"";
const char HTTP_FORM_SDC_DIRb[] PROGMEM =
@@ -4573,6 +4679,11 @@ void Script_FileUploadConfiguration(void) {
WSContentSend_P(HTTP_FORM_FILE_UPLOAD,D_SDCARD_DIR);
WSContentSend_P(HTTP_FORM_FILE_UPG, D_SCRIPT_UPLOAD);
#ifdef SDCARD_DIR
+ char ts[16];
+ char fs[16];
+ form1000(get_fsinfo(0), ts, '.');
+ form1000(get_fsinfo(1), fs, '.');
+ WSContentSend_P(HTTP_FORM_FILE_UPGc, ts, fs);
WSContentSend_P(HTTP_FORM_SDC_DIRa);
if (glob_script_mem.script_sd_found) {
ListDir(path, depth);
@@ -4837,6 +4948,11 @@ void ScriptSaveSettings(void) {
}
void SaveScriptEnd(void) {
+
+#ifdef USE_SCRIPT_GLOBVARS
+ Script_Stop_UDP();
+#endif //USE_SCRIPT_GLOBVARS
+
if (glob_script_mem.script_mem) {
Scripter_save_pvars();
free(glob_script_mem.script_mem);
@@ -4856,6 +4972,9 @@ void SaveScriptEnd(void) {
#endif // USE_SCRIPT_COMPRESSION
if (bitRead(Settings.rule_enabled, 0)) {
+
+
+
int16_t res = Init_Scripter();
if (res) {
AddLog_P2(LOG_LEVEL_INFO, PSTR("script init error: %d"), res);
@@ -7110,8 +7229,8 @@ bool Xdrv10(uint8_t function)
Webserver->on("/exs", HTTP_GET, ScriptExecuteUploadSuccess);
#ifdef USE_SCRIPT_FATFS
- Webserver->on("/u3", HTTP_POST,[]() { Webserver->sendHeader("Location","/u3");Webserver->send(303);}, script_upload);
- Webserver->on("/u3", HTTP_GET, ScriptFileUploadSuccess);
+ Webserver->on("/u13", HTTP_POST,[]() { Webserver->sendHeader("Location","/u13");Webserver->send(303);}, script_upload);
+ Webserver->on("/u13", HTTP_GET, ScriptFileUploadSuccess);
Webserver->on("/upl", HTTP_GET, Script_FileUploadConfiguration);
#endif //USE_SCRIPT_FATFS
break;
From 8038c2446029a3e31ab15b2e60224abfb322d798 Mon Sep 17 00:00:00 2001
From: Jason2866 <24528715+Jason2866@users.noreply.github.com>
Date: Thu, 17 Sep 2020 18:17:26 +0200
Subject: [PATCH 10/92] Use Tasmota stage, core 2.7.4.2
core 2.7.4.2 includes some backports from arduino / master
https://github.com/esp8266/Arduino/pull/7547
https://github.com/esp8266/Arduino/pull/7586
https://github.com/esp8266/Arduino/pull/7585
https://github.com/esp8266/Arduino/pull/7595
---
platformio_override_sample.ini | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini
index ddd74a60d..188e05e7e 100644
--- a/platformio_override_sample.ini
+++ b/platformio_override_sample.ini
@@ -88,7 +88,7 @@ extra_scripts = ${scripts_defaults.extra_scripts}
[tasmota_stage]
; *** Esp8266 core for Arduino version Tasmota stage
platform = espressif8266@2.6.2
-platform_packages = jason2866/framework-arduinoespressif8266
+platform_packages = framework-arduinoespressif8266@https://github.com/Jason2866/Arduino.git#2.7.4.2
build_unflags = ${esp_defaults.build_unflags}
build_flags = ${esp82xx_defaults.build_flags}
From ecf9e4ea65cb1ee053ac307cc4290d0bc8a4b7a0 Mon Sep 17 00:00:00 2001
From: stefanbode
Date: Fri, 18 Sep 2020 08:31:05 +0200
Subject: [PATCH 11/92] Fix int16 overflow on large open times
---
tasmota/xdrv_27_shutter.ino | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tasmota/xdrv_27_shutter.ino b/tasmota/xdrv_27_shutter.ino
index 19e89f113..a1625c786 100644
--- a/tasmota/xdrv_27_shutter.ino
+++ b/tasmota/xdrv_27_shutter.ino
@@ -560,7 +560,7 @@ int32_t ShutterCalculatePosition(uint32_t i)
if (Shutter[i].direction != 0) {
switch (ShutterGlobal.position_mode) {
case SHT_COUNTER:
- return ((int32_t)RtcSettings.pulse_counter[i]*Shutter[i].direction*STEPS_PER_SECOND*RESOLUTION / ShutterGlobal.open_velocity_max)+Shutter[i].start_position;
+ return ((int32_t)RtcSettings.pulse_counter[i]*Shutter[i].direction*STEPS_PER_SECOND / ShutterGlobal.open_velocity_max * RESOLUTION)+Shutter[i].start_position;
break;
case SHT_TIME:
case SHT_TIME_UP_DOWN:
From aafa31ff8c2991ef99ee3898d8a1a8b4f0503435 Mon Sep 17 00:00:00 2001
From: Staars
Date: Fri, 18 Sep 2020 17:46:58 +0200
Subject: [PATCH 12/92] NRF24: add MHOC and ATC
---
tasmota/xsns_61_MI_NRF24.ino | 196 +++++++++++++++++++++--------------
1 file changed, 120 insertions(+), 76 deletions(-)
diff --git a/tasmota/xsns_61_MI_NRF24.ino b/tasmota/xsns_61_MI_NRF24.ino
index 44456a6a7..bc945ef2a 100644
--- a/tasmota/xsns_61_MI_NRF24.ino
+++ b/tasmota/xsns_61_MI_NRF24.ino
@@ -20,7 +20,8 @@
--------------------------------------------------------------------------------------------
Version yyyymmdd Action Description
--------------------------------------------------------------------------------------------
-
+ 0.9.8.0 20200918 integrate - add MHOC303, ATC-custom FW, allow lower case commands
+ ---
0.9.8.0 20200705 integrate - add YEE-RC, NLIGHT and MJYD2S, add NRFUSE
---
0.9.7.0 20200624 integrate - fix BEARSSL-decryption, remove MBEDTLS, prepare night light sensors
@@ -84,8 +85,10 @@
#define MJYD2S 8
#define YEERC 9
#define MHOC401 10
+#define MHOC303 11
+#define ATC 12
-#define MI_TYPES 10 //count this manually
+#define MI_TYPES 12 //count this manually
#define D_CMND_NRF "NRF"
@@ -121,7 +124,9 @@ const uint16_t kMINRFDeviceID[MI_TYPES]={ 0x0098, // Flora
0x03dd, // NLIGHT
0x07f6, // MJYD2S
0x0153, // yee-rc
- 0x0387 // MHO-C401
+ 0x0387, // MHO-C401
+ 0x06d3, // MHO-C303
+ 0x0a1c // ATC -> this is a fake ID
};
const char kMINRFDeviceType1[] PROGMEM = "Flora";
@@ -134,7 +139,9 @@ const char kMINRFDeviceType7[] PROGMEM = "NLIGHT";
const char kMINRFDeviceType8[] PROGMEM = "MJYD2S";
const char kMINRFDeviceType9[] PROGMEM = "YEERC";
const char kMINRFDeviceType10[] PROGMEM = "MHOC401";
-const char * kMINRFDeviceType[] PROGMEM = {kMINRFDeviceType1,kMINRFDeviceType2,kMINRFDeviceType3,kMINRFDeviceType4,kMINRFDeviceType5,kMINRFDeviceType6,kMINRFDeviceType7,kMINRFDeviceType8,kMINRFDeviceType9,kMINRFDeviceType10};
+const char kMINRFDeviceType11[] PROGMEM = "MHOC303";
+const char kMINRFDeviceType12[] PROGMEM = "ATC";
+const char * kMINRFDeviceType[] PROGMEM = {kMINRFDeviceType1,kMINRFDeviceType2,kMINRFDeviceType3,kMINRFDeviceType4,kMINRFDeviceType5,kMINRFDeviceType6,kMINRFDeviceType7,kMINRFDeviceType8,kMINRFDeviceType9,kMINRFDeviceType10,kMINRFDeviceType11,kMINRFDeviceType12};
// PDU's or different channels 37-39
const uint32_t kMINRFFloPDU[3] = {0x3eaa857d,0xef3b8730,0x71da7b46};
@@ -146,11 +153,13 @@ const uint32_t kMINRFCGGPDU[3] = {0x4760cd6e,0xdbcc0cdb,0x33048dfd};
const uint32_t kMINRFCGDPDU[3] = {0x5da0d752,0xc10c16e7,0x29c497c1};
// const uint32_t kMINRFNLIPDU[3] = {0x4760C56E,0xDBCC04DB,0x0330485FD}; //NLIGHT
const uint32_t kMINRFYRCPDU[3] = {0x216D63E2,0x5C3DD47E,0x0A5D0E96}; //yee-rc - 50 30
+const uint32_t kMINRFATCPDU[3] = {0xA6E4D00A,0xD0CDAD5A,0x8B03FB3A}; //ATC
// start-LSFR for different channels 37-39
const uint8_t kMINRFlsfrList_A[3] = {0x4b,0x17,0x23}; // Flora, LYWSD02
const uint8_t kMINRFlsfrList_B[3] = {0x21,0x72,0x43}; // MJ_HT_V1, LYWSD03, CGx
const uint8_t kMINRFlsfrList_C[3] = {0x38,0x25,0x2e}; // yee-rc
+const uint8_t kMINRFlsfrList_D[3] = {0x26,0x23,0x20}; // ATC
#pragma pack(1) // important!!
@@ -224,6 +233,15 @@ struct mjysd02_Packet_t{
uint8_t data[18];
};
+struct ATCPacket_t{
+ uint8_t MAC[6];
+ int16_t temp; //sadly this is in wrong endianess
+ uint8_t hum;
+ uint8_t batPer;
+ uint16_t batMV;
+ uint8_t frameCnt;
+};
+
union mi_bindKey_t{
struct{
uint8_t key[16];
@@ -363,7 +381,7 @@ bool MINRFinitBLE(uint8_t _mode)
MINRFchangePacketModeTo(_mode);
return true;
}
- // DEBUG_SENSOR_LOG(PSTR("MINRF chip NOT !!!! connected"));
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF chip NOT !!!! connected"));
return false;
}
@@ -406,35 +424,38 @@ bool MINRFreceivePacket(void)
MINRFswapbuf((uint8_t*)&MINRF.buffer, sizeof(MINRF.buffer) );
// MINRF_LOG_BUFFER();
- // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: _lsfrlist: %x, chan: %u, mode: %u"),_lsfrlist[MINRF.currentChan],MINRF.currentChan, MINRF.packetMode);
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: _lsfrlist: %x, chan: %u, mode: %u"),_lsfrlist[MINRF.currentChan],MINRF.currentChan, MINRF.packetMode);
switch (MINRF.packetMode) {
- case 0: case 7: case 8:
+ case 0: case NLIGHT: case MJYD2S:
MINRFwhiten((uint8_t *)&MINRF.buffer, sizeof(MINRF.buffer), MINRF.channel[MINRF.currentChan] | 0x40); // "BEACON" mode, "NLIGHT" mode, "MJYD2S" mode
break;
- case 1: case 3:
+ case FLORA: case LYWSD02: case MHOC303:
MINRFwhiten((uint8_t *)&MINRF.buffer, sizeof(MINRF.buffer), kMINRFlsfrList_A[MINRF.currentChan]); // "flora" mode, "LYWSD02" mode
break;
- case 2: case 4: case 5: case 6: case MHOC401:
+ case MJ_HT_V1: case LYWSD03: case CGG1: case CGD1: case MHOC401:
MINRFwhiten((uint8_t *)&MINRF.buffer, sizeof(MINRF.buffer), kMINRFlsfrList_B[MINRF.currentChan]); // "MJ_HT_V1" mode, LYWSD03" mode, "CGG1" mode, "CGD1" mode
break;
- case 9:
+ case YEERC:
MINRFwhiten((uint8_t *)&MINRF.buffer, sizeof(MINRF.buffer), kMINRFlsfrList_C[MINRF.currentChan]); // "YEE-RC" mode
break;
+ case ATC:
+ MINRFwhiten((uint8_t *)&MINRF.buffer, sizeof(MINRF.buffer), kMINRFlsfrList_D[MINRF.currentChan]); // ATC
+ break;
}
- // DEBUG_SENSOR_LOG(PSTR("MINRF: LSFR:%x"),_lsfr);
+ // DEBUG_SENSOR_LOG(PSTR("NRF: LSFR:%x"),_lsfr);
// if (_lsfr>254) _lsfr=0;
}
- // DEBUG_SENSOR_LOG(PSTR("MINRF: did read FIFO"));
+ // DEBUG_SENSOR_LOG(PSTR("NRF: did read FIFO"));
return true;
}
// #ifdef DEBUG_TASMOTA_SENSOR
void MINRFshowBuffer(uint8_t (&buf)[32]){ // we use this only for the 32-byte-FIFO-buffer, so 32 is hardcoded
- // DEBUG_SENSOR_LOG(PSTR("MINRF: Buffer: %c %c %c %c %c %c %c %c"
+ // DEBUG_SENSOR_LOG(PSTR("NRF: Buffer: %c %c %c %c %c %c %c %c"
// " %c %c %c %c %c %c %c %c"
// " %c %c %c %c %c %c %c %c"
// " %c %c %c %c %c %c %c %c")
- DEBUG_SENSOR_LOG(PSTR("MINRF: Buffer: %02x %02x %02x %02x %02x %02x %02x %02x "
+ DEBUG_SENSOR_LOG(PSTR("NRF: Buffer: %02x %02x %02x %02x %02x %02x %02x %02x "
"%02x %02x %02x %02x %02x %02x %02x %02x "
"%02x %02x %02x %02x %02x %02x %02x %02x "
"%02x %02x %02x %02x %02x %02x %02x %02x ")
@@ -511,7 +532,7 @@ void MINRFhandleScan(void){
MINRFscanResult.erase(std::remove_if(MINRFscanResult.begin(),
MINRFscanResult.end(),
[&i](scan_entry_t e) {
- if(e.showedUp>2) AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: Beacon %02u: %02X%02X%02X%02X%02X%02X Cid: %04X Svc: %04X UUID: %04X"),i,e.MAC[0],e.MAC[1],e.MAC[2],e.MAC[3],e.MAC[4],e.MAC[5],e.cid,e.svc,e.uuid);
+ if(e.showedUp>2) AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: Beacon %02u: %02X%02X%02X%02X%02X%02X Cid: %04X Svc: %04X UUID: %04X"),i,e.MAC[0],e.MAC[1],e.MAC[2],e.MAC[3],e.MAC[4],e.MAC[5],e.cid,e.svc,e.uuid);
i++;
return ((e.showedUp < 3));
}),
@@ -524,7 +545,7 @@ void MINRFhandleScan(void){
for(uint32_t i=0; i30) break;
uint32_t ADtype = _buf[i+1];
- // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: Size: %u AD: %x i:%u"), size, ADtype,i);
+ // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: Size: %u AD: %x i:%u"), size, ADtype,i);
if (size+i>32+offset) size=32-i+offset-2;
if (size>30) break;
char _stemp[(size*2)];
uint32_t backupSize;
switch(ADtype){
case 0x01:
- AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: Flags: %02x"), _buf[i+2]);
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: Flags: %02x"), _buf[i+2]);
break;
case 0x02: case 0x03:
entry->uuid = _buf[i+3]*256 + _buf[i+2];
- AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: UUID: %04x"), entry->uuid);
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: UUID: %04x"), entry->uuid);
success = true;
break;
case 0x08: case 0x09:
backupSize = _buf[i+size+1];
_buf[i+size+1] = 0;
- AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: Name: %s"), (char*)&_buf[i+2]);
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: Name: %s"), (char*)&_buf[i+2]);
success = true;
_buf[i+size+1] = backupSize;
break;
case 0x0a:
- AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: TxPow: %02u"), _buf[i+2]);
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: TxPow: %02u"), _buf[i+2]);
break;
case 0xff:
entry->cid = _buf[i+3]*256 + _buf[i+2];
- AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: Cid: %04x"), entry->cid);
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: Cid: %04x"), entry->cid);
ToHex_P((unsigned char*)&_buf+i+4,size-3,_stemp,(size*2));
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s"),_stemp);
success = true;
break;
case 0x16:
entry->svc = _buf[i+3]*256 + _buf[i+2];
- AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: Svc: %04x"), entry->svc);
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: Svc: %04x"), entry->svc);
ToHex_P((unsigned char*)&_buf+i+4,size-3,_stemp,(size*2));
AddLog_P2(LOG_LEVEL_DEBUG,PSTR("%s"),_stemp);
success = true;
@@ -712,7 +733,7 @@ int MINRFdecryptPacket(char *_buf){
br_ccm_run(&ctx, 0, output, sizeof(packet->payload.cipher));
ret = br_ccm_check_tag(&ctx, packet->payload.tag);
- AddLog_P2(LOG_LEVEL_DEBUG,PSTR("BEARSSL: Err:%i, Decrypted : %02x %02x %02x %02x %02x "), ret, output[0],output[1],output[2],output[3],output[4]);
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: Err:%i, Decrypted : %02x %02x %02x %02x %02x "), ret, output[0],output[1],output[2],output[3],output[4]);
memcpy((uint8_t*)(packet->payload.cipher)+1,output,sizeof(packet->payload.cipher));
return ret;
}
@@ -833,6 +854,7 @@ void MINRFAddKey(char* payload){
*/
void MINRFKeyMACStringToBytes(char* _string,uint8_t _keyMAC[]) { //uppercase
uint32_t index = 0;
+ UpperCase(_string,_string);
while (index < 44) {
char c = _string[index];
uint8_t value = 0;
@@ -843,9 +865,9 @@ void MINRFKeyMACStringToBytes(char* _string,uint8_t _keyMAC[]) { //uppercase
_keyMAC[(index/2)] += value << (((index + 1) % 2) * 4);
index++;
}
- DEBUG_SENSOR_LOG(PSTR("MINRF: %s to:"),_string);
- DEBUG_SENSOR_LOG(PSTR("MINRF: key-array: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X"),_keyMAC[0],_keyMAC[1],_keyMAC[2],_keyMAC[3],_keyMAC[4],_keyMAC[5],_keyMAC[6],_keyMAC[7],_keyMAC[8],_keyMAC[9],_keyMAC[10],_keyMAC[11],_keyMAC[12],_keyMAC[13],_keyMAC[14],_keyMAC[15]);
- DEBUG_SENSOR_LOG(PSTR("MINRF: MAC-array: %02X%02X%02X%02X%02X%02X"),_keyMAC[16],_keyMAC[17],_keyMAC[18],_keyMAC[19],_keyMAC[20],_keyMAC[21]);
+ DEBUG_SENSOR_LOG(PSTR("NRF: %s to:"),_string);
+ DEBUG_SENSOR_LOG(PSTR("NRF: key-array: %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X"),_keyMAC[0],_keyMAC[1],_keyMAC[2],_keyMAC[3],_keyMAC[4],_keyMAC[5],_keyMAC[6],_keyMAC[7],_keyMAC[8],_keyMAC[9],_keyMAC[10],_keyMAC[11],_keyMAC[12],_keyMAC[13],_keyMAC[14],_keyMAC[15]);
+ DEBUG_SENSOR_LOG(PSTR("NRF: MAC-array: %02X%02X%02X%02X%02X%02X"),_keyMAC[16],_keyMAC[17],_keyMAC[18],_keyMAC[19],_keyMAC[20],_keyMAC[21]);
}
#endif //USE_MI_DECRYPTION
/**
@@ -856,6 +878,7 @@ void MINRFKeyMACStringToBytes(char* _string,uint8_t _keyMAC[]) { //uppercase
*/
void MINRFMACStringToBytes(char* _string, uint8_t _MAC[]) { //uppercase
uint32_t index = 0;
+ UpperCase(_string,_string);
while (index < 12) {
char c = _string[index];
uint8_t value = 0;
@@ -866,7 +889,7 @@ void MINRFMACStringToBytes(char* _string, uint8_t _MAC[]) { //uppercase
_MAC[(index/2)] += value << (((index + 1) % 2) * 4);
index++;
}
- // DEBUG_SENSOR_LOG(PSTR("MINRF: %s to MAC-array: %02X%02X%02X%02X%02X%02X"),_string,_MAC[0],_MAC[1],_MAC[2],_MAC[3],_MAC[4],_MAC[5]);
+ // DEBUG_SENSOR_LOG(PSTR("NRF: %s to MAC-array: %02X%02X%02X%02X%02X%02X"),_string,_MAC[0],_MAC[1],_MAC[2],_MAC[3],_MAC[4],_MAC[5]);
}
/**
@@ -876,7 +899,7 @@ void MINRFMACStringToBytes(char* _string, uint8_t _MAC[]) { //uppercase
void MINRFcomputefirstUsedPacketMode(void){
for (uint32_t i = 0; iMI_TYPES) MINRF.firstUsedPacketMode=0;
break;
@@ -913,34 +936,37 @@ void MINRFchangePacketModeTo(uint8_t _mode) {
case 0: // normal BLE advertisement
NRF24radio.openReadingPipe(0,0x6B7D9171); // advertisement address: 0x8E89BED6 (bit-reversed -> 0x6B7D9171)
break;
- case 1: // special flora packet
+ case FLORA: // special flora packet
NRF24radio.openReadingPipe(0,kMINRFFloPDU[_nextchannel]); // 95 fe 71 20 -> flora
break;
- case 2: // special MJ_HT_V1 packet
+ case MJ_HT_V1: // special MJ_HT_V1 packet
NRF24radio.openReadingPipe(0,kMINRFMJPDU[_nextchannel]); // 95 fe 50 20 -> MJ_HT_V1
break;
- case 3: // special LYWSD02 packet
+ case LYWSD02: case MHOC303: // special LYWSD02 packet
NRF24radio.openReadingPipe(0,kMINRFL2PDU[_nextchannel]);// 95 fe 70 20 -> LYWSD02
break;
- case 4: case MHOC401: // special LYWSD03 packet, MHOC401 has the same
+ case LYWSD03: case MHOC401: // special LYWSD03 packet, MHOC401 has the same
NRF24radio.openReadingPipe(0,kMINRFL3PDU[_nextchannel]);// 95 fe 58 58 -> LYWSD03 (= encrypted data message)
break;
- case 5: // special CGG1 packet
+ case CGG1: // special CGG1 packet
NRF24radio.openReadingPipe(0,kMINRFCGGPDU[_nextchannel]); // 95 fe 50 30 -> CGG1
break;
- case 6: // special CGD1 packet
+ case CGD1: // special CGD1 packet
NRF24radio.openReadingPipe(0,kMINRFCGDPDU[_nextchannel]); // cd fd 08 0c -> CGD1
break;
- case 7: case 8:// MAC based LIGHT packet
+ case NLIGHT: case MJYD2S:// MAC based LIGHT packet
if (MIBLElights.size()==0) break;
NRF24radio.openReadingPipe(0,MIBLElights[MINRF.activeLight].PDU[_nextchannel]); // computed from MAC -> NLIGHT and MJYSD2S
MINRF.activeLight++;
break;
- case 9: // YEE-RC packet
+ case YEERC: // YEE-RC packet
NRF24radio.openReadingPipe(0,kMINRFYRCPDU[_nextchannel]);// 95 fe 50 30 -> YEE-RC
break;
+ case ATC:
+ NRF24radio.openReadingPipe(0,kMINRFATCPDU[_nextchannel]);// 10 16 1a 18 -> ATC
+ break;
}
- // DEBUG_SENSOR_LOG(PSTR("MINRF: Change Mode to %u"),_mode);
+ // DEBUG_SENSOR_LOG(PSTR("NRF: Change Mode to %u"),_mode);
MINRF.packetMode = _mode;
}
@@ -953,27 +979,27 @@ void MINRFchangePacketModeTo(uint8_t _mode) {
*/
uint32_t MINRFgetSensorSlot(uint8_t (&_MAC)[6], uint16_t _type){
- DEBUG_SENSOR_LOG(PSTR("MINRF: will test ID-type: %x"), _type);
+ DEBUG_SENSOR_LOG(PSTR("NRF: will test ID-type: %x"), _type);
bool _success = false;
for (uint32_t i=0;itype,MINRF.buffer.miBeacon.type);
+ DEBUG_SENSOR_LOG(PSTR("NRF: %u %u %u"),_slot,_sensorVec->type,MINRF.buffer.miBeacon.type);
float _tempFloat;
int decryptRet;
@@ -1082,7 +1108,7 @@ void MINRFhandleMiBeaconPacket(void){
switch(MINRF.buffer.miBeacon.type){
case 0x1:
if(MINRF.buffer.miBeacon.counter==_sensorVec->lastCnt) break;
- // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: YEE-RC button: %u Long: %u"), MINRF.buffer.miBeacon.Btn.num, MINRF.buffer.miBeacon.Btn.longPress);
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: YEE-RC button: %u Long: %u"), MINRF.buffer.miBeacon.Btn.num, MINRF.buffer.miBeacon.Btn.longPress);
_sensorVec->lastCnt=MINRF.buffer.miBeacon.counter;
_sensorVec->btn=MINRF.buffer.miBeacon.Btn.num + (MINRF.buffer.miBeacon.Btn.longPress/2)*6;
_sensorVec->shallSendMQTT = 1;
@@ -1154,7 +1180,7 @@ void MINRFhandleMiBeaconPacket(void){
void MINRFhandleCGD1Packet(void){ // no MiBeacon
MINRFreverseMAC(MINRF.buffer.CGDPacket.MAC);
uint32_t _slot = MINRFgetSensorSlot(MINRF.buffer.CGDPacket.MAC, 0x0576); // This must be hard-coded, no object-id in Cleargrass-packet
- DEBUG_SENSOR_LOG(PSTR("MINRF: Sensor slot: %u"), _slot);
+ DEBUG_SENSOR_LOG(PSTR("NRF: Sensor slot: %u"), _slot);
if(_slot==0xff) return;
switch (MINRF.buffer.CGDPacket.mode){
@@ -1179,7 +1205,7 @@ void MINRFhandleCGD1Packet(void){ // no MiBeacon
}
break;
default:
- DEBUG_SENSOR_LOG(PSTR("MINRF: unexpected CGD1-packet"));
+ DEBUG_SENSOR_LOG(PSTR("NRF: unexpected CGD1-packet"));
MINRF_LOG_BUFFER(MINRF.buffer.raw);
}
}
@@ -1188,10 +1214,10 @@ void MINRFhandleNlightPacket(void){ // no MiBeacon
uint32_t offset = 6;
uint8_t _buf[32+offset];
MINRFrecalcBuffer((uint8_t*)&_buf,offset);
- // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: NLIGHT: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x"),_buf[0],_buf[1],_buf[2],_buf[3],_buf[4],_buf[5],_buf[6],_buf[7],_buf[8],_buf[9],_buf[10],_buf[11],_buf[12],_buf[13],_buf[14],_buf[15],_buf[16],_buf[17],_buf[18]);
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: NLIGHT: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x"),_buf[0],_buf[1],_buf[2],_buf[3],_buf[4],_buf[5],_buf[6],_buf[7],_buf[8],_buf[9],_buf[10],_buf[11],_buf[12],_buf[13],_buf[14],_buf[15],_buf[16],_buf[17],_buf[18]);
uint32_t _frame_PID = _buf[15]<<24 | _buf[16]<<16 | _buf[17]<<8 | _buf[18];
if(_frame_PID!=0x4030dd03) return; // invalid packet
- // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: NLIGHT:%x"),_frame_PID);
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: NLIGHT:%x"),_frame_PID);
uint32_t _idx = MINRF.activeLight-1;
if((millis() - MIBLElights[_idx].lastTime)<1500) return;
if(_buf[19]!=MIBLElights[_idx].lastCnt){
@@ -1199,7 +1225,7 @@ void MINRFhandleNlightPacket(void){ // no MiBeacon
MIBLElights[_idx].events++;
MIBLElights[_idx].shallSendMQTT = 1;
MIBLElights[_idx].lastTime = millis();
- AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: NLIGHT %u: events: %u, Cnt:%u"), _idx,MIBLElights[_idx].events, MIBLElights[_idx].lastCnt);
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: NLIGHT %u: events: %u, Cnt:%u"), _idx,MIBLElights[_idx].events, MIBLElights[_idx].lastCnt);
}
}
@@ -1209,20 +1235,20 @@ void MINRFhandleMJYD2SPacket(void){ // no MiBeacon
MINRFrecalcBuffer((uint8_t*)&_buf,offset);
mjysd02_Packet_t *_packet = (mjysd02_Packet_t*)&_buf;
if(_packet->PID!=0x07f6) return; // invalid packet
- // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: MJYD2S: %02u %04x %04x %04x %02x"),_packet->payloadSize,_packet->UUID,_packet->frameCtrl,_packet->PID,_packet->frameCnt);
- // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: PAYLOAD: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x"),_packet->data[0],_packet->data[1],_packet->data[2],_packet->data[3],_packet->data[4],_packet->data[5],_packet->data[6],_packet->data[7],_packet->data[8],_packet->data[9],_packet->data[10],_packet->data[11],_packet->data[12],_packet->data[13],_packet->data[14],_packet->data[15],_packet->data[16],_packet->data[17]);
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: MJYD2S: %02u %04x %04x %04x %02x"),_packet->payloadSize,_packet->UUID,_packet->frameCtrl,_packet->PID,_packet->frameCnt);
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: PAYLOAD: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x"),_packet->data[0],_packet->data[1],_packet->data[2],_packet->data[3],_packet->data[4],_packet->data[5],_packet->data[6],_packet->data[7],_packet->data[8],_packet->data[9],_packet->data[10],_packet->data[11],_packet->data[12],_packet->data[13],_packet->data[14],_packet->data[15],_packet->data[16],_packet->data[17]);
uint32_t _idx = MINRF.activeLight-1;
switch(_packet->frameCtrl){
case 0x5910:
if(_packet->frameCnt!=MIBLElights[_idx].lastCnt){
- // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: MJYD2S after motion:%x"),_packet->frameCnt);
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: MJYD2S after motion:%x"),_packet->frameCnt);
MIBLElights[_idx].lastCnt = _packet->frameCnt;
if(millis()-MIBLElights[_idx].lastTime>120000){
MIBLElights[_idx].eventType = 1;
MIBLElights[_idx].events++;
MIBLElights[_idx].shallSendMQTT = 1;
MIBLElights[_idx].lastTime = millis();
- AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: MJYD2S secondary PIR"));
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: MJYD2S secondary PIR"));
}
}
break;
@@ -1238,7 +1264,7 @@ void MINRFhandleMJYD2SPacket(void){ // no MiBeacon
if(millis()-MIBLElights[_idx].lastTime>1000){
MIBLElights[_idx].eventType = 1; //PIR
MIBLElights[_idx].shallSendMQTT = 1;
- AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: MJYD2S primary PIR"));
+ AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: MJYD2S primary PIR"));
MIBLElights[_idx].events++;
}
MIBLElights[_idx].lastTime = millis();
@@ -1259,23 +1285,23 @@ void MINRFhandleMJYD2SPacket(void){ // no MiBeacon
MIBLElights[_idx].NMT = output[6]<<24 | output[5]<<16 | output[4]<<8 | output[3];
MIBLElights[_idx].eventType = 3; // NMT 0, 120, 300, 600, 1800, ... seconds
MIBLElights[_idx].shallSendMQTT = 1;
- // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("MINRF: MJYD2S NMT: %u"), MIBLElights[_idx].NMT );
+ // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("NRF: MJYD2S NMT: %u"), MIBLElights[_idx].NMT );
break;
}
}
}
- // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: NLIGHT:%x"),_frame_PID);
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: NLIGHT:%x"),_frame_PID);
}
void MINRFhandleLightPacket(void){
switch(MIBLElights[MINRF.activeLight-1].type){
case NLIGHT:
- // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: NLIGHT!!"));
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: NLIGHT!!"));
MINRFhandleNlightPacket();
break;
case MJYD2S:
- // AddLog_P2(LOG_LEVEL_INFO,PSTR("MINRF: MJYD2S !!"));
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: MJYD2S !!"));
MINRFhandleMJYD2SPacket();
break;
}
@@ -1285,7 +1311,7 @@ void MINRFhandleLightPacket(void){
void MINRFaddLight(uint8_t _MAC[], uint8_t _type){ // no MiBeacon
for(uint32_t i=0; iMAC, 0x0a1c); // This must be a hard-coded fake ID
+ // AddLog_P2(LOG_LEVEL_DEBUG,PSTR("known %s at slot %u"), kMINRFDeviceType[MIBLEsensors[_slot].type-1],_slot);
+ // AddLog_P2(LOG_LEVEL_INFO,PSTR("NRF: ATC: %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x %02x"),MINRF.buffer.raw[0],MINRF.buffer.raw[1],MINRF.buffer.raw[2],MINRF.buffer.raw[3],MINRF.buffer.raw[4],MINRF.buffer.raw[5],MINRF.buffer.raw[6],MINRF.buffer.raw[7],MINRF.buffer.raw[8],MINRF.buffer.raw[9],MINRF.buffer.raw[10],MINRF.buffer.raw[11]);
+ if(_slot==0xff) return;
+
+ MIBLEsensors.at(_slot).temp = (float)(__builtin_bswap16(_packet->temp))/10.0f;
+ MIBLEsensors.at(_slot).hum = (float)_packet->hum;
+ MIBLEsensors.at(_slot).bat = _packet->batPer;
+
+ MIBLEsensors[_slot].shallSendMQTT = 1;
}
/*********************************************************************************************\
@@ -1309,14 +1349,15 @@ void MINRFaddLight(uint8_t _MAC[], uint8_t _type){ // no MiBeacon
void MINRF_EVERY_50_MSECOND() { // Every 50mseconds
if(MINRF.timer>6000){ // happens every 6000/20 = 300 seconds
- DEBUG_SENSOR_LOG(PSTR("MINRF: check for FAKE sensors"));
+ DEBUG_SENSOR_LOG(PSTR("NRF: check for FAKE sensors"));
MINRFpurgeFakeSensors();
MINRF.timer=0;
}
MINRF.timer++;
if (!MINRFreceivePacket()){
- // DEBUG_SENSOR_LOG(PSTR("MINRF: nothing received"));
+ // DEBUG_SENSOR_LOG(PSTR("NRF: nothing received"));
+ // if (MINRF.packetMode==ATC) AddLog_P2(LOG_LEVEL_INFO,PSTR("no ATC.."));
}
else {
@@ -1327,7 +1368,7 @@ void MINRF_EVERY_50_MSECOND() { // Every 50mseconds
}
else MINRFhandleScan();
break;
- case FLORA: case MJ_HT_V1: case LYWSD02: case CGG1: case LYWSD03: case YEERC: case MHOC401:
+ case FLORA: case MJ_HT_V1: case LYWSD02: case CGG1: case LYWSD03: case YEERC: case MHOC401: case MHOC303:
MINRFhandleMiBeaconPacket();
break;
case CGD1:
@@ -1336,6 +1377,9 @@ void MINRF_EVERY_50_MSECOND() { // Every 50mseconds
case NLIGHT: //case MJYD2S:
MINRFhandleLightPacket();
break;
+ case ATC:
+ MINRFhandleATCPacket();
+ break;
default:
break;
}
@@ -1534,7 +1578,7 @@ void MINRFShow(bool json)
if (json) {
for (uint32_t i = 0; i < MIBLEsensors.size(); i++) {
if(MIBLEsensors[i].showedUp < 3){
- DEBUG_SENSOR_LOG(PSTR("MINRF: sensor not fully registered yet"));
+ DEBUG_SENSOR_LOG(PSTR("NRF: sensor not fully registered yet"));
if(MIBLEsensors[i].type != YEERC) break; // send every RC code, even if there is a potentially false MAC
}
switch(MIBLEsensors[i].type){
@@ -1647,7 +1691,7 @@ void MINRFShow(bool json)
WSContentSend_PD(HTTP_NRF24NEW, NRF24type, NRF24.chipType, i+1,stemp,MINRF.confirmedSensors);
for (i ; i
Date: Fri, 18 Sep 2020 17:51:20 +0200
Subject: [PATCH 13/92] version correction
---
tasmota/xsns_61_MI_NRF24.ino | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tasmota/xsns_61_MI_NRF24.ino b/tasmota/xsns_61_MI_NRF24.ino
index bc945ef2a..e9d2841ea 100644
--- a/tasmota/xsns_61_MI_NRF24.ino
+++ b/tasmota/xsns_61_MI_NRF24.ino
@@ -20,7 +20,7 @@
--------------------------------------------------------------------------------------------
Version yyyymmdd Action Description
--------------------------------------------------------------------------------------------
- 0.9.8.0 20200918 integrate - add MHOC303, ATC-custom FW, allow lower case commands
+ 0.9.8.1 20200918 integrate - add MHOC303, ATC-custom FW, allow lower case commands
---
0.9.8.0 20200705 integrate - add YEE-RC, NLIGHT and MJYD2S, add NRFUSE
---
From f6705d6a1a91c4417ef83b1d4287ee5bbb7da2ae Mon Sep 17 00:00:00 2001
From: Federico Leoni
Date: Fri, 18 Sep 2020 18:45:20 -0300
Subject: [PATCH 14/92] Prep for official Hass Discovery
---
tasmota/xdrv_12_home_assistant.ino | 145 +++++++++++++++++++++++++----
1 file changed, 129 insertions(+), 16 deletions(-)
diff --git a/tasmota/xdrv_12_home_assistant.ino b/tasmota/xdrv_12_home_assistant.ino
index ef9d25be9..a9673eb38 100644
--- a/tasmota/xdrv_12_home_assistant.ino
+++ b/tasmota/xdrv_12_home_assistant.ino
@@ -174,6 +174,110 @@ uint8_t hass_init_step = 0;
uint8_t hass_mode = 0;
int hass_tele_period = 0;
+// NEW DISCOVERY
+
+const char HASS_DISCOVER_DEVICE[] PROGMEM = // Basic parameters for Discovery
+ "{\"ip\":\"%s\"," // IP Address
+ "\"dn\":\"%s\"," // Device Name
+ "\"fn\":[%s]," // Friendly Names
+ "\"hn\":\"%s\"," // Host Name
+ "\"mac\":\"%s\"," // Full MAC as Device id
+ "\"md\":\"%s\"," // Module Name
+ "\"ofln\":\"" D_OFFLINE "\"," // Payload Offline
+ "\"onln\":\"" D_ONLINE "\"," // Payload Online
+ "\"state\":[\"%s\",\"%s\",\"%s\",\"%s\"]," // State text for "OFF","ON","TOGGLE","HOLD"
+ "\"sw\":\"%s\"," // Software Version
+ "\"t\":\"%s\"," // Topic
+ "\"ft\":\"%s\"," // Ful Topic
+ "\"tp\":[\"%s\",\"%s\",\"%s\"]," // Topics for command, stat and tele
+ "\"rl\":[%s],\"swc\":[%s],\"btn\":[%s]," // Inputs / Outputs
+ "\"so\":{\"11\":%d,\"13\":%d,\"17\":%d,\"20\":%d," // SetOptions
+ "\"30\":%d,\"37\":%d,\"68\":%d,\"73\":%d,\"80\":%d},"
+ "\"lt_st\":%d,\"ver\":1}"; // Light SubType, and Discovery version
+
+#define D_CMND_HADIS "HaDis"
+
+const char kHAssCommand[] PROGMEM = "|" // No prefix
+ D_CMND_HADIS;
+
+void (* const HAssCommand[])(void) PROGMEM = { &CmndHaDis };
+
+void CmndHaDis(void) { // New discovery manager. Stored on E98 in settings.h
+
+ uint8_t index = XdrvMailbox.index;
+ uint8_t payload1 = XdrvMailbox.payload;
+ if (XdrvMailbox.data_len > 0 && (XdrvMailbox.payload > 0 || XdrvMailbox.payload <= 2)) {
+ if (2 == XdrvMailbox.payload) { payload1 = 0; }
+ char scmnd[20];
+ if (Settings.flag.hass_discovery != payload1) {
+ snprintf_P(scmnd, sizeof(scmnd), PSTR(D_CMND_SETOPTION "19 %d"), payload1);
+ ExecuteCommand(scmnd, SRC_IGNORE);
+ }
+ Settings.hass_new_discovery = XdrvMailbox.payload;
+
+ }
+ Response_P(PSTR("{\"" D_CMND_HADIS "\":\"%d\"}"), Settings.hass_new_discovery);
+}
+
+void NewHAssDiscovery(void)
+{
+ char stopic[TOPSZ];
+ char stemp1[TOPSZ];
+ char stemp2[200];
+ char stemp3[TOPSZ];
+ char stemp4[TOPSZ];
+ char stemp5[TOPSZ];
+ char unique_id[30];
+ char *state_topic = stemp1;
+
+ stemp2[0] = '\0';
+ uint32_t maxfn = (devices_present > MAX_FRIENDLYNAMES) ? MAX_FRIENDLYNAMES : (!devices_present) ? 1 : devices_present;
+ for (uint32_t i = 0; i < MAX_FRIENDLYNAMES; i++) {
+ char fname[TOPSZ];
+ snprintf_P(fname, sizeof(fname), PSTR("\"%s\""), EscapeJSONString(SettingsText(SET_FRIENDLYNAME1 +i)).c_str());
+ snprintf_P(stemp2, sizeof(stemp2), PSTR("%s%s%s"), stemp2, (i > 0 ? "," : ""), (i < maxfn) ? fname : "null");
+ }
+
+ stemp3[0] = '\0';
+
+ uint32_t maxrl = (devices_present > MAX_RELAYS) ? MAX_RELAYS : (!devices_present) ? 1 : devices_present;
+
+ for (uint32_t i = 0; i < MAX_RELAYS; i++) {
+ snprintf_P(stemp3, sizeof(stemp3), PSTR("%s%s%d"), stemp3, (i > 0 ? "," : ""), (i < maxfn) ? (Settings.flag.hass_light ? 2 : 1) : 0);
+ }
+
+ stemp4[0] = '\0';
+ //stemp6[0] = '\0';
+ for (uint32_t i = 0; i < MAX_SWITCHES; i++) {
+ snprintf_P(stemp4, sizeof(stemp4), PSTR("%s%s%d"), stemp4, (i > 0 ? "," : ""), PinUsed(GPIO_SWT1, i) ? Settings.switchmode[i] : -1);
+ }
+ stemp5[0] = '\0';
+ for (uint32_t i = 0; i < MAX_KEYS; i++) {
+ snprintf_P(stemp5, sizeof(stemp5), PSTR("%s%s%d"), stemp5, (i > 0 ? "," : ""), PinUsed(GPIO_KEY1, i));
+ }
+
+ mqtt_data[0] = '\0'; // Clear retained message
+
+ // Full 12 chars MAC address as ID
+ String mac_address = WiFi.macAddress();
+ mac_address.replace(":", "");
+ String mac_part = mac_address.substring(0);
+ snprintf_P(unique_id, sizeof(unique_id), PSTR("%s"), mac_address.c_str());
+
+ snprintf_P(stopic, sizeof(stopic), PSTR(HOME_ASSISTANT_DISCOVERY_PREFIX "/discovery/%s/config"), unique_id);
+ GetTopic_P(state_topic, TELE, mqtt_topic, PSTR(D_RSLT_HASS_STATE));
+
+ Response_P(HASS_DISCOVER_DEVICE, WiFi.localIP().toString().c_str(), SettingsText(SET_DEVICENAME),
+ stemp2, my_hostname, unique_id, ModuleName().c_str(), GetStateText(0), GetStateText(1), GetStateText(2), GetStateText(3),
+ my_version, mqtt_topic, MQTT_FULLTOPIC, SUB_PREFIX, PUB_PREFIX, PUB_PREFIX2, stemp3, stemp4, stemp5, Settings.flag.button_swap,
+ Settings.flag.button_single, Settings.flag.decimal_text, Settings.flag.not_power_linked, Settings.flag.hass_light,
+ light_controller.isCTRGBLinked(), Settings.flag3.pwm_multi_channels, Settings.flag3.mqtt_buttons, Settings.flag3.shutter_mode, Light.subtype);
+ MqttPublish(stopic, true);
+
+}
+
+// NEW DISCOVERY
+
void TryResponseAppend_P(const char *format, ...)
{
va_list args;
@@ -364,7 +468,7 @@ void HAssAnnounceRelayLight(void)
}
}
MqttPublish(stopic, true);
- masterlog_level = 4;
+ masterlog_level = 4; // Restore WebLog state
}
}
@@ -421,7 +525,7 @@ void HAssAnnouncerTriggers(uint8_t device, uint8_t present, uint8_t key, uint8_t
}
}
MqttPublish(stopic, true);
- masterlog_level = 4;
+ masterlog_level = 4; // Restore WebLog state
}
}
@@ -472,7 +576,7 @@ void HAssAnnouncerBinSensors(uint8_t device, uint8_t present, uint8_t dual, uint
}
}
MqttPublish(stopic, true);
- masterlog_level = 4;
+ masterlog_level = 4; // Restore WebLog state
}
void HAssAnnounceSwitches(void)
@@ -784,7 +888,7 @@ void HAssAnnounceShutters(void)
}
MqttPublish(stopic, true);
- masterlog_level = 4;
+ masterlog_level = 4; // Restore WebLog state
}
#endif
}
@@ -823,7 +927,7 @@ void HAssAnnounceDeviceInfoAndStatusSensor(void)
MqttPublish(stopic, true);
if (!Settings.flag.hass_discovery) {
masterlog_level = 0;
- AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_LOG "Home Assistant Discovery disabled. "));
+ AddLog_P2(LOG_LEVEL_INFO, PSTR(D_LOG_LOG "Home Assistant Classic Discovery disabled."));
}
}
@@ -842,19 +946,21 @@ void HAssPublishStatus(void)
void HAssDiscovery(void)
{
// Configure Tasmota for default Home Assistant parameters to keep discovery message as short as possible
- if (Settings.flag.hass_discovery)
- { // SetOption19 - Control Home Assistant automatic discovery (See SetOption59)
- Settings.flag.mqtt_response = 0; // SetOption4 - Switch between MQTT RESULT or COMMAND - Response always as RESULT and not as uppercase command
- Settings.flag.decimal_text = 1; // SetOption17 - Switch between decimal or hexadecimal output - Respond with decimal color values
- Settings.flag3.hass_tele_on_power = 1; // SetOption59 - Send tele/%topic%/STATE in addition to stat/%topic%/RESULT - send tele/STATE message as stat/RESULT
- // the purpose of that is so that if HA is restarted, state in HA will be correct within one teleperiod otherwise state
- // will not be correct until the device state is changed this is why in the patterns for switch and light, we tell HA to trigger on STATE, not RESULT.
- //Settings.light_scheme = 0; // To just control color it needs to be Scheme 0 (on hold due to new light configuration)
+ if (Settings.flag.hass_discovery || Settings.hass_new_discovery > 0)
+ { // SetOption19 - Control Home Assistant automatic discovery (See SetOption59)
+ Settings.flag.mqtt_response = 0; // SetOption4 - Switch between MQTT RESULT or COMMAND - Response always as RESULT and not as uppercase command
+ Settings.flag.decimal_text = 1; // SetOption17 - Switch between decimal or hexadecimal output - Respond with decimal color values
+ if (Settings.hass_new_discovery == 1) { // Official Home Assistant Discovery doesn't need the /STATE topic.
+ Settings.flag3.hass_tele_on_power = 1; // SetOption59 - Send tele/%topic%/STATE in addition to stat/%topic%/RESULT - send tele/STATE message as stat/RESULT
+ // the purpose of that is so that if HA is restarted, state in HA will be correct within one teleperiod otherwise state
+ // will not be correct until the device state is changed this is why in the patterns for switch and light, we tell HA to trigger on STATE, not RESULT.
+ }
+ //Settings.light_scheme = 0; // To just control color it needs to be Scheme 0 (on hold due to new light configuration)
}
if (Settings.flag.hass_discovery || (1 == hass_mode))
{ // SetOption19 - Control Home Assistantautomatic discovery (See SetOption59)
- masterlog_level = 4;
+ masterlog_level = 4; // Restore WebLog state
// Send info about buttons
HAssAnnounceButtons();
@@ -881,11 +987,12 @@ void HAssDiscover(void)
{
hass_mode = 1; // Force discovery
hass_init_step = 1; // Delayed discovery
+ Settings.hass_new_discovery = Settings.flag.hass_discovery;
}
void HAssAnyKey(void)
{
- if (!Settings.flag.hass_discovery)
+ if (!Settings.flag.hass_discovery || Settings.hass_new_discovery == 0)
{
return;
} // SetOption19 - Control Home Assistantautomatic discovery (See SetOption59)
@@ -940,7 +1047,7 @@ void HassLwtSubscribe(bool hasslwt)
{
char htopic[TOPSZ];
snprintf_P(htopic, sizeof(htopic), PSTR(HOME_ASSISTANT_LWT_TOPIC));
- if (hasslwt && Settings.flag.hass_discovery) {
+ if (hasslwt && (Settings.flag.hass_discovery || Settings.hass_new_discovery == 2)) {
MqttSubscribe(htopic);
} else { MqttUnsubscribe(htopic); }
}
@@ -983,6 +1090,9 @@ bool Xdrv12(uint8_t function)
case FUNC_MQTT_INIT:
hass_mode = 0; // Discovery only if Settings.flag.hass_discovery is set
hass_init_step = 2; // Delayed discovery
+ if (!Settings.flag.hass_discovery && Settings.hass_new_discovery == 0) {
+ NewHAssDiscovery();
+ }
break;
case FUNC_MQTT_SUBSCRIBE:
@@ -991,6 +1101,9 @@ bool Xdrv12(uint8_t function)
case FUNC_MQTT_DATA:
result = HAssMqttLWT();
break;
+ case FUNC_COMMAND:
+ result = DecodeCommand(kHAssCommand, HAssCommand);
+ break;
}
}
return result;
From 4e84e331039c646ef973d8cf168da1bc69617abe Mon Sep 17 00:00:00 2001
From: Federico Leoni
Date: Fri, 18 Sep 2020 18:45:58 -0300
Subject: [PATCH 15/92] Prep for new Hass Discovery
---
tasmota/settings.h | 5 +++--
tasmota/settings.ino | 4 ++--
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/tasmota/settings.h b/tasmota/settings.h
index 0803bcca2..0ce6b9f57 100644
--- a/tasmota/settings.h
+++ b/tasmota/settings.h
@@ -557,7 +557,7 @@ struct {
uint16_t dimmer_hw_min; // E90
uint16_t dimmer_hw_max; // E92
uint32_t deepsleep; // E94
- uint16_t ex2_energy_power_delta; // E98 - Free since 8.4.0.3
+ uint16_t hass_new_discovery; // E98 - ex2_energy_power_delta on 8.4.0.3, replaced on 8.5.0.1
uint8_t shutter_motordelay[MAX_SHUTTERS]; // E9A
int8_t temp_comp; // E9E
uint8_t weight_change; // E9F
@@ -613,7 +613,8 @@ struct {
uint16_t energy_power_delta[3]; // F44
uint16_t shutter_pwmrange[2][MAX_SHUTTERS]; // F4A
- uint8_t free_f5a[90]; // F5A - Decrement if adding new Setting variables just above and below
+
+ uint8_t free_f5a[89]; // F5A - Decrement if adding new Setting variables just above and below
// Only 32 bit boundary variables below
SysBitfield5 flag5; // FB4
diff --git a/tasmota/settings.ino b/tasmota/settings.ino
index 2c17cbad2..ddc5dfc2d 100644
--- a/tasmota/settings.ino
+++ b/tasmota/settings.ino
@@ -1356,7 +1356,7 @@ void SettingsDelta(void)
Settings.ex_sbaudrate = 0;
*/
Settings.flag3.fast_power_cycle_disable = 0;
- Settings.ex2_energy_power_delta = Settings.tuyamcu_topic;
+ Settings.hass_new_discovery = Settings.tuyamcu_topic; // replaced ex2_energy_power_delta on 8.5.0.1
Settings.tuyamcu_topic = 0; // replaced ex_energy_power_delta on 8.5.0.1
}
if (Settings.version < 0x06060015) {
@@ -1514,7 +1514,7 @@ void SettingsDelta(void)
Settings.fallback_module = FALLBACK_MODULE;
}
if (Settings.version < 0x08040003) {
- Settings.energy_power_delta[0] = Settings.ex2_energy_power_delta;
+ Settings.energy_power_delta[0] = Settings.hass_new_discovery; // replaced ex2_energy_power_delta on 8.5.0.1
Settings.energy_power_delta[1] = 0;
Settings.energy_power_delta[2] = 0;
}
From ee475e833a2ed02d60e734e0c40743140692663b Mon Sep 17 00:00:00 2001
From: Jason2866 <24528715+Jason2866@users.noreply.github.com>
Date: Sat, 19 Sep 2020 12:30:03 +0200
Subject: [PATCH 16/92] Remove not needed, add stage core32
---
platformio_override_sample.ini | 25 +++++++++++++++----------
platformio_tasmota32.ini | 6 +++++-
2 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini
index 188e05e7e..dde700a23 100644
--- a/platformio_override_sample.ini
+++ b/platformio_override_sample.ini
@@ -36,7 +36,6 @@ default_envs =
[common]
-platform = ${core.platform}
platform_packages = ${core.platform_packages}
build_unflags = ${core.build_unflags}
build_flags = ${core.build_flags}
@@ -74,20 +73,17 @@ extra_scripts = ${scripts_defaults.extra_scripts}
[core]
; Activate only (one set) if you want to override the standard core defined in platformio.ini !!!
-;platform = ${tasmota_stage.platform}
;platform_packages = ${tasmota_stage.platform_packages}
;build_unflags = ${tasmota_stage.build_unflags}
;build_flags = ${tasmota_stage.build_flags}
-;platform = ${core_stage.platform}
-;platform_packages = ${core_stage.platform_packages}
-;build_unflags = ${core_stage.build_unflags}
-;build_flags = ${core_stage.build_flags}
+platform_packages = ${core_stage.platform_packages}
+build_unflags = ${core_stage.build_unflags}
+build_flags = ${core_stage.build_flags}
[tasmota_stage]
; *** Esp8266 core for Arduino version Tasmota stage
-platform = espressif8266@2.6.2
platform_packages = framework-arduinoespressif8266@https://github.com/Jason2866/Arduino.git#2.7.4.2
build_unflags = ${esp_defaults.build_unflags}
build_flags = ${esp82xx_defaults.build_flags}
@@ -123,9 +119,8 @@ build_flags = ${esp82xx_defaults.build_flags}
[core_stage]
; *** Esp8266 core version. Tasmota stage or Arduino stage version. Built with GCC 10.1 toolchain
-platform = espressif8266@2.6.2
-platform_packages = framework-arduinoespressif8266 @ https://github.com/Jason2866/platform-espressif8266/releases/download/2.9.0/framework-arduinoespressif8266-3.20900.0.tar.gz
- ;framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git
+platform_packages = ;framework-arduinoespressif8266 @ https://github.com/Jason2866/platform-espressif8266/releases/download/2.9.0/framework-arduinoespressif8266-3.20900.0.tar.gz
+ framework-arduinoespressif8266 @ https://github.com/esp8266/Arduino.git
toolchain-xtensa @ ~2.100100.0
build_unflags = ${esp_defaults.build_unflags}
-Wswitch-unreachable
@@ -160,6 +155,16 @@ build_flags = ${esp82xx_defaults.build_flags}
; -lstdc++-exc
+[core32]
+; Activate Stage Core32 by removing ";" in next line, if you want to override the standard core32
+;platform_packages = ${core32_stage.platform_packages}
+
+
+[core32_stage]
+platform_packages = tool-esptoolpy@1.20800.0
+ arduino-esp32@https://github.com/espressif/arduino-esp32.git#esp32s2
+
+
; *** Debug version used for PlatformIO Home Project Inspection
[env:tasmota-debug]
build_type = debug
diff --git a/platformio_tasmota32.ini b/platformio_tasmota32.ini
index 739ec2338..bdd07be81 100644
--- a/platformio_tasmota32.ini
+++ b/platformio_tasmota32.ini
@@ -39,9 +39,13 @@ default_envs = ${build_envs.default_envs}
; tasmota32-UK
-[common32]
+[core32]
platform = espressif32@2.0.0
platform_packages = tool-esptoolpy@1.20800.0
+
+[common32]
+platform = ${core32.platform}
+platform_packages = ${core32.platform_packages}
board = esp32dev
board_build.ldscript = esp32_out.ld
board_build.partitions = esp32_partition_app1984k_spiffs64k.csv
From ae2e54d97b90430df19dacaf4cb627db6f7edbfe Mon Sep 17 00:00:00 2001
From: Federico Leoni
Date: Sat, 19 Sep 2020 08:26:29 -0300
Subject: [PATCH 17/92] TuyaMCU update
---
tasmota/xdrv_16_tuyamcu.ino | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/tasmota/xdrv_16_tuyamcu.ino b/tasmota/xdrv_16_tuyamcu.ino
index 2c4b9e0d6..c58dc2130 100644
--- a/tasmota/xdrv_16_tuyamcu.ino
+++ b/tasmota/xdrv_16_tuyamcu.ino
@@ -444,7 +444,6 @@ bool TuyaSetChannels(void)
if (LT_RGB != light_type) {
for (uint8_t i = 0; i <= idx; i++) {
-
if (Tuya.Snapshot[i] != Tuya.Levels[i]) {
if (i == 0 && LightMode && Tuya.ModeSet ) { noupd = true;}
if (!noupd) {
@@ -457,6 +456,7 @@ bool TuyaSetChannels(void)
}
if (light_type >= LT_RGB) {
+
light_state.getHSB(&hue, &sat, &bri);
sat = changeUIntScale(sat, 0, 255, 0, 100);
bri = changeUIntScale(bri, 0, 255, 0, 100);
@@ -1114,6 +1114,7 @@ bool Xdrv16(uint8_t function)
result = TuyaButtonPressed();
break;
case FUNC_EVERY_SECOND:
+ TuyaSetChannels();
if (TuyaSerial && Tuya.wifi_state != TuyaGetTuyaWifiState()) { TuyaSetWifiLed(); }
if (!Tuya.low_power_mode) {
Tuya.heartbeat_timer++;
@@ -1131,9 +1132,9 @@ bool Xdrv16(uint8_t function)
}
if (Tuya.ignore_topic_timeout < millis()) { Tuya.SuspendTopic = false; }
break;
- case FUNC_SET_CHANNELS:
- result = TuyaSetChannels();
- break;
+ // case FUNC_SET_CHANNELS:
+ // result = TuyaSetChannels();
+ // break;
case FUNC_COMMAND:
result = DecodeCommand(kTuyaCommand, TuyaCommand);
break;
From a9af3baea0c927a5131d332688f0c41338469e09 Mon Sep 17 00:00:00 2001
From: nicandris
Date: Sat, 19 Sep 2020 13:43:14 +0200
Subject: [PATCH 18/92] Added SetOption112 - Use friendly name in zigbee topic
(use with SetOption89)
---
tasmota/settings.h | 2 +-
tasmota/xdrv_23_zigbee_2_devices.ino | 12 +++++++++---
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/tasmota/settings.h b/tasmota/settings.h
index 0ce6b9f57..83f6a1a17 100644
--- a/tasmota/settings.h
+++ b/tasmota/settings.h
@@ -131,7 +131,7 @@ typedef union { // Restricted by MISRA-C Rule 18.4 bu
uint32_t alexa_gen_1 : 1; // bit 27 (v8.4.0.3) - SetOption109 - Alexa gen1 mode - if you only have Echo Dot 2nd gen devices
uint32_t zb_disable_autobind : 1; // bit 28 (v8.5.0.1) - SetOption110 - disable Zigbee auto-config when pairing new devices
uint32_t buzzer_freq_mode : 1; // bit 29 (v8.5.0.1) - SetOption111 - Use frequency output for buzzer pin instead of on/off signal
- uint32_t spare30 : 1; // bit 30
+ uint32_t zb_topic_fname : 1; // bit 30 (v8.5.0.1) - SetOption112 - Use friendly name in zigbee topic (use with SetOption89)
uint32_t spare31 : 1; // bit 31
};
} SysBitfield4;
diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino
index 82000b2e1..a5a3ab2f9 100644
--- a/tasmota/xdrv_23_zigbee_2_devices.ino
+++ b/tasmota/xdrv_23_zigbee_2_devices.ino
@@ -896,9 +896,15 @@ void Z_Devices::jsonPublishFlush(uint16_t shortaddr) {
attr_list.reset(); // clear the attributes
if (Settings.flag4.zigbee_distinct_topics) {
- char subtopic[16];
- snprintf_P(subtopic, sizeof(subtopic), PSTR("%04X/" D_RSLT_SENSOR), shortaddr);
- MqttPublishPrefixTopic_P(TELE, subtopic, Settings.flag.mqtt_sensor_retain);
+ if (Settings.flag4.zb_topic_fname && fname) {
+ char frtopic[13];
+ snprintf_P(frtopic, sizeof(frtopic) + strlen(fname), PSTR("tele/%s/" D_RSLT_SENSOR), fname);
+ MqttPublish(frtopic, Settings.flag.mqtt_sensor_retain);
+ } else {
+ char subtopic[16];
+ snprintf_P(subtopic, sizeof(subtopic), PSTR("%04X/" D_RSLT_SENSOR), shortaddr);
+ MqttPublishPrefixTopic_P(TELE, subtopic, Settings.flag.mqtt_sensor_retain);
+ }
} else {
MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain);
}
From 7adab74ed57c9bc847f16b82652325b7a775c3a6 Mon Sep 17 00:00:00 2001
From: nicandris
Date: Sat, 19 Sep 2020 14:02:15 +0200
Subject: [PATCH 19/92] Fix possible buffer overflow
---
tasmota/xdrv_23_zigbee_2_devices.ino | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino
index a5a3ab2f9..26667786a 100644
--- a/tasmota/xdrv_23_zigbee_2_devices.ino
+++ b/tasmota/xdrv_23_zigbee_2_devices.ino
@@ -897,8 +897,8 @@ void Z_Devices::jsonPublishFlush(uint16_t shortaddr) {
if (Settings.flag4.zigbee_distinct_topics) {
if (Settings.flag4.zb_topic_fname && fname) {
- char frtopic[13];
- snprintf_P(frtopic, sizeof(frtopic) + strlen(fname), PSTR("tele/%s/" D_RSLT_SENSOR), fname);
+ char frtopic[13 + strlen(fname)];
+ snprintf_P(frtopic, sizeof(frtopic), PSTR("tele/%s/" D_RSLT_SENSOR), fname);
MqttPublish(frtopic, Settings.flag.mqtt_sensor_retain);
} else {
char subtopic[16];
From 0fce283532fefd1860f438ac4ac16c4e2dfef416 Mon Sep 17 00:00:00 2001
From: nicandris
Date: Sat, 19 Sep 2020 14:34:16 +0200
Subject: [PATCH 20/92] included prefix3 in topic
---
tasmota/xdrv_23_zigbee_2_devices.ino | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino
index 26667786a..6951f626b 100644
--- a/tasmota/xdrv_23_zigbee_2_devices.ino
+++ b/tasmota/xdrv_23_zigbee_2_devices.ino
@@ -898,7 +898,7 @@ void Z_Devices::jsonPublishFlush(uint16_t shortaddr) {
if (Settings.flag4.zigbee_distinct_topics) {
if (Settings.flag4.zb_topic_fname && fname) {
char frtopic[13 + strlen(fname)];
- snprintf_P(frtopic, sizeof(frtopic), PSTR("tele/%s/" D_RSLT_SENSOR), fname);
+ snprintf_P(frtopic, sizeof(frtopic), PSTR("%s/%s/" D_RSLT_SENSOR), SettingsText(SET_MQTTPREFIX3), fname);
MqttPublish(frtopic, Settings.flag.mqtt_sensor_retain);
} else {
char subtopic[16];
From 03da44b2b4ea346b845f15e6516fdcc2b7981038 Mon Sep 17 00:00:00 2001
From: nicandris
Date: Sat, 19 Sep 2020 15:20:17 +0200
Subject: [PATCH 21/92] Clean up of friendly name before setting it to topic
---
tasmota/xdrv_23_zigbee_2_devices.ino | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino
index 6951f626b..f8467afd9 100644
--- a/tasmota/xdrv_23_zigbee_2_devices.ino
+++ b/tasmota/xdrv_23_zigbee_2_devices.ino
@@ -897,8 +897,13 @@ void Z_Devices::jsonPublishFlush(uint16_t shortaddr) {
if (Settings.flag4.zigbee_distinct_topics) {
if (Settings.flag4.zb_topic_fname && fname) {
- char frtopic[13 + strlen(fname)];
- snprintf_P(frtopic, sizeof(frtopic), PSTR("%s/%s/" D_RSLT_SENSOR), SettingsText(SET_MQTTPREFIX3), fname);
+ //Clean special characters and check size of friendly name
+ char stemp[TOPSZ];
+ strlcpy(stemp, (!strlen(fname)) ? MQTT_TOPIC : fname, sizeof(stemp));
+ MakeValidMqtt(0, stemp);
+ //Create topic with Prefix3 and cleaned up friendly name
+ char frtopic[13 + strlen(stemp)];
+ snprintf_P(frtopic, sizeof(frtopic), PSTR("%s/%s/" D_RSLT_SENSOR), SettingsText(SET_MQTTPREFIX3), stemp);
MqttPublish(frtopic, Settings.flag.mqtt_sensor_retain);
} else {
char subtopic[16];
From 8a3f3b271a079ac071f0bba2f9a29502f3364d6e Mon Sep 17 00:00:00 2001
From: nicandris
Date: Sat, 19 Sep 2020 16:02:19 +0200
Subject: [PATCH 22/92] use TOPSZ for size of topic
---
tasmota/xdrv_23_zigbee_2_devices.ino | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tasmota/xdrv_23_zigbee_2_devices.ino b/tasmota/xdrv_23_zigbee_2_devices.ino
index f8467afd9..fa7775d03 100644
--- a/tasmota/xdrv_23_zigbee_2_devices.ino
+++ b/tasmota/xdrv_23_zigbee_2_devices.ino
@@ -902,7 +902,7 @@ void Z_Devices::jsonPublishFlush(uint16_t shortaddr) {
strlcpy(stemp, (!strlen(fname)) ? MQTT_TOPIC : fname, sizeof(stemp));
MakeValidMqtt(0, stemp);
//Create topic with Prefix3 and cleaned up friendly name
- char frtopic[13 + strlen(stemp)];
+ char frtopic[TOPSZ];
snprintf_P(frtopic, sizeof(frtopic), PSTR("%s/%s/" D_RSLT_SENSOR), SettingsText(SET_MQTTPREFIX3), stemp);
MqttPublish(frtopic, Settings.flag.mqtt_sensor_retain);
} else {
From f6ef721e814e1579f0a242b2918e57f55357915e Mon Sep 17 00:00:00 2001
From: Jason2866 <24528715+Jason2866@users.noreply.github.com>
Date: Sun, 20 Sep 2020 18:01:14 +0200
Subject: [PATCH 23/92] Use specific commit for ESP32 stage
because with later ones Tasmota32 does not compile.
---
platformio_override_sample.ini | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/platformio_override_sample.ini b/platformio_override_sample.ini
index dde700a23..444a5097c 100644
--- a/platformio_override_sample.ini
+++ b/platformio_override_sample.ini
@@ -162,7 +162,8 @@ build_flags = ${esp82xx_defaults.build_flags}
[core32_stage]
platform_packages = tool-esptoolpy@1.20800.0
- arduino-esp32@https://github.com/espressif/arduino-esp32.git#esp32s2
+ ; latest working commit
+ framework-arduinoespressif32 @ https://github.com/espressif/arduino-esp32.git#c09ec5bd3d35ba7dfc135755ab300e2b45416def
; *** Debug version used for PlatformIO Home Project Inspection
From 20e8a4cfc542ae7fa7ace47c13b25faa92d41715 Mon Sep 17 00:00:00 2001
From: Federico Leoni
Date: Sun, 20 Sep 2020 22:29:02 -0300
Subject: [PATCH 24/92] MAC address VAR for rules
---
tasmota/xdrv_10_rules.ino | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/tasmota/xdrv_10_rules.ino b/tasmota/xdrv_10_rules.ino
index c377c4a91..4476fc2b0 100644
--- a/tasmota/xdrv_10_rules.ino
+++ b/tasmota/xdrv_10_rules.ino
@@ -719,9 +719,13 @@ bool RuleSetProcess(uint8_t rule_set, String &event_saved)
RulesVarReplace(commands, F("%UPTIME%"), String(MinutesUptime()));
RulesVarReplace(commands, F("%TIMESTAMP%"), GetDateAndTime(DT_LOCAL));
RulesVarReplace(commands, F("%TOPIC%"), mqtt_topic);
- char unique_id[7];
- snprintf_P(unique_id, sizeof(unique_id), PSTR("%06X"), ESP_getChipId());
- RulesVarReplace(commands, F("%DEVICEID%"), unique_id);
+ snprintf_P(stemp, sizeof(stemp), PSTR("%06X"), ESP_getChipId());
+ RulesVarReplace(commands, F("%DEVICEID%"), stemp);
+ char macaddr[13];
+ String mac_address = WiFi.macAddress();
+ mac_address.replace(":", "");
+ snprintf_P(macaddr, sizeof(macaddr), PSTR("%s"), mac_address.c_str());
+ RulesVarReplace(commands, F("%MACADDR%"), macaddr);
#if defined(USE_TIMERS) && defined(USE_SUNRISE)
RulesVarReplace(commands, F("%SUNRISE%"), String(SunMinutes(0)));
RulesVarReplace(commands, F("%SUNSET%"), String(SunMinutes(1)));
From 2ff756b3cc76982796fce5583efdfea7f8cd36ff Mon Sep 17 00:00:00 2001
From: Stephan Hadinger
Date: Mon, 21 Sep 2020 21:49:32 +0200
Subject: [PATCH 25/92] Change replace ArduinoJson with JSMN for JSON parsing
---
lib/jsmn-shadinger-1.0/README.md | 181 ++++++
lib/jsmn-shadinger-1.0/library.properties | 8 +
lib/jsmn-shadinger-1.0/src/JsonParser.cpp | 524 ++++++++++++++++++
lib/jsmn-shadinger-1.0/src/JsonParser.h | 260 +++++++++
lib/jsmn-shadinger-1.0/src/JsonParser.hpp.gch | Bin 0 -> 2299028 bytes
lib/jsmn-shadinger-1.0/src/jsmn.cpp | 449 +++++++++++++++
lib/jsmn-shadinger-1.0/src/jsmn.h | 118 ++++
lib/jsmn-shadinger-1.0/test/test-json.cpp | 55 ++
tasmota/CHANGELOG.md | 1 +
tasmota/support.ino | 44 +-
tasmota/support_json.ino | 2 +
tasmota/xdrv_01_webserver.ino | 18 +
tasmota/xdrv_05_irremote.ino | 14 +
tasmota/xdrv_05_irremote_full.ino | 85 ++-
tasmota/xdrv_09_timers.ino | 90 +++
tasmota/xdrv_10_rules.ino | 60 +-
tasmota/xdrv_20_hue.ino | 65 ++-
tasmota/xdrv_23_zigbee_1_headers.ino | 36 --
tasmota/xdrv_23_zigbee_2_devices.ino | 58 +-
tasmota/xdrv_23_zigbee_3_hue.ino | 54 +-
tasmota/xdrv_23_zigbee_A_impl.ino | 311 +++++------
21 files changed, 2077 insertions(+), 356 deletions(-)
create mode 100644 lib/jsmn-shadinger-1.0/README.md
create mode 100644 lib/jsmn-shadinger-1.0/library.properties
create mode 100644 lib/jsmn-shadinger-1.0/src/JsonParser.cpp
create mode 100644 lib/jsmn-shadinger-1.0/src/JsonParser.h
create mode 100644 lib/jsmn-shadinger-1.0/src/JsonParser.hpp.gch
create mode 100644 lib/jsmn-shadinger-1.0/src/jsmn.cpp
create mode 100644 lib/jsmn-shadinger-1.0/src/jsmn.h
create mode 100644 lib/jsmn-shadinger-1.0/test/test-json.cpp
diff --git a/lib/jsmn-shadinger-1.0/README.md b/lib/jsmn-shadinger-1.0/README.md
new file mode 100644
index 000000000..f98503eb1
--- /dev/null
+++ b/lib/jsmn-shadinger-1.0/README.md
@@ -0,0 +1,181 @@
+# JSMN lightweight JSON parser for Tasmota
+
+Intro:
+
+## Benefits
+
+## How to use
+
+`{"Device":"0x1234","Power":true,"Temperature":25.5}`
+
+The JSON above is split into 8 tokens, and requires 32 bytes of dynamic memory.
+
+- Token 0: `OBJECT`, size=3: `{"Device":"0x1234","Power":true,"Temperature":25.5}`
+- Token 1: `KEY`: `Device`
+- Token 2: `STRING`: `0x1234`
+- Token 3: `KEY`: `Power`
+- Token 4: `BOOL_TRUE`: `true`
+- Token 5: `KEY`: `Temperature`
+- Token 6: `FLOAT`: `25.5`
+- Token 7: `INVALID`
+
+If what you need is to parse a JSON Object for values with default values:
+```
+#include "JsonParser.h"
+
+char json_buffer[] = "{\"Device\":\"0x1234\",\"Power\":true,\"Temperature\":25.6}";
+JsonParserObject root = JsonParser(json_buffer).getRootObject();
+if (!root) { ResponseCmndChar_P(PSTR("Invalid JSON")); return; }
+
+uint16_t d = root.getUInt(PSTR("DEVICE"), 0xFFFF); // case insensitive
+bool b = root.getBool(PSTR("Power"), false);
+float f = root.getFloat(PSTR("Temperature), -100);
+```
+
+Alternative pattern, if you want to test the existence of the attribute first:
+```
+#include "JsonParser.h"
+
+char json_buffer[] = "{\"Device\":\"0x1234\",\"Power\":true,\"Temperature\":25.6}";
+JsonParserObject root = JsonParser(json_buffer).getRootObject();
+if (!root) { ResponseCmndChar_P(PSTR("Invalid JSON")); return; }
+
+JsonParserToken val = root[PSTR("DEVICE")];
+if (val) {
+ d = val.getUInt();
+}
+val = root[PSTR("Power")];
+if (val) {
+ b = val.getBool();
+}
+val = root[PSTR("Temperature)];
+if (val) {
+ f = val.getFloat();
+}
+```
+
+## Types and conversion
+
+JSMN relies on the concept of JSON Tokens `JsonParserToken`. Tokens do not hold any data, but point to token boundaries in JSON string instead. Every jsmn token has a type, which indicates the type of corresponding JSON token. JSMN for Tasmota extends the type from JSMN to ease further parsing.
+
+Types are:
+- `INVALID` invalid token or end of stream, see Error Handling below
+- `OBJECT` a JSON sub-object, `size()` contains the number of key/values of the object
+- `ARRAY` a JSON array, `size()` contains the number of sub values
+- `STRING` a JSON string, return the sub-string, unescaped, without surrounding quotes. UTF-8 is supported.
+- `PRIMITIVE` an unrecognized JSON token, consider as an error
+- `KEY` a JSON key in an object as a string
+- `NULL` a JSON `null` value, automatically converted to `0` or `""`
+- `BOOL_FALSE` a JSON `false` value, automatically converted to `0` or `""`
+- `BOOL_TRUE` a JSON `true` value, automatically converted to `1` or `""`
+- `UINT` a JSON unsigned int
+- `INT` a JSON negative int
+- `FLOAT` a JSON floating point value, i.e. a numerical value containing a decimal ot `.`
+
+Note: values are converted in this priority: 1/ `UINT`, 2/ `INT`, 3/ `FLOAT`.
+
+`JsonParserToken` support the following getters:
+- `getBool()`: returns true if 'true' or non-zero int (default false)
+- `getUInt()`: converts to unsigned int (default 0), boolean true is 1
+- `getInt()`: converts to signed int (default 0), boolean true is 1
+- `getULong()`: converts to unsigned 64 bits (default 0), boolean is 1
+- `getStr()`: converts to string (default "")
+
+There are variants of the same function for which you can choose the default values. Remember that using a getter if the token type is INVALID returns the default value.
+
+Conversion to `OBJECT` or `ARRAY`:
+- `getObject()`: converts token to `OBJECT` or `INVALID`
+- `getArray()`: converts token to `ARRAY` or `INVALID`
+
+For `JsonParserKey`:
+- `getValue()`: returns the value token associated to the key
+
+## Checking Token types
+
+Type checking for `JsonParserToken`:
+- `isSingleToken()` returns `true` for a single level token, `false` for `OBJECT`, `ARRAY` and `INVALID`
+- `isKey()` returns `true` for a `KEY` within an `OBJECT`
+- `isStr()` returns `true` for `STRING` (note: other types can still be read as strings with `getStr()`)
+- `isNull()` returns `true` for `NULL`
+- `isBool()` returns `true` for `BOOL_FALSE` or `BOOL_TRUE`
+- `isUInt()` returns `true` for `UINT` (see below for number conversions)
+- `isInt()` returns `true` for `INT` (see below for number conversions)
+- `isFloat()` returns `true` for `FLOAT` (see below for number conversions)
+- `isNum()` returns `true` for any numerical value, i.e. `UINT`, `INT` or `FLOAT`
+- `isObject()` returns `true` for `OBJECT`
+- `isArray()` returns `true` for `ARRAY`
+- `isValid()` returns `true`for any type except `INVALID`
+
+JSMN for Tasmota provides sub-classes:
+- `JsonParserKey` of type `KEY` or `INVALID`, used as a return value for Object iterators
+- `JsonParserObject` of type `OBJECT` or `INVALID`, providing iterators
+- `JsonParserArray` of type `ARRAY` or `INVALID`, providing iterators
+
+Converting from Token to Object or Array is done with `token.getObject()` or `token.getArray()`. If the conversion is invalid, the resulting object has type `INVALID` (see Error Handling).
+
+## Iterators and accessors for Objects and Arrays
+
+The `JsonParserObject` provides an easy to use iterator:
+```
+JsonParserToken obj = <...>
+for (auto key : obj) {
+ // key is of type JsonParserKey
+ JsonParserToken valie = key.getValue(); // retrieve the value associated to key
+}
+```
+
+If the object contains only one element, you can retrieve it with `obj.getFirstElement()`.
+
+You can access any key with `obj["Key"]`. Note: the search is on purpose **case insensitive** as it is the norm in Tasmota. The search string can be in PROGMEM. If the token is not found, it is of type `INVALID`.
+
+The `JsonParserArray` provides an easy to use iterator:
+```
+JsonParserArray arr = <...>
+for (auto elt : arr) {
+ // elt is of type JsonParserToken
+}
+```
+
+You can access any element in the array with the `[]` operator. Ex: `arr[0]` fof first element. If the index is invalid, the token has type `INVALID`.
+
+## Memory
+
+The `JsonParserToken` fits in 32 bits, so it can be freely returned or copied without any penalty of object copying. Hence it doesn't need the `const` modifier either, since it is always passed by value.
+
+## Error handling
+
+This library uses a `zero error` pattern. This means that calls never report any error nor throw exceptions. If something wrong happens (bad JSON, token not found...), function return an **Invalid Token**. You can call any function on an Invalid Token, they will always return the same Invalid Token (aka fixed point).
+
+You can easily test if the current token is invalid with the following:
+
+Short version:
+```
+if (token) { /* token is valid */ }
+```
+
+Long version:
+```
+if (token->isValiid()) { /* token is valid */ }
+```
+
+This pattern allows to cascade calls and check only the final result:
+```
+char json_buffer[] = "";
+JsonParserObject json = JsonParser(json_buffer).getRootObject();
+JsonParserToken zbstatus_tok = json["ZbStatus"];
+JsonParserObject zbstatus = zbstatus_tok.getObject();
+if (zbstatus) { /* do some stuff */
+ // reaching this point means: JSON is valid, there is a root object, there is a `ZbStatus` key and it contains a sub-object
+}
+```
+
+Warning: there is an explicit convert to `bool` to allow the short version. Be careful, `(bool)token` is equivalent to `token->isValid()`, it is **NOT** equivalent to `token->getBool()`.
+
+## Limits
+
+Please keep in mind the current limits for this library:
+- Maximum JSON buffer size 2047 bytes
+- Maximum 63 JSON tokens
+- No support for exponent in floats (i.e. `1.0e3` is invalid)
+
+These limits shouldn't be a problem since buffers in Tasmota are limited to 1.2KB. The support for exponent in floats is commented out and can be added if needed (slight increase in Flash size)
\ No newline at end of file
diff --git a/lib/jsmn-shadinger-1.0/library.properties b/lib/jsmn-shadinger-1.0/library.properties
new file mode 100644
index 000000000..674aa76e7
--- /dev/null
+++ b/lib/jsmn-shadinger-1.0/library.properties
@@ -0,0 +1,8 @@
+name=JSMN JSON parser customized and optimized for ESP8266 and Tasmota
+version=1.0
+author=Serge Zaitsev, Stephan Hadinger
+maintainer=Stephan
+sentence=Lightweight in-place JSON parser
+paragraph=
+url=https://github.com/zserge/jsmn
+architectures=esp8266
diff --git a/lib/jsmn-shadinger-1.0/src/JsonParser.cpp b/lib/jsmn-shadinger-1.0/src/JsonParser.cpp
new file mode 100644
index 000000000..f2ae50a97
--- /dev/null
+++ b/lib/jsmn-shadinger-1.0/src/JsonParser.cpp
@@ -0,0 +1,524 @@
+/*
+ JsonParser.h - lightweight JSON parser
+
+ Copyright (C) 2020 Stephan Hadinger
+
+ 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 .
+*/
+
+#include "JsonParser.h"
+#include "WSTring.h"
+
+/*********************************************************************************************\
+ * Utilities
+\*********************************************************************************************/
+
+const char * k_current_json_buffer = "";
+
+/*********************************************************************************************\
+ * Lightweight String to Float, because atof() or strtof() takes 10KB
+ *
+ * To remove code, exponents are not parsed
+ * (commented out below, just in case we need them after all)
+\*********************************************************************************************/
+// Inspired from https://searchcode.com/codesearch/view/22115068/
+float json_strtof(const char* s) {
+ const char* p = s;
+ float value = 0.;
+ int32_t sign = +1;
+ float factor;
+ // unsigned int expo;
+
+ while (isspace(*p)){ // skip any leading white-spaces
+ p++;
+ }
+
+ switch (*p) {
+ case '-': sign = -1;
+ case '+': p++;
+ default : break;
+ }
+
+ while ((unsigned int)(*p - '0') < 10u) {
+ value = value*10 + (*p++ - '0');
+ }
+
+ if (*p == '.' ) {
+ factor = 1.0f;
+
+ p++;
+ while ((unsigned int)(*p - '0') < 10u) {
+ factor *= 0.1f;
+ value += (*p++ - '0') * factor;
+ }
+ }
+
+// if ( (*p | 32) == 'e' ) {
+// expo = 0;
+// factor = 10.L;
+
+// switch (*++p) { // ja hier weiß ich nicht, was mindestens nach einem 'E' folgenden MUSS.
+// case '-': factor = 0.1;
+// case '+': p++;
+// break;
+// case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9':
+// break;
+// default : value = 0.L;
+// p = s;
+// goto done;
+// }
+
+// while ( (unsigned int)(*p - '0') < 10u )
+// expo = 10 * expo + (*p++ - '0');
+
+// while ( 1 ) {
+// if ( expo & 1 )
+// value *= factor;
+// if ( (expo >>= 1) == 0 )
+// break;
+// factor *= factor;
+// }
+// }
+
+// done:
+ // if ( endptr != NULL )
+ // *endptr = (char*)p;
+
+ return value * sign;
+}
+
+/*********************************************************************************************\
+ * Read-only JSON token object, fits in 32 bits
+\*********************************************************************************************/
+
+void JsonParserToken::skipToken(void) {
+ // printf("skipToken type = %d %s\n", t->type, json_string + t->start);
+ switch (t->type) {
+ case JSMN_OBJECT:
+ skipObject();
+ break;
+ case JSMN_ARRAY:
+ skipArray();
+ break;
+ case JSMN_STRING:
+ case JSMN_PRIMITIVE:
+ case JSMN_KEY:
+ case JSMN_NULL:
+ case JSMN_BOOL_FALSE:
+ case JSMN_BOOL_TRUE:
+ case JSMN_FLOAT:
+ case JSMN_INT:
+ case JSMN_UINT:
+ t++; // skip 1 token
+ break;
+ case JSMN_INVALID:
+ default:
+ break; // end of stream, stop advancing
+ }
+}
+
+void JsonParserToken::skipArray(void) {
+ if (t->type == JSMN_ARRAY) {
+ size_t obj_size = t->size;
+ t++; // array root
+ if (t->type == JSMN_INVALID) { return; }
+ for (uint32_t i=0; itype == JSMN_OBJECT) {
+ size_t obj_size = t->size;
+ t++; // object root
+ if (t->type == JSMN_INVALID) { return; }
+ for (uint32_t i=0; itype == JSMN_INVALID) { return; }
+ skipToken();
+ }
+ }
+}
+
+/*********************************************************************************************\
+ * JsonParserArray
+\*********************************************************************************************/
+
+JsonParserArray::JsonParserArray(const jsmntok_t * token) : JsonParserToken(token) {
+ if (t->type != JSMN_ARRAY) {
+ t = &token_bad;
+ }
+}
+JsonParserArray::JsonParserArray(const JsonParserToken token) : JsonParserToken(token.t) {
+ if (t->type != JSMN_ARRAY) {
+ t = &token_bad;
+ }
+}
+
+JsonParserArray::const_iterator::const_iterator(const JsonParserArray t): tok(t), remaining(0) {
+ if (tok.t == &token_bad) { tok.t = nullptr; }
+ if (nullptr != tok.t) {
+ // ASSERT type == JSMN_ARRAY by constructor
+ remaining = tok.t->size;
+ tok.nextOne(); // skip array root token
+ }
+}
+
+JsonParserArray::const_iterator JsonParserArray::const_iterator::const_iterator::operator++() {
+ if (remaining == 0) { tok.t = nullptr; }
+ else {
+ remaining--;
+ tok.skipToken(); // munch value
+ if (tok.t->type == JSMN_INVALID) { tok.t = nullptr; } // unexpected end of stream
+ }
+ return *this;
+}
+
+JsonParserToken JsonParserArray::operator[](int32_t i) const {
+ if ((i >= 0) && (i < t->size)) {
+ uint32_t index = 0;
+ for (const auto elt : *this) {
+ if (i == index) {
+ return elt;
+ }
+ index++;
+ }
+ }
+ // fallback
+ return JsonParserToken(&token_bad);
+}
+
+/*********************************************************************************************\
+ * JsonParserObject
+\*********************************************************************************************/
+
+JsonParserObject::JsonParserObject(const jsmntok_t * token) : JsonParserToken(token) {
+ if (t->type != JSMN_OBJECT) {
+ t = &token_bad;
+ }
+}
+JsonParserObject::JsonParserObject(const JsonParserToken token) : JsonParserToken(token.t) {
+ if (t->type != JSMN_OBJECT) {
+ t = &token_bad;
+ }
+}
+
+JsonParserKey JsonParserObject::getFirstElement(void) const {
+ if (t->size > 0) {
+ return JsonParserKey(t+1); // return next element and cast to Key
+ } else {
+ return JsonParserKey(&token_bad);
+ }
+}
+
+JsonParserObject::const_iterator::const_iterator(const JsonParserObject t): tok(t), remaining(0) {
+ if (tok.t == &token_bad) { tok.t = nullptr; }
+ if (nullptr != tok.t) {
+ // ASSERT type == JSMN_OBJECT by constructor
+ remaining = tok.t->size;
+ tok.nextOne();
+ }
+}
+
+JsonParserObject::const_iterator JsonParserObject::const_iterator::operator++() {
+ if (remaining == 0) { tok.t = nullptr; }
+ else {
+ remaining--;
+ tok.nextOne(); // munch key
+ if (tok.t->type == JSMN_INVALID) { tok.t = nullptr; } // unexpected end of stream
+ tok.skipToken(); // munch value
+ if (tok.t->type == JSMN_INVALID) { tok.t = nullptr; } // unexpected end of stream
+ }
+ return *this;
+}
+
+/*********************************************************************************************\
+ * JsonParserKey
+\*********************************************************************************************/
+
+
+JsonParserKey::JsonParserKey(const jsmntok_t * token) : JsonParserToken(token) {
+ if (t->type != JSMN_KEY) {
+ t = &token_bad;
+ }
+}
+JsonParserKey::JsonParserKey(const JsonParserToken token) : JsonParserToken(token.t) {
+ if (t->type != JSMN_KEY) {
+ t = &token_bad;
+ }
+}
+
+JsonParserToken JsonParserKey::getValue(void) const {
+ return JsonParserToken(t+1);
+}
+
+/*********************************************************************************************\
+ * Implementation for JSON Parser
+\*********************************************************************************************/
+
+// fall-back token object when parsing failed
+const jsmntok_t token_bad = { JSMN_INVALID, 0, 0, 0 };
+
+JsonParser::JsonParser(char * json_in) :
+ _size(0),
+ _token_len(0),
+ _tokens(nullptr),
+ _json(nullptr)
+{
+ parse(json_in);
+}
+
+JsonParser::~JsonParser() {
+ this->free();
+}
+
+const JsonParserObject JsonParser::getRootObject(void) const {
+ return JsonParserObject(&_tokens[0]);
+}
+
+const JsonParserToken JsonParser::operator[](int32_t i) const {
+if ((_token_len > 0) && (i < _token_len)) {
+ return JsonParserToken(&_tokens[i]);
+ } else {
+ return JsonParserToken(&token_bad);
+ }
+}
+
+// pointer arithmetic
+// ptrdiff_t JsonParser::index(JsonParserToken token) const {
+// return token.t - _tokens;
+// }
+
+bool JsonParserToken::getBool(bool val) const {
+ if (t->type == JSMN_INVALID) { return val; }
+ if (t->type == JSMN_BOOL_TRUE) return true;
+ if (t->type == JSMN_BOOL_FALSE) return false;
+ if (isSingleToken()) return strtol(&k_current_json_buffer[t->start], nullptr, 0) != 0;
+ return false;
+}
+int32_t JsonParserToken::getInt(int32_t val) const {
+ if (t->type == JSMN_INVALID) { return val; }
+ if (t->type == JSMN_BOOL_TRUE) return 1;
+ if (isSingleToken()) return strtol(&k_current_json_buffer[t->start], nullptr, 0);
+ return 0;
+}
+uint32_t JsonParserToken::getUInt(uint32_t val) const {
+ if (t->type == JSMN_INVALID) { return val; }
+ if (t->type == JSMN_BOOL_TRUE) return 1;
+ if (isSingleToken()) return strtoul(&k_current_json_buffer[t->start], nullptr, 0);
+ return 0;
+}
+uint64_t JsonParserToken::getULong(uint64_t val) const {
+ if (t->type == JSMN_INVALID) { return val; }
+ if (t->type == JSMN_BOOL_TRUE) return 1;
+ if (isSingleToken()) return strtoull(&k_current_json_buffer[t->start], nullptr, 0);
+ return 0;
+}
+float JsonParserToken::getFloat(float val) const {
+ if (t->type == JSMN_INVALID) { return val; }
+ if (t->type == JSMN_BOOL_TRUE) return 1;
+ if (isSingleToken()) return json_strtof(&k_current_json_buffer[t->start]);
+ return 0;
+}
+const char * JsonParserToken::getStr(const char * val) const {
+ if (t->type == JSMN_INVALID) { return val; }
+ if (t->type == JSMN_NULL) return "";
+ return (t->type >= JSMN_STRING) ? &k_current_json_buffer[t->start] : "";
+}
+
+
+JsonParserObject JsonParserToken::getObject(void) const { return JsonParserObject(*this); }
+JsonParserArray JsonParserToken::getArray(void) const { return JsonParserArray(*this); }
+
+
+bool JsonParserToken::getBool(void) const { return getBool(false); }
+int32_t JsonParserToken::getInt(void) const { return getInt(0); }
+uint32_t JsonParserToken::getUInt(void) const { return getUInt(0); }
+uint64_t JsonParserToken::getULong(void) const { return getULong(0); }
+float JsonParserToken::getFloat(void) const { return getFloat(0); }
+const char * JsonParserToken::getStr(void) const { return getStr(""); }
+
+int32_t JsonParserObject::getInt(const char * needle, int32_t val) const {
+ return (*this)[needle].getInt(val);
+}
+uint32_t JsonParserObject::getUInt(const char * needle, uint32_t val) const {
+ return (*this)[needle].getUInt(val);
+}
+uint64_t JsonParserObject::getULong(const char * needle, uint64_t val) const {
+ return (*this)[needle].getULong(val);
+}
+const char * JsonParserObject::getStr(const char * needle, const char * val) const {
+ return (*this)[needle].getStr(val);
+}
+
+void JsonParser::parse(char * json_in) {
+ k_current_json_buffer = "";
+ if (nullptr == json_in) { return; }
+ _json = json_in;
+ k_current_json_buffer = _json;
+ size_t json_len = strlen(json_in);
+ if (_size == 0) {
+ // first run is used to count tokens before allocation
+ jsmn_init(&this->_parser);
+ int32_t _token_len = jsmn_parse(&this->_parser, json_in, json_len, nullptr, 0);
+ if (_token_len <= 0) { return; }
+ _size = _token_len + 1;
+ }
+ allocate();
+ jsmn_init(&this->_parser);
+ _token_len = jsmn_parse(&this->_parser, json_in, json_len, _tokens, _size);
+ // TODO error checking
+ if (_token_len >= 0) {
+ postProcess(json_len);
+ }
+}
+
+// post process the parsing by pre-munching extended types
+void JsonParser::postProcess(size_t json_len) {
+ // add an end marker
+ if (_size > _token_len) {
+ _tokens[_token_len].type = JSMN_INVALID;
+ _tokens[_token_len].start = json_len;
+ _tokens[_token_len].len = 0;
+ _tokens[_token_len].size = 0;
+ }
+ for (uint32_t i=0; i<_token_len; i++) {
+ jsmntok_t & tok = _tokens[i];
+
+ if (tok.type >= JSMN_STRING) {
+ // we modify to null-terminate the primitive
+ _json[tok.start + tok.len] = 0;
+ }
+
+ if (tok.type == JSMN_STRING) {
+ if (tok.size == 1) { tok.type = JSMN_KEY; }
+ else { json_unescape(&_json[tok.start]); }
+ } else if (tok.type == JSMN_PRIMITIVE) {
+ if (tok.len >= 0) {
+ // non-null string
+ char c0 = _json[tok.start];
+ switch (c0) {
+ case 'n':
+ case 'N':
+ tok.type = JSMN_NULL;
+ break;
+ case 't':
+ case 'T':
+ tok.type = JSMN_BOOL_TRUE;
+ break;
+ case 'f':
+ case 'F':
+ tok.type = JSMN_BOOL_FALSE;
+ break;
+ case '-':
+ case '0'...'9':
+ // look if there is a '.' in the string
+ if (nullptr != memchr(&_json[tok.start], '.', tok.len)) {
+ tok.type = JSMN_FLOAT;
+ } else if (c0 == '-') {
+ tok.type = JSMN_INT;
+ } else {
+ tok.type = JSMN_UINT;
+ }
+ break;
+ default:
+ tok.type = JSMN_PRIMITIVE;
+ break;
+ }
+ } else {
+ tok.type = JSMN_PRIMITIVE;
+ }
+ }
+ }
+}
+
+JsonParserToken JsonParserObject::operator[](const char * needle) const {
+ // key can be in PROGMEM
+ if ((!this->isValid()) || (nullptr == needle) || (0 == pgm_read_byte(needle))) {
+ return JsonParserToken(&token_bad);
+ }
+ // if needle == "?" then we return the first valid key
+ bool wildcard = (strcmp_P("?", needle) == 0);
+
+ for (const auto key : *this) {
+ if (wildcard) { return key.getValue(); }
+ if (0 == strcasecmp_P(key.getStr(), needle)) { return key.getValue(); }
+ }
+ // if not found
+ return JsonParserToken(&token_bad);
+}
+
+
+JsonParserToken JsonParserObject::findStartsWith(const char * needle) const {
+ // key can be in PROGMEM
+ if ((!this->isValid()) || (nullptr == needle) || (0 == pgm_read_byte(needle))) {
+ return JsonParserToken(&token_bad);
+ }
+
+ String needle_s((const __FlashStringHelper *)needle);
+ needle_s.toLowerCase();
+
+ for (const auto key : *this) {
+ String key_s(key.getStr());
+ key_s.toLowerCase();
+
+ if (key_s.startsWith(needle_s)) {
+ return key.getValue();
+ }
+ }
+ // if not found
+ return JsonParserToken(&token_bad);
+}
+
+const char * JsonParserObject::findConstCharNull(const char * needle) const {
+ const char * r = (*this)[needle].getStr();
+ if (*r == 0) { r = nullptr; } // if empty string
+ return r;
+}
+
+// JsonParserToken JsonParser::find(JsonParserObject obj, const char *needle, bool case_sensitive) const {
+// // key can be in PROGMEM
+// if ((!obj.isValid()) || (nullptr == needle) || (0 == pgm_read_byte(needle))) {
+// return JsonParserToken(&token_bad);
+// }
+// // if needle == "?" then we return the first valid key
+// bool wildcard = (strcmp_P("?", needle) == 0);
+
+// for (const auto key : obj) {
+// if (wildcard) { return key.getValue(); }
+// if (case_sensitive) {
+// if (0 == strcmp_P(this->getStr(key), needle)) { return key.getValue(); }
+// } else {
+// if (0 == strcasecmp_P(this->getStr(key), needle)) { return key.getValue(); }
+// }
+// }
+// // if not found
+// return JsonParserToken(&token_bad);
+// }
+
+void JsonParser::free(void) {
+ if (nullptr != _tokens) {
+ delete[] _tokens; // TODO
+ _tokens = nullptr;
+ }
+}
+
+void JsonParser::allocate(void) {
+ this->free();
+ if (_size != 0) {
+ _tokens = new jsmntok_t[_size];
+ }
+}
diff --git a/lib/jsmn-shadinger-1.0/src/JsonParser.h b/lib/jsmn-shadinger-1.0/src/JsonParser.h
new file mode 100644
index 000000000..42886d628
--- /dev/null
+++ b/lib/jsmn-shadinger-1.0/src/JsonParser.h
@@ -0,0 +1,260 @@
+/*
+ JsonParser.h - lightweight JSON parser
+
+ Copyright (C) 2020 Stephan Hadinger
+
+ 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 .
+*/
+
+#ifndef __JSON_PARSER__
+#define __JSON_PARSER__
+
+#include "jsmn.h"
+#include
+#include
+
+// #define strcmp_P(x, y) strcmp(x,y)
+// #define strcasecmp_P(x,y) strcasecmp(x,y)
+// #define pgm_read_byte(x) (*(uint8_t*)(x))
+#ifndef ARRAY_SIZE
+#define ARRAY_SIZE(x) (sizeof(x) / sizeof((x)[0]))
+#endif
+
+/*********************************************************************************************\
+ * Utilities
+\*********************************************************************************************/
+
+// The code uses a zero-error approach. Functions never return an error code nor an exception.
+// In case an operation fails, it returns an "Invalid Token".
+// To know if a token is valid, use the `isValid()` method or just use it in an if statement.
+//
+// Internally, the bad token is a pointer to a constant token of type JSMN_INVALID
+// fall-back token object when parsing failed
+const extern jsmntok_t token_bad;
+
+// To reduce code size, the current buffer is stored in a global variable.
+// This prevents all calls to add this parameter on the stack and reduces code size.
+// The caveat is that code is not re-entrant.
+// If you ever intermix two or more JSON parsers, use `parser->setCurrent()` before further calls
+//
+// the current json buffer being used, for convenience
+// Warning: this makes code non-reentrant.
+extern const char * k_current_json_buffer;
+
+/*********************************************************************************************\
+ * Read-only JSON token object, fits in 32 bits
+\*********************************************************************************************/
+// forward class declarations
+class JsonParserObject;
+class JsonParserArray;
+
+class JsonParserToken {
+public:
+
+ // constructor
+ // If parameter is null, we use the Invalid Token instead.
+ JsonParserToken(const jsmntok_t * token) : t(token) {
+ if (nullptr == t) { t = &token_bad; }
+ }
+ // no explicit destructor (not needed)
+
+ inline bool isValid(void) const { return t->type != JSMN_INVALID; }
+ inline size_t size(void) const { return t->size; }
+
+ inline bool isSingleToken(void) const { return (t->type >= JSMN_STRING); }
+ inline bool isKey(void) const { return (t->type == JSMN_KEY); }
+ inline bool isStr(void) const { return (t->type == JSMN_STRING); }
+ inline bool isNull(void) const { return (t->type == JSMN_NULL); }
+ inline bool isBool(void) const { return (t->type == JSMN_BOOL_TRUE) || (t->type == JSMN_BOOL_FALSE); }
+ inline bool isFloat(void) const { return (t->type == JSMN_FLOAT); }
+ inline bool isInt(void) const { return (t->type == JSMN_INT); }
+ inline bool isUint(void) const { return (t->type == JSMN_UINT); }
+ inline bool isNum(void) const { return (t->type >= JSMN_FLOAT) && (t->type <= JSMN_UINT); }
+ inline bool isObject(void) const { return (t->type == JSMN_OBJECT); }
+ inline bool isArray(void) const { return (t->type == JSMN_ARRAY); }
+
+ // move to token immediately after in the buffer
+ void nextOne(void) { if (t->type != JSMN_INVALID) { t++; } }
+
+ // conversion operators
+ // Warning - bool does not test for Boolean value but for validity, i.e. equivalent to token.valid()
+ inline explicit operator bool() const { return t->type != JSMN_INVALID; };
+
+ // all the following conversion will try to give a meaninful value
+ // if the content is not of the right type or the token is invalid, returns the 'default'
+ bool getBool(void) const; // true if 'true' or non-zero int (default false)
+ int32_t getInt(void) const; // convert to int (default 0)
+ uint32_t getUInt(void) const; // convert to unsigned int (default 0)
+ uint64_t getULong(void) const; // convert to unsigned 64 bits (default 0)
+ float getFloat(void) const; // convert to float (default 0), does not support exponent
+ const char * getStr(void) const; // convert to string (default "")
+
+ // same as above, but you can choose the default value
+ bool getBool(bool val) const;
+ int32_t getInt(int32_t val) const;
+ uint32_t getUInt(uint32_t val) const;
+ uint64_t getULong(uint64_t val) const;
+ float getFloat(float val) const;
+ const char * getStr(const char * val) const;
+
+ // convert to JsonParserObject or JsonParserArray, or Invalid Token if not allowed
+ JsonParserObject getObject(void) const;
+ JsonParserArray getArray(void) const;
+
+public:
+ // the following should be 'protected' but then it can't be accessed by iterators
+ const jsmntok_t * t;
+ // skip the next Token as a whole (i.e. skip an entire array)
+ void skipToken(void);
+
+protected:
+
+ // skip the next token knowing it's an array
+ void skipArray(void);
+
+ // skip the next token knowing it's an object
+ void skipObject(void);
+};
+
+/*********************************************************************************************\
+ * Subclass for Key
+\*********************************************************************************************/
+class JsonParserKey : public JsonParserToken {
+public:
+ JsonParserKey(const jsmntok_t * token);
+ explicit JsonParserKey(const JsonParserToken token);
+
+ // get the value token associated to the key
+ JsonParserToken getValue(void) const;
+};
+
+/*********************************************************************************************\
+ * Subclass for Object
+\*********************************************************************************************/
+class JsonParserObject : public JsonParserToken {
+public:
+ JsonParserObject(const jsmntok_t * token);
+ explicit JsonParserObject(const JsonParserToken token);
+
+ // find key with name, case-insensitive, '?' matches any key. Returns Invalid Token if not found
+ JsonParserToken operator[](const char * needle) const;
+ // find a key starting with `needle`, case insensitive
+ JsonParserToken findStartsWith(const char * needle) const;
+ // find a key, case-insensitive, return nullptr if not found (instead of "")
+ const char * findConstCharNull(const char * needle) const;
+
+ // all-in-one methods: search for key (case insensitive), convert value and set default
+ int32_t getInt(const char *, int32_t) const;
+ uint32_t getUInt(const char *, uint32_t) const;
+ uint64_t getULong(const char *, uint64_t) const;
+ float getFloat(const char *, float) const;
+ const char * getStr(const char *, const char *) const;
+
+ // get first element (key)
+ JsonParserKey getFirstElement(void) const;
+
+ //
+ // const iterator
+ //
+ class const_iterator {
+ public:
+ const_iterator(const JsonParserObject t);
+ const_iterator operator++();
+ bool operator!=(const_iterator & other) const { return tok.t != other.tok.t; }
+ const JsonParserKey operator*() const { return JsonParserKey(tok); }
+ private:
+ JsonParserToken tok;
+ size_t remaining;
+ };
+ const_iterator begin() const { return const_iterator(*this); } // start with 'head'
+ const_iterator end() const { return const_iterator(JsonParserObject(&token_bad)); } // end with null pointer
+};
+
+/*********************************************************************************************\
+ * Subclass for Array
+\*********************************************************************************************/
+class JsonParserArray : public JsonParserToken {
+public:
+ JsonParserArray(const jsmntok_t * token);
+ JsonParserArray(const JsonParserToken token);
+
+ // get the element if index `i` from 0 to `size() - 1`
+ JsonParserToken operator[](int32_t i) const;
+
+ //
+ // const iterator
+ //
+ class const_iterator {
+ public:
+ const_iterator(const JsonParserArray t);
+ const_iterator operator++();
+ bool operator!=(const_iterator & other) const { return tok.t != other.tok.t; }
+ const JsonParserToken operator*() const { return tok; }
+ private:
+ JsonParserToken tok;
+ size_t remaining;
+ };
+ const_iterator begin() const { return const_iterator(*this); } // start with 'head'
+ const_iterator end() const { return const_iterator(JsonParserArray(&token_bad)); } // end with null pointer
+};
+
+/*********************************************************************************************\
+ * JSON Parser
+\*********************************************************************************************/
+
+class JsonParser {
+public:
+ // constructor, parse the json buffer
+ // Warning: the buffer is modified in the process (in-place parsing)
+ // Input: `json_in` can be nullptr, but CANNOT be in PROGMEM (remember we need to change characters in-place)
+ JsonParser(char * json_in);
+
+ // destructor
+ ~JsonParser();
+
+ // set the current buffer for attribute access (i.e. set the global)
+ void setCurrent(void) { k_current_json_buffer = _json; }
+
+ // test if the parsing was successful
+ inline explicit operator bool() const { return _token_len > 0; }
+
+ const JsonParserToken getRoot(void) { return JsonParserToken(&_tokens[0]); }
+ // const JsonParserObject getRootObject(void) { return JsonParserObject(&_tokens[0]); }
+ const JsonParserObject getRootObject(void) const;
+
+ // pointer arithmetic
+ // ptrdiff_t index(JsonParserToken token) const;
+
+protected:
+ uint16_t _size; // size of tokens buffer
+ int16_t _token_len; // how many tokens have been parsed
+ jsmntok_t * _tokens; // pointer to token buffer
+ jsmn_parser _parser; // jmsn_parser structure
+ char * _json; // json buffer
+
+ // disallocate token buffer
+ void free(void);
+
+ // allocate token buffer of size _size
+ void allocate(void);
+
+ // access tokens by index
+ const JsonParserToken operator[](int32_t i) const;
+ // parse
+ void parse(char * json_in);
+ // post-process parsing: insert NULL chars to split strings, compute a more precise token type
+ void postProcess(size_t json_len);
+};
+
+#endif // __JSON_PARSER__
\ No newline at end of file
diff --git a/lib/jsmn-shadinger-1.0/src/JsonParser.hpp.gch b/lib/jsmn-shadinger-1.0/src/JsonParser.hpp.gch
new file mode 100644
index 0000000000000000000000000000000000000000..e3d9ed686152f6e8b238238039f13a6c1089fec5
GIT binary patch
literal 2299028
zcmc#+30zgh_n-Sf(D#goYp#iF?BKqkBA9!Hxn#Kj0xB+uf@@ZYOPWhqu4#&jrn#Zw
zR+{+KL?y*sGE2iPZ84wbR%-pf=geGryhpzu|3ZKF|Z0@x;BLHXZVylo0Q%5U&{qzg@|`mn}|{GAuqRA%5v0UJpZjCK-GmCVQn(RHqtg
zAwHQFpDcr4wq;^{D5qJx(vqR#qpO6r$%h(zXBfO68ldDG)aHOsR*3iF5N{L!C0!8W
z%^7&7SoTFDk<1XkoX4oeXF;-8HlD!ZTgCZ!$Zy8^WmF~v0Fym&cX-k-K
znTKgbk};n~InA^=%$V+B+8AhD8ED#KCDpu?pe6@>GtnhNys2>wewPitbAbA0`Jy}J
zgm`Bg{L(C*=mm3gLcGxXCP7a2L+?rn@j}b)2=Ru}u4JEVOK1k#5iKoy*F&3@4GQe+
zGsobUiq2*UK)2du@Cj=3jBlnT06qF-vJXN^9y(meEc+&Y8RC@@;;FY$0ER4{J1G7ezd0co
z@&TwaSwb`_l|RHABHAVm!iwWH-EycJY-nb{lxL%;E7+0jA7n;^!U
zr(33vHnaIvQ+luwGDg_86yuf@BYN^mYw$?bXT$PtZ;UXmLXQi^sL9!{d2v4qIEjj)
z4+=*;meGMVKkg-MA-^MgVeMhE7l26eF639b%j3R7efMqfs_y+YJ2TKqJ3qqN#
zOfqFqm~Dy`c7qDE#TtM8ZxhIC`))3Hbd0u--D!AIJqDusjGIyT*%j9%qi6b|Pr}B*
z$YvXSCs{nH`P&c3F>VYtZl%gIJ+7%ecm!cGSE`iPYhiF)nm%R*y9kq@fN#dN9(|2~d?(AL#RgxxEnsj}BX~-U>@-
z!h>wFik*9XrY3vOz`*0ST8!A-se>8}LkR<61Z}NT(q6K2e$EDM)a(e!A2PFt+2;oWO>u9VTUwK8`Q%ysg1e6Ld5i9khoJLS7~?Gv+??yCQmxo
z8iUq1C)xL;1tU2Cqa1Zdg9Kw%*$3aC8QOC&FQ`lO2;>EGw#yQL=E+F*#6x8NU9>q`
zRSYQ_zf_OXIN^
z9-Cl~&Cw~YYCzcZ=T+2|sFSR=(s-ot5n)=x2>KnS!PVBV-!)|CP*@BL41N#OPHR|+EjctwX`;o{
z6=uTRLz57OOt5KliphM^Ha6N7BR`C0N}A<7jOJ=}Z4!65jn2Ci8fOK7=#iCJiHP-0|`z1P8a3NFTJVB&i0
zC2_t%wVdb%a)QhCe_bcE6r!93AlDUt?_<#8LhAyH=wV_^Oy)Et7a}!KYw=Uo4LkS
zNQ{hnN<+H@&}r91nB;7jS62(|(!i*v`Nc%jn@wA|9P>Sy?3w3U8_=E6JJI*hL1C*g
zU%`z+C-la&mqM0mU$HMk{HBKZ>@q0V9VRRdRt(g*U_rlnP5z;$gn1$&PRr6~+OdrQX{gaOxY}#lw<;9G5
z_cfWlXok^U#5`}fi6*^9o{B92`}ygD8aOF*hQ%kt;G2%}T0%o}Li{mvKSaqfo$%BW
z)VY{a)R#2mBI-7LVvHWc2Q|q4A-hAU6B>B14--c!jbz`y(QulqG+@F*e5O)$+GgPd
z&z1{i@gzSMrZw4zDm59+=6TsNaity`^PlG~p19zGVlvP*oVMyEKvZ8en)C-B-`@B5
zHrp0hGpCb!nrK8@_jKPCUDpdU5rWgA){HR`r$NqUpCAxU1VTCG@Wa
zme8`tRlUo5J`31yf{6_ZES*msE_P6083mz=(Rf(~uOQE|3W8I6G6ZEPr{rD9o~tdM
z@IYl_E`YU#*^Dlq|F$L1u-dk5f?g`yj^#8-p|qIroJzKwMox-l;(T?(LIVNqM1v{M
zkAU-}x)v%v1I?x<1(zNVCtk;rL@b!B^N3;>pYB
zDu4GZPN!7@|EV0IR|`~v!-&qH4{$snO-n&x)l^<+HF9vt`S3wsR7F{=RnQ;OYYr0N
zWdJJPt|AIfdq`xkX{R9fp4xEqwM(&jPl%5P`aVnHQ
zo*5kj4*vq`AwEHY10TnL>B=FyZho;+UUgv^8RAbRYBDHK(fA3xR9Nv~qq$sVy`0KJ
z)}#8OJHh!2>NbLXD6;jKz)*RZt=x^MFf0yuGa@HjUkk~oJo5I1x;{3jRQu!;!qGOF
zJ66Sg^tFem-w?kHi&K=Yh}2<`JxpoVz;k+va@al0XQ3ue)MG>#x8@pGD3`(giXKTH
zn~&GtPAl~oR^#$ia``ZoVv(>pf_(p#*1+p_QFWUGD}ZI`K8M(tBH>~5tfn4?hdCA(
z=mfMJNyb_bZkIQfo+
zrg5d5-Snm~_o7*q>Q*n1oPtm+bj);2KOy+*jZq%ddAV#zEY;+C3l6%KMhBdmoz|q8
zcnXVWx&=EAZ05K=Ffg&?x@-wZW8)L#o2I8hTR55UUfk#E@iJ%;LY7&N6xvUJ1X*_{
zz4UItk4QTJ-Wc#a@H}C~ChF=Kcpl_zZ$qE62CvsMP@8KOUpyjBtRbG+=&Qc#^ddd>
z0(R;#ph&hFUI=-)k2f7c@8~(PXM~p7fkg?eNZ_#Xrqj#5s;J3wF@$X+Hul{%>zQH2
z12v`*@cVaiSLq)5^6uGuGt%g@@HEy?towHZRnp|yL^462n
zJ9)7;&}cDj%rtIew{a^*SgCis#ot=21#6HA7jK#%lEPv-Stv6-lNYRuifrBZ>9_%%{|b0>*Bx
z!T<3H&I<9v><;%WXnbz|s3<=?&dn(_IN%7w1;Y{vtF^6xa8#-7Cp)Q5DGPx{!CaV^
zUf9L_=}`g^v_FW7hMh7WZ=&}&SX%f`G}Xx&BPj5)9(5J`U!Ee&-J9yI4v)*5rX1R)
z@M4dS=H;NUAM{8Vn6PZL{tYwIh7-nPn9&>+@1_PV8_X^6L4!>gu^6|~zeaz7!9!1h
zW&9LzWHARP{g-Iz7zi+Vh=wVwz$9L2rWc+3viy@|Xob~!HJ-tIlK+{oA%(Fdw?xyD
z{mE|7-co3#b^jrbfhG#fF9Dm;d{gLt+?EKZ$v$-nte{w3EuIee}Qk$Ye<;-qd
z@z_XVi;=loFSKp5<*yBCZ+wEHt$JIk>0Y0%@;6!F0X<3dJgyyhfY>93M}p-@XdUz8
z2H%lH`}pV;FbArUur5RQ#tisvka|*mWWc#aLlm&|>3mx+76u0`Nihion{;cN>T*ZF
zt-KP5!NxuRy=rLxVMV&pJSajhF{~b{4Vm!R%U7NGJ1zA-Y@M*FTWd{mxu+N5@gUd&
zJBn^;HLb$-7{)GlYug>sHL0|~T!Z0h&CJy+l;#vNDSkn{f7g?QHxyV;
zYVTy9AkPN6mTVUDj2HD
zsT1exkzkT&6w-15t_4ljDaMa6f8;B6>KZ*g?o!z8=IM_bDgfgWHXdV}JeSUcyO6oW
zcJLac3V#W+xIVNptnQoglEJ|CPltEPeJaN?@l?SDvT5(6oqBBPsbc9qgLB1t2`OP=
z4ktD!XxQTtU+TEubf`je!?6WpK)r6fwG$8sKY6abf
zo+9skSIQ|pG3oH9Vcl+^O&hcb#+sgFXiYsyOgwrr<{5od145m5j22s*5_Ls)BbE-R
zK6pxK$#vG$Tsd&_V-lUaYBb#US)w={O93Nu5bH~`KCmM*oQ`*O02fsCf
zJS*;t>9_Ty^$GHv`EIA1nMDSM!0=E(76|a)}S5nQa&xWW$RLlyPPFN{fIBU{Jb_wu`4MPY0AAMfd
zKv2YLyE3I1w_(`I7n*WI1hZ4`L3R}eJ1GYZhJc!~)HJ(qt9{|tM!?;t+FJEEy=Mp8
zrH7smM++7Z8<47G(HnI$u4+Gmwhw5J7q1EUVY%wX-Y?VytE!%C3QyAQH#sDLQGn&!
zI;_lR57FlY)ddiVH(&gN25zy7hcN^{FNOS_)gC5zrtopl3(RGP7MMmV>c#?3XPaHk
zqW4%a98Bv{XiT#Gx8~}Fm#ZGy9ZGNx5
znLQfw*XZ=M}XtC$=Cu72EA#`-~gB6%*P8lZ<3eqs!h)hN=dT!B&lR4SaT(3Sa9k}IebngFE3Y(6?JUug_}sqkyD
z31%OUI(odx-2ILG>a_he2`>#XSZICe71ZdtAD1orT&mgi7<3fciOk#d(9?Tye7-WFfiT*FZ#k3$MgkRQ{x-k5!8bd^>!iU8
z?@nnJCBqME!83ovZ9NTDlz0>7%+=_#m^|E#@YmC^z>5gWUs5=Q7-_t?Lhr;>gEd7u
zW~n?^#>3^Vp0FI6s3d6QZB=8GAqn2XMpm>D8(}n`e>2}>Vuu)RngwQ8ZTOQLV|o13
zfqhf*mkPcclCjxC!rl#DfnxBOd%g9a6Hd3_w*uZL4ZcAG|Hxl%SuIFaZn}_JawoUK
z{8j!WlYMrfLs~rNU>ReXxL+?B>c!o-DU6;Ja|~}}xw|~lvsjMNjrJyQk&F%A;?l
zjc|=3FP`+p@A@c^{a@~xV|)hn{7X*^(<#(3qpEe{
z08g6TURgG~mtq()8N8i3!-BccgmSOpQ6b5_cyffOfsBwc-9>cix!Ijt*Catg|HYCb2laz<#A8T*=NJGI=eRoT(HO3sAJnG-bgm0Y)5
zDdt~==(x${Qj&RigzJGy3!GYJPYiM&oa1&q$$X$vc4(8-iA~)H2bzBibAz}qXwJkc
zWu=%9ziw|(c4$+iwtvDVm)}??A$XnEGIwv)Y>)B@R+n`tZWlew7d*_rTk*3zx4U6(
zzgXR@VQ#mqE-S2V*K^!1C{8`46nsd8?@K>9jW|lrO4u^Vdy-w38=(
z?f>jW8fO8mtXTK0Y9HSX^RETw+2%o0w5CLx#muvkV(H
zBx+z}tghE4rhnwfA&LHDq7wBx0V5Me1oVrI3-I?3h>eRHGBP?QAj-!lU{u|Jgv99R
znEw6)k!PD)F)j~>=ZvoY+*8ayJUnVxT>sbs$iG|rRpPtBM@CL~GWk;%HJZ|KWA@PYLgh)K|{r{x>W8)IJ{U;?}H+VKYKKg%9|6GJQ
z?%PCKwq>|h4*jolY`+naBgWeF^Uz^KhepOlcaDvVp)Q{g(6vpcgn$<#qh9XX(?214
zFn4mBHsv(a%Fg@dnb2nka-06!G{+_cgeQ)Tj{!sCO~O%a8MQe5K~Et4x{W9UjEntG
zE|2>4R30avTpo=Ys5~yamp|-V<=K8kQCE69jMN(f^>n)yrypI^>FW*QT=mJMuQx*3
zhEFDay#Z>S8!r~03rw6)6yxjhSnwa%pwC(-N;k5I(v8A|lz^V!=1PEJ4$|FA^KDU#
z-Gs42`wbfs9yug7G660P4X$o6Lj~ng{n;YQBYs#y?3nP#QIWAjBKyS-iA@~KV>r5M
z1+qgCMbS5hMn(;U5mdIwe_-P5^e{B6GjUFeAF*WJM@xuMEW%#lptDnSy
zBVr<>1H$8FKn`TCqd67lQymQ{iaJVo+&|e?V}6xJD6L#Y%A;>l)YPqzf^d}c(`c!^B
zg9a`Qy~WF)E$TDX^=YnUMkuo`+PG|QA-uN4_YTH7_TSDOi89%4F1srT&!S0FH+#`8
zCl`qgZo7NO|NC;bwwrVkU79`IrdfZ=IZ}+e>c6fB+w?nN89E{^atImjNiMr$FdmA%
zUIcmA+KzrvMAY%|U~{VC>+J)bdS!bf3q5xZML
z)N*YR@YLrDs{h!y{=?As8n|wMk@HV^igge(CMqVLcK5LF<~q;kf71R#V*7P-4e)yE
zb&sxxta;o=yN(%Fbn~=KHqmulPZ8U=WS_7iuu)h?jK(14dCqk++t;F=hvH+S*@Dk;
zJx%EsiO**{B}^n(MS_Vj$`zqG;+RA$9GIO#Xk3tn8^6pVeyGP
zIqY|JZAj(hY!Ved1A~M
z>X5fxt27ajsQ09QU+;3=pO?6^u}mW>dr_=$sO$lc9{{ud$S7{uvc^#zw0OUDB9jO4?IgkOrmsFqm7$+4P6vt!#0nR
z%Gf0uH;iXnR|M-SN`FL5!myDeqGEU+FisprZC%vsg~ZsQF{9*?O3X2KC~}(@6BivD
z8RtJR&8T?^8g~Q#eIMc>rzecQpJD9QRKyfb+T5--Y410tcyhUmpzp)~7>osMK={jD
zL&he?ygVYB-cMs6%$QP%^EmVG>qgH*nx*_1T{cEcrv066MPM%?hYT4O73n|lw$Z}=
zNl}m6sKoFQBjb1}E}Tsp*)|k)-`6uJYcf5Nu{9cR$ki6GsAan1+WCHV5&08T`2pcW
zxe@R##B_+;ps4Lybj&C=oNY`ypGf@=#SaW9gKnk^mHsDd5$qjCo8B7Ac}G0Oby!@?
z=tNWITN>>>Jk9m@$V7fQG|6;+$y2ZI@bJWuaIxo@N>%2(vz}so$Hv7b#^Md!xR?>)
zLt+yWVgJ%h=l>`BAGE8RVVctMe^CF?G5tmk*l#-0lKb7$-18a|I}}?^;-u-pZyBF`
zGx-ggf1cKFjl#o6#^K#AjAhh_VF}{0sdi<<(|$(~i+$_J#9{ckjks-c?!ayTH0wWh
zSVI5N(Zbp7O#7$a?|2gQA6VAy{L8%O(&XR!I4Vg|%T4Y*7q#y(v>(n1b!{h#x0}jQ*SfumBkLjyzI8;?V@Tkm86JvYucp9-e;*q
z6Wu=JbzM>Whod5Sw%|4JB)26Mcx~77-}hfyW9aoU$1UTD_-@1c_h;WN&FzkSCmHzf
z%S~%Vs*DV`OfGX#*LM`W+QD+EF7~@UnfH7243x!5H+S;+`6cDQ-3PMW3!wB9;$xzy
zo-Vr`dye~h5xlnwA00)zGu#(%yJc14bi@9An}*vY>}*b_aa82*R7b
z<-61(=sUE2);ndi**b&gshp?i*N2Dq9~l=#FSH_ug!hX~h{5lf&7+%gIj;RL%Ml(}
zAAN9=dEQd?wd0>+S$LI&GdkvMSr1S1{URnc`HB(iD|3xoPk$^%Cg8|NVupE~XHktC
znWF!|{pN%BpZdB9Ylfk*QRqoHYtq0Yw5_>S@m!poIX;qpF(*Xo7{%XE{B6Z2D*le*
z(-nVD@ed>qo~QUi$s-m^p0rf*)a8n=RD8AKYZc!hdBi5kJu)N@-Y$94r|SN0g=b2h
zx=-@VFO+;p@`!B7la49;E6FoYNuGN~a`Bzw-z(0iS5P58NICVIE?kFXXReZeSZ%dv!QSyj)BoCf0xyO47|3KmM6~0Jvu~f+`6#rP^
zYbDR!AbHXz$x|~F-=Xkbl6<Jaez)x%(wgJ*fC$$%DU?JR(Q&6Ot#LR`0e%-SFq#pwVY$vDEvoDOZ0Tte}Mihl_H3~08J7b(6NoZ@_>OzFP4$ihrW`TE*8XzFzSSif>drUGYtdZ&rMZ;u(r>ReYP`+ZErT
z_)f(?Rs1u>cPYMG@y`|Cqj;v`d%>%LEK1&|_rFD*lt=KP&zVIQ7@-ivOzk4aKdB-&FjT;=d_=Tk$)J-&Opc
z;`bH*UGYB@f1vn7#UCmDr{aGpPKS-De4640#hnzVBgu5XnBp#qyMk8*UR=o~lxzg2
z`ZFoKq>`UevKx3H?wb{NSG<(EUs~}pikAha@mo&u@`^vJcm;4OUq!_$DZH|ht0=ju
z;?F5wO>qx!vK!SEuc7do;Pia8z$yLOiq}#6dBr`!>3O{r_g1)%l6{rzr(}P{0~8Nb
zysqN)6tAy%1H~IE-bnGriZ@Zbsp8EPZ?1Td;w=CBNgwbc$DIF;EnWSl-ytO0pOI+Kyd2Mv5M1SMZyOw
zd5DsSf>S?PC_-e)1DE^7!YZYIo_$Lif>o<
zcYu@rPQ^b}{4>RODZU$=+VyiK?*XTBWGZ>D;ugjCDZU?^^bRQg1vr)GpyFBJ)c+1C
zepvA%if4mU`Mw0FbdD;144l%*QSxymf2HITN|Kg
z`8&nWDg3;WzgIk0@e7JyRQ!_SmleOF_z#L-Rs5RbKPvu{;y)|?i{jT6|5foDidz-G
zsrW6$e^dOn;&&9ktN1;|?<@Yh;(sXqK=Fr)KT`Zp#s5;AmVi`mn&Jkr#jBur
zMa3&APD@m}Uq$h%ia)1#HN`!^sT|dnTto4i;M6~BDNakUGp9=M2nze_+t=;lD`yZ2r-2pLF0SB
z$3QUvM>(4F;-L2#qLtC-3>C|W`H>-7B#i@P5H2NyFEUgd5c%dVY3^+qpaoMqj#j*Z
z;Uq)&?{PH%~>0FMH^1QE!}S$LsW6JHrIsv
zN&iO-(K?OBmL^I^ciPL48IT*NRVHxyiwwB~>cLQXkLO=ts1%^u3{}YRjAf`apgs&$
zzJKZ)hROii$xtj%kegT1?m-Mx20-h3O?dZy@d86t
z0Fj+_5#4r7R1$&ZnaPtb4AGw>XMG(_`Bz2A=k>TG8?bN{UGr*dO>0D6E(4?&MW
z{%0zbWZmkxVI|<1r)d|>@*4EO_B4?q(^og2Qv
zPynDt47FM}>lQ;~#ny9~f`4(V%uroGw1Cn?>#(L{7@{$}owY)&je9dxA5b(yp%u1g
zFw_9h8IIUC>D`44(HNhL9GyhFZpAk-)CkZCL~K{KBpM83w=sZT1h}`F%mB@dG!tl|
zL+Xh>3^fJh%23D7%{npE3=pl?HSyy81+w2Yhe*37ns_Pmr~RxIL>dgeQgXCxtQLSG
zxa3_kCP{Pa2z4o1ptj|_}=Cao7&Od*_X$1pn%(dPtX!j6?S_4`S>Xqf<%RmT(
zKrU#X+w#u
z47CFk#s%nCwvK${_JF>CRzKf%_M4lv%t4r~}fA&FS2jbzTNAhD$#*J!LmT
zVSv8lVhkHoJB^_(fNB65wz~dY23`R`9)y#K?-_TRp{{^7fa2S%s>MJz2>p?L{KNjT
zJlz4T2B3)%@t=IjIz0fr&eqvrHqeA2p>HzK7eW&}
ztR^OAwR)SOa6q)jr-?}wB8oE<0Vt7EnmlRiBZeXY^#C+^o#$-^`T@Aj(WZnQ-^x%F
zpal%Qb4_*qXh435rirOJnl#KYfckS0Qg#ez#u57i+Q|`T@9I;Ep#gxJbHusr(_I-F
z2#9t{G%&kk
z-UnZ2XedP5<1~n6wKB>w6bI-dK+B@Pk@Lea0JNW~iIwZhr?XBxAo5=|@$nF`mZ9N*
z$o1638mrSE42=NPk)d@SUFS2D0O%N}wcfLzH1mmo8nD)elufcTjsz6MQ2PCyUL0{0
zAo46Vv3cO;feeiXL{7UwYz=z75<_DEwFb1+dPx>xEPyvSzumQhrO6ows0=s7p8cT@
zIocb5CIWI2nOSXrU}!v`S%5N=tPL1=6Tn?i=Ka=22Ht`Yh@=f-Z-&QqhTaCW0+IIq
zvfwHMNdS(3G+}A7(ubi55PNWm_ARbrWhfcYZ-AV|{@YP->&glj!!$g_yC7Osz7L|+
z;r_JK-*JSAh;S46><^mwE(4PQWRk|N7hYmuGJuv`xGeX06GKw~g#pUS`RY#w-T}}B
zfF=%~>C%Fssem#-nm9H-L)P*%h_qDDL{4^JIWpe`6u=R`x;*_bN1P6*CQ77WQK*JDC6S+;M++b({pw|4b7i#U1onav$T9|0!qP3=+
zIX?u{o1shB_J(rAMS%8k#H(#u{=(2=Ky?_po_1gQq#psIMU^J3oom0$T1x<(g_c3w
ztkqc7!BRjU0=k**+m&@v0sP31{9BV!gBV%{XfBQ
z-Lz}+>Czz3n!zCMXFYt5byfn}1G-<~wzRsdAbi4=@@M*lj;up9X<U%5FGKlgEg`Bi@0HPIK`JG39Vx64;yg8*xV?)|7^eLd@AWc->sYNcAsNqIM*nVets>t{6h9a*WP(rOvzQL_nhB_WU(U=oB3#dJEHHc2d4o_$3TR@ed)oJhU6%2d_
zpdB}A=b0{(7&-^&Wq#%b?=4jm~)I7{7
zT>!KTxpg0|$%elO;5K>Q1LN^x
z*&OXQpvzp7vE}COVCW7YZ`K-ABIgx`$hBF-(2&`mbYSQnpo(0Cp*wHNIqg0mvg(?M
z8(B`Sz^OtqIAZ*?zD^wR4?yp6j>9{YmCyq~e=#(oRF`*I>mi^@{5T1Zwn*#w2+%9g
zN@y`_9qarFfL_sSB5~$~P7M78=m8*u82wDUk__RtScf8vK4i8re^y~vO=#3~HEHP-hCL&YF|$({>xCNa_A2pQ5%>*Eq
zlb(F4)32;k63`wlz?7kBaSS~J$RDJMX?<=kVaN@lnW1-IY$=B#i5K{hr|&)^Coy+G
zM?p?vW-(9M^GZQHhva9z)=fG*v^u33cjnHU6*=iL0H$)8X05Dumm%tz0U%AhH}=>~
zhRQ)K!Np98JucU}KO(0FIn6Rr4Lp31Mt~C}$Evm@g
zSrcMQPJGqmV|Un<*WLj(74#k`1vDR|iPgi~$ttM@F%(e@VqNW)(*39ns2reme&wXA
zUI#!3$RO6o%<<;TpNB}}Wqs_RqYQWgpgnI*Y#4cKKSN~pR`PRg8hTTX6>=#KA?Zz<
zUjBl0d;qipX(HoJvP_z`%3g+!llZLEX&2U_)E#Jx
z-yn8H#$RD55MonA+O_7ow3c-N&}#@yeBLlZ_S8$v9{t^Hi)fGUUidfSknMxuuWs
zY)0$DV@N3nbR4vI(_6CFHN=fX0#~0v>AhniQ;l)PQ#2w-{G+*YMO
z3R?g;3(`dP%*&yid`pPSK?ZSjS;)5xQH=+o+(+vUmGuw|U@#XUXML3H;k1zL03A&n
z*It#A8}$MD37J8h7&`^dE>?=pGl9(l%>~T^eF!?S>4Q?7Pbh?Ioa{-z2A?w22GG|G
zoqDGG3Wi<))E%UWGgs6KpH^$$O3J3#9Y(Mfze`-mK>?E$$$>sznk^65JO
z*o$iWcK1utjpztqH|O@9st3ratuvtZfX=m;vypXP1TcYh&d>C#
z&(KSN>H#`mYQQB1UIy?L08M<~@J(5sFhD0!9)q}0!YT(TS@^M_3m^R@ZSE@&hI7&v
z>(+h5DbeBKcK%rzdtIuj)4IXPH>t2=n!jRXdob(@ip;a=kp~PiUpL)(4$Qg
ziZe6_5Ea89v_W_7Ff4rj>O*mNOl#X+Ps6&8_=
zZpmsH28di(gK*v4uQhAM1KJF7jr(Xj1C$Cmo|?da^dYUl2tWh4T1=mdHLR5Y=qf`c
zZ*7w9av~sVekWo6HPpyjBOz9SmU+t0a?&0JU^%MAy!oh1dNhFAAiSaavw|GKV<3*^
zsw;c9x}0~Yhrh|WmD}8*F(*9^5P7VcC_k>pXAHdo=rBkV&$?_2VQ4(W-w@GRRA|)q
zBZl4tWC2v+a6{QY-vSVV{32
zqDGzBQ5=zGS!Ym<@=N42LQ^j7E*M13qo2q!O#}1|60iAT+g=>$T>!su0cyqFl+QLD
z5Sa)~JpYrYY@`_w=d+fVTe`*($>_RsF?}uREg2#kY`|(!6aFvlm)4hN{P#Ix-Fl;Q
zcwnah8jgq~KnWm&s6T7*Xii`@Zk+(tKmDbg_1}k3o1-^0R*-e^0U%nvXrfv9U%GR&
zIeM%_u*1?
z0n`S`zxKx~au!<)fJ~Z`czwg3t*o;SVh~b#eU{p#Uk@OHfIluSV~q^}E^=PI-@4z9
zp^bnxbGgHfTR&$g9ne75ik#vW%g`o3v<+bp(Sv@Los?|bIZ$-Vrm~N1f$)f<#Y~wg
z=iUrJ!ARUm^lv$8F{iW@&|Oe}x6GOhY=dwJ(fS`wmD9&|02er&0fQdMRpt&r_duE$
zc<}`}1b0HDUvq0>h*6k0rB4CT28$+!p8v1{L!SYPL;(yU&NxjvZMy)a0*W*IHj;IA
z0~pIS5Z}_fHA9~RqRG!7Mr{6aDnol9mH~}8U2ZS~nGn)Bw}e@vWL@qBw3SmzY&Pi|
z*0KQlf{QtFP_0o6?E|!t>vD9Z$#O>74~RDIMsM(yJ%A>m+aRn&f9fTDf-fM_0$dYg
z4Zpv~DIFw;^BdnRKo&C#&_IUXoVP~KD2D*O$k5x3ewAsFF{gUh#N@ukHgH-r4{k$2
zoy4?v=gG&(2DAndr_GBPz&c+7c$O#BD1cWu>38F5Y+>jaAQMOv(>va%!B7sw
z@3{_U*6DSPAsX=phGw0gE;k#$0`wU}b6h-2vKCpjd4LRJ{`)Dp44niN4`}`umt>hv
z0ifL!O)Q98>(4r;0WD@|VaKs|82TE}3m{E==siS^mNO7v;=~s$+W5Aidkv-HEGuQQMfpaiFr`e%4JLl*#bf==p)Y7-f_2;g(pS!VE+Bl8lV
zaOf;Md-(wCTn4ZfW!A*5!cbm&9@&gbQMr8Yr*n;
z%+NJJV>zvlH~7wH=tn?*vR3-$md`Wv6Cm0g)WoKKC(AJOGa%aZ!D~0;B58Ym0aTaM
z+GK#CR
z0e#-=sXifJpy!)3whx4(FTV81Vm8}SguHW
z_ZI{VVU*~LANt8z8QF-wfHm=DJ=3=wO#{@BQ#$rmwj2#K7zaS>*y;Y#aM6~)mmsVn
z-+fhDJZFe?kvLY7Zl%_9O2q&T26Up)sZ|WP0O-uoPQG<-AVaPYH*%Ry$MyJ{q2hpU
zvDVj?LqixU0kH*ZeLE{BogpKjzZm-NrMCkZG66aR$Vr^5=Px@(NkFFno%8M~mkiGU
z*g$}B#5j&bzr3poz#z_#>)DtgGa%ZYKVPZRYzEu`GzA$%?vms_43&cT2BPJ@)u#jl
zr2z~8Y2w0?>C($C1F;cT?xk7PWD&~(nuBPX__1SDU5;1|(0xEo!a5~TR$O^NF^Fg_
z?a_~Qo&_+9fX$1f6{`SX2mq|KdK{CvRRq)&(5-%v+c;7s0J}g~X-)Y-IvtfEQYX~J
z?d2n-si^{pcC$5cXUPgb_LQmu@Ye
z?B_3~Ehne7J#?Ie%P?akr&I?}Ge9m|rpju09>5xq%a$Bz+C3p`0BORtSI^-b$qQl=
zu7l#CE7mbYOYm2b4u0`>Z+#L&K7i^oWHi-Y#E>tb;~-6#HeT(^kRQY)tW~nzU$Q^>
z19IWyOEtdd!dd};d5opHKQd)}bE~#6ZVMJl|`WT=Y}N(eRYd*PPIUb%Fu>0iEYN
z9d}}&HGr2mdrxz2B10j7-UT@c?-kys7z%~>8nnDW>lV*I8@f-x;4fP;@B#pOzh@AB
zpWV)7s4bwL(D9r9%S8s-0Vu_J1)lincZS*niUnbP^>w)o40V9`3AF0B@*cxLM*!cj
zPJ=~fr6KMFaXEAvG%m54b;t$|;-nidS|GiP7Xi`ii5~?XJ0UCWB|zN(HS@C*n*=ev`|P$t&;W>;h}dW2LRmUmPf;h)L`2sMby$bG897A;5jh}A8ZK%>YNg0(cNehE
zUiC%PB+Ld8|MsiC1O`~67
zt$2tn5V8Mz#pL35H~@OdsEGk9ip%91J?e)bO~kHfB$rtU5KD1>Lta%st4IWtg#56~
zYCh<7PMq?(3n=dEAX!ZEqiJw!V%Yb8zR5bHA>QC(j#z7!re+MFx}12zun8krYb>A@
z95JzLmCX!|gLs}Jjtr>$5kqePdY6kZx_Qs<85$3$57HXltFK&}yb0hA2&<~f+hxan
z3nKk=L=)p~6qDZj+kmK*HSyN>zMpeSNf7sP;uHSL`IMmvfEELq@W!1c3?u_s#nC1$
zO_c8XM2OFEw8?MRS;AU0aIXQH{M!L(ZYKjMi9#C0JMF6d#yV2~k)?hoH0l8Z??7nC
zI@7kRAB|51G!N-aJMyI;>r4YcKLXIiyGv`o&k)(nR?xvxY=BD~L(>6~`O?IUiTi(M
zXa+=Q)|z>+sqCyX0nzxvYq~wBX0z5Th?xwf?71w>@_P_z@uP{^%{Qj7Rtlg?T!;@&
zT$Iz&Y(UwpHD}}CL#*{aAOq((@B2N+8TtSajeER;+jvs;^EnV{&1(=JKEHGhYt038
z7!g10+Ew}x^8jq)loq}H@f)l&9}pG8AU^tj>>`F1K>QY3AMNQZ&E-M>WS+1b`)j6L
z#Ze#cjNFz^?7p5OEdpTTM_GO(Og7bGK+QR~k4J0mS?eP}v}D11xoTIgF+?_~Bx|ib
za$olIrGT!0oW+LFSDs_7RETNN+ORZ5T0Jr#VFZ+MmCn#|0JJ%?VeLm3I0}sc6d8GK
zJQnx|Lun9~a$f1%qkd&*C7|vMZ90)8J2dsm6o$6k*j$#iJ_ht65_b|A;oII}Xf>c;
zp_Sn}vjPKa0Q7@S#>C+@82E%VK$`fh`47@-UkmXbC%x;h&9d{X14KW+(Zuf1#9)rN
z9uWC8SUQbvBd3B5BqE|gWSWwOvergGZitv!xv8w9bN~%F+TO1}k&D|+5I;pUCvm`B
zu{*!Wa+;1Z&H#M?ngdz@I`H0-FivzcBGCHuK{aEWWK=ca~O`K>vX*EN;0JY(?PJIw9XY1X7avA#iLvQ8$1Y>+0-&fnISp}i1kUN?yEo*gaS3JYl<@$Xt~
zzsWi@?`+{>o(pJEo}vAKXu}07pnZp`@*H~r5H%FF3{|!!ez>t^I%|IcF^kJ~b>mQJ
z4Gscoi3F}Te(ebBWKlFcxItVqmyqV~5TH$ncFpvGoP`eqh~spA`fI`oj&=ml7l`(g
z$>7NVJs}!U;0#uLd;;+AsRAlG`AXjM7NauH@*pn8%pjL$8iUN%S
zO(Ox)J6M9bkQVC7qtgnWx;7zLBE=*Fb!e-im(Y2mS89AdQ*TeRQJ|_)9zd`
z6ubgRzg|>fnnCEI(MlI}o0Ficpg$Bw8)hD$HlTqZ{i3DwEYLDg2IwG2zs@3jUCA|2
zR}De>)gIwqHZtLHkSEw-y8ayCAA>$s_s=VS6TDn8Y^#8pgTg`Rmkt-EgnSn|7+*ht
z8(n0$75FPEtSiSTd5j%S_ZI`(sBnwoKY;%ZDv2Hv2nq%DkiA7u(t*d~{&+i`_msTc
z4&MqHpWGGSf_}BRr%@-gxoRu9iQ=7X?)OD_IB1f><}3b*;*`v8o3Kr?kAoM~{oCNK
z*w73DMSv1OQ$V)YH%fkFhm(%G5ii_8bTv|(!W|Iq4N9`X(h&Ye$&bL@Y39aN6TA_q
z1E{ycZG&4?xGWYK1zk-P-U+-f=mQ(QRS53{WrM!A!6}_v
zkc&Nou?W&HAB0SQ)v*6eDduIKvq!7n3xPszs8*eL`Bg6Ikb?*WPh
zjR&QI^lJ;kU)#up{{;CF$h{2u9*C|+;2l(WDEMqpnv&`Mc7zXu&MC|aUb`$-0H98w
zz971WgU?mr)rx-xepH2}7G9kx`L^P&<>Vb7#an`RQQ_f=>v1PRUJc5&!N0c&JC)Zp
z9eH`&>!j`vQ#=`bF6b*IQ!LkK9phF~SXagSEB-F{M=G2SegSk_$(&XNnN}rmKTto=
zNYFwM{#Ql5a=>q^utenF3KeZ7xWkYmKnb8JpnV{^u7dvodae>yrXc-lqr$I(zX6)1
zc!iBj%G)3x0(}R%0WwsPNwinIm*RxJ
zjqv-Rm7pD?C)v7a2vAsVW0?6ep>v3U{`_!c};j;!AAqsiedvh3yBof}Eby@4#q`%2A{X4bI(`)o@&?h>0z49wsAR(4LHI)@ueZVJo_=N8
z;U^#$uL0WyqRSt=wF*ll?{7C7d;r4JK}$fJK>I=0Ky;O?2^$9L1{woe45DizxCL|y
zbWLIMPQia4K*y~Xe)kH}uLcOWQ*r|M6q|bs5ncz%0o?|<*2ZtOK`lXa^#dOXTBu}+
z=Ks4+-P@!1CB^T8SE?g*{lJ4&SdZNma(~cR&<1tyEAYE2T;h3MGe58LH#lhpsC&`i
zQ$dSB8x&@n$^l?!3cw%QVIH2CYeBDqW`LH0HiHg;&VYUf*B4C)1IkAMPH_sq
zZxbe&u9d)c*x*MHK5rvSt-QZCanHFSc6pawC{|LKVV}2
zi4!LwKVX8Y^FUvCfU5iNA1=Y6Mf(qzXu{!hBZd1&NC962Dct8j9KQ8oDB#l`_86{auVy%WS3^@@%|N?!Lc6?)k%-?h+J79w;qw{z
z8KM1`F!1im{!18!LVV|fK38G)bqa^?S7?R&?nL3fDN(o&NEGlLh=RWGVE;jfW%i$Z
z&_n?rYWRoWQowJi?7yX;Iesaja34m{96xUj9QxKoVa|AKj%U0JJg)65VgZG>wj^o`X8Q`HaMPzb~r6vz~N}k@o+SL1@VuMG~1tGb~xkw
z505k(91j^goJ;=4M~pR5xZ}fxIT)-t9t)Z;5aT`
z*V`Y*#m}Sd58L9@to>P7$J4TWt`;M{fhNz@;TLLFIkIG<`L;4o?tojt`zfC_cg)Zx^q!?{v};~~+j_Qy#7@u^Wwof<`R_Cj+`
zhXKb~Q2V2w4u?ZEadWTZSy20{KNwlqV~-988l4fBu~fl^pkjimI1oVPyxs7p6J;+&GBrV!SSG+!%;d-6mW8`Fh}4F
zjtATvj=&XoyseqHu@O{_#<=g3gWEp9*t4H)embtiVHJg*g%S56^LF
z!r>g3=6EDa6AmY^@P!Zi^HmNfunIhI^^Z?iIUJJGgghjLQKKJ{D)1~+K?k4g&pA1q
zgu<6K><>N_c+Sb;P!ks7g*dW=4`eu=P%7k6Bm0v@4rh-v@uZI$*&ifwJZfZr7zxLS
z>`x6joGa2qVa^O`1)cVBIGqDCT6AY`{_9a2htoOuP+k!pv~f5>qd6X-(HzgYU|nv1
z&ZU53EjYtse`LksP|FiM!(u4l$cp`;793fzKcV7qh6U$S?2n^399S_p9!GIFuu`~#
zCWSdwf+HmMCq^93lwe!g{=|sGnG#L><0B-0dlm#XGZMSLbQVMt1swP&%;^rz@h}HY
zZrC5xa5&0=ErUXw$|&%}g~Ra*d_K+octwGyDgNQn39SAdPf8T#SOoU2?TplKxFS`6CA
zA^FFm!l!I73SI*}u)*ArHeJ;fr!ei+6y|CNz1NlQQ1IC*yj5{ZgHBS=RSm|ppsQX1
z_zQMetdi+;79An7z39vf9la>%`lbN<7hr!CfK&X^F!r`rZ6!AW@1(+g!IMFAm29hT
zAKs~SvcbPEgg$+)_&;6GtNYCqe-S)Fg%cG2yZBT7%YEB;3vG1Q*@XAlgl+W;3STOO
z?%n^QgR?JuRRIqGg@ADIQVay)=vKaKmYtmMrj3BpfcY*Q5)x-@Fr07`f7-~paw!)*
zgx?+6gb8hIC+BJAg*rl~kJ6h^fd0w?Fp9lH;YYyFgUYz(rCA5zrb_N?gOf(Ml1JF!
zlWoGJv%pR!OuyFJ-22=neB35Xx|i)_J-*KG+1)piA+x)xDl8B@w1E4h-UE0nXgnwt
zv>!y**NRj4Cl!7K?p}hBzrSkYek0K9prN27kbZq&6DDjG||ZD
zlfwIg4+l*G%?Et~qU)&QQgZxz0r&A?O5s{s-t$r1R
zY@4ngK52LVs*KhmhBU
zGHviXHsOjkXH)bz#C`t;x7BG?2%T37(223b#z0;S+Nki$;P*gAoOP*cbKb=^Zwd#h
zd!he9$39LE8{Jrh$1C|g@GYP(Ko1n=W^-<)o=w;`%^twV{mnhnTViw1ViVTWIAw!f
zL%1Z)+61bzH>4Aaa1R@quvjIJx53}D36l<8%YkiGc$VG$Z36o4$
z6NPmG?`wB|IOIv7`3nEU=KgM*Fr|}YBVV!!-$l3t&hyw_RUij|LO|U?13`4Xp}3Uf
z-&qP>1T`dsC35Y@2*33_xKzVlyBJK
ze;40?^F-y;`66A%(@xfL;>~gIC3Qbi@x%i3-zfn5P+{u}xOcn&>@u)>1>m;jDJUGd
z4F2OxXVI^uM`-AN>x3DTgf_{!xVuMilF$q(b?aT6(k9xVfEtFB5;*Kqv7VOIzIa4*
z8@I7px@sF|k1|P}MZIW4%53+fk`-+|UyMB?6V)U>s1-a`p(FQnF%+(zV)qgi=5
z9!ZmAQkhxBD;-6-=vhkFa&|981=OVQ*b0%!dh7#HN&z3>s>rm(xWDoxn8s^e7)%X
zpH>WROHUD<{?-6qU0pGg>tKq
zI%pM9CzVdQ^nPcaQ(FB0pZ9&|bLPxEzh^Gr-*4tOGtbOC^9aq)$F!8;ZE=agJI4!l
zZ7$mhiA>(+%=J$AX)Az(YBAxKIHcJLxg~_dmVMBhrd9;kwyz8M1Jf`e8d5*Z>{+0EFg;(KC{$f8<+1e7qU5aZVIQr_ic^5~W6Mm+pex{vmIUA!S
zvR{slUTOWbpp)YxFs{?}m%N^v^Lh%kndyG7PloC5E9iuO7pwgJ6S*MymRXNC&k{g%
z2t#GUjLsi(P5&{pJ|CgTGB|77P5UaeTp@K8Et^Yl-T&iBX%)
zg(-au@AcW(x{We!y)99@(&V^X9okIQKDvUqOpC4cHK*sP
ze-A@=ovw@SI6W^oLH*1)BnA;>kmQona}sDpHT
z>S4aK#yPa-mJvT>?ceeMu>_K+76jNxtzMB{TKZ`V8v(6Q>AIqGmPMEPTt4l2qjJ7b
zem8T?O;#hcz;b|5N4WZ8zu~UuM{L2P#+o6!+=jb$8%h1lh!d><>7vVUS29JPm;(%+
zmHAunBtO$QJN}SF&u|$Ty)a|T(}$X3jr07GTSgS#*}tW>4+Zv4p8?ASj=qn}X#n=a=fIAZX`GWEmHbU~dYO|s
zu4%1)1-t`KAKslc-1YIqEhB<+_g94pBqV(`tNK0wksqYki87Vwk0p~(gJr5s$$t$g
zSw0V^#-jF{92ruwmin3DghF}$$os!8CHY2D6YC;9F_Geic0K_fRj6WTpSI&Bpa%-9
zteihZq3B?4UG)3U^KM^#`s>oy$z81wt1G7CN-wUXN4LsY#r#&)t76KQ5d-q}n<(Y#
zioVZxEgtUbXnq*~t?>;a(+s<|Q}}}ME%|jRH>y($ds^oZ-!WEXVDQwgJuw(YYnuaR
z8o2ZhEf?@r^6--3uCj}-@P?l6CY>}kvna2roFBHx6i>(Y<_6C#bvQ%4!|rLriT6??
zVlF=6L)jt0OG6bM$k|F@&`
z*2(n3<$9tM-HP;HT|J$?^7Z_}!mjb>P7j$~G5_|Cbn@ux=@#5XV)>K4gme(+99Qf#
zc!HfC(@a~=;W<7!tIk;&Y^+0%1;GpF>C%X07{hNJ+~xrjHc^(gYQf(#2%{l-W}V)X
zjx!fSZBgH;kx*vi?HIFhGwYnsPE5O@Y?W@Aby{Ezw!5MvymGPq)7Jdj7z{B*Sc&AQ_nm*LaMs?i%0!+z@@j
zDQ#EPbdC3?e%^2ogn@2@vv)2fmc~lDVU9JX>>j5ac<1z*@7}M%9K+1sp&jlID~fQw
zbNZ|MJC{y>(=|Ts+>k+bcTS_|&C7n_Rp0k_DFSp)-vhmlne7rh*7r4KxU*@
z+A?YQ#VXV^Rt;D^us}U{YBpYgb3)L449;#7wBUp{s|T7pZGt{9KQa^&^Bgy#avOZ3
zO%QQGg(aMnbpZ@DktR+Eo}Uw(`=~bwvlNp)>+E_gmhs*g`zO(zDNJYbms
zy#smOGO5+M3-fxTi%bh_DRU<>sd~`Qu?Ks<=<)*P7CU$>gAZjo)4)
ztTlYvp2fURb$*hP<=7LGhA*lf9IQ`uKJ1<}{Ce!cJ5^m?R4X>Pu5wTMIX)XAAP%{m
zJ!@E!V^>YxDc^+-lQYuu+^sK23}17N25Fs*V@79m)!~XtPFkQJ)3_(Kx}Qyv#u!kY
z=>S<}nE7aBRqACDlzWSpRooDhpjo%6pEnB(xtc`%;dce{0ni!R%zV_DGyC(pMvdzr
z)|cj|6RBs&{J}9(JM;sw*YLYv4L1q;{*HINmAGK_)SZ|JSN^tw2=ddpF3(R|%tl>I
z4EKyF@g${kZcFt8;ygrKo@gfSG0ouyg=m`zS96^e9d*E=dT_D=`Mz7X{^Ly
zZpK2Hz9<$kNT=QUM7%!OO~t=0>9b~~DyjN`y2m-^nM0FWoxeSA-|^}PR0AgbmPebo
zgShj<&?LTzV|Ck-mDgBpc^zD75_GAEXUI3*Bxt<4sk|KANpASB-;C1dYPiDZC-*E5
znXmjrl{wwAlaD8uWeyP-1UnFQ{L~JL19_KTK0mo3!z8F)?H%v#Hfp`T$nOAjAIKlP|{Rj(3HUES#^iSU$02qtd++iQFA%lxkW`p;-pcyn_2
z=_&+>oR+wy_+^f5zy6q~!fSDB#G=&mD=$}96drq@vRo+Zp25{t6pm_0S*~g*kC#fW
zI~;|7u%yDO{l%5tBx`$%m%`us;>ycLm4z)L_L!K3ux^)*gACI1*H*T7pPd>pl8f-%
zmGpSa`FWa)@*MnotBr`S_-~)UehhBn`aHE@;09ZIQU>kqFHSL%N~8XsL!>S(h&xf)
zf~>c&yOcjnA}3>>hP1}URoig)HZj@>Rdgc87>)s4n2l*mHh5H~e^_3j)7h5$6xHFk
z?cFQWD5`Y#Nge4mfiR1KI0k?cJxPL_l0?hU_nI?|z8+S!}VU
zBT-pif6?BZxO_fdRNEBxKRzj9(Ew!gW_d;A`$zp}uC6sAG4RNU#3#Uy^U0!Nh5ibu6hECshP$3D{G@)>ftfb@zbJf>{{aafRbBxg~ck4^IzT!Cu
zA6bHrgtur(<21;m+
z$L$@h5_ZnYV1thjzqU#^PyM{nQy7!2#-n$%vZh}y`E*cQ4H%}??{}GzPOU_J#kmgh
z4B{|6b@?!p3M==_mHI@}wj#cd_^yL*2G>d?B?iB=x~4bLPT3HrNE~$C0z8~tUSe9Uv@QqjjVnN~LZurPhpo{esQs_Wx&IJt&
zmC0M{;4y&Q)aMRW{DmHT!Jhg;SM=Gko3**L&kjsGZS+^gUrl|snUdEC?g
zHcH5+{wCZ?cb~Vwee`qOee1NixeRG!22p*N78Lx;LaVsq$tCV(YKvR0ih(DL5?sV%
zK^$ES+>_7;0QVCI<@W&vF0!R*O3>Lz;-vf0Pz9?DRsb+E(
zCL3A1ic3!|v^xEY#~lpZr4C0QH^WT|^@(vUa4Q$o1v1=Z-CKscYr$!w4z(X~JO3N^
z_!MA0!sEV>YLwuR)Eu|WL-HKBb3VhZeA$TmV21Q)21%?+3o1=rXtlIra>?B?^^O`>
z#jJBi*8Rm7LF`z;<30r3FCDnLX1HZ9^tl^6?#BvV-o-p_Y0>HE`^|8>XmKa=@~$6k
zoKSO@$31JTae~FYCfwq_MD9^KWPpO?8<&1qZk5vDr~j$usU!-kz-1Ft+Hv&Lj}@!)
z(%%*Zjcr&S?{+Waz}On~SK|gK)DB1Mih_iR?TEf7Q9C$zse?M{C>_F964^5DvWV{J
zhS5pZcQ#0iZfH1?5uqx}k!oD08>HRDTnpDw6f|&%c+J~}Bck=HGBiH6P&KBjbpHs|
z)hj$NBPJRrgyizPgiJC{*iHSs0WX0bE;GPOfpKs0|OSIi%vsCH0~Q3nZCmSjB^5eJB;m?`2TTjJ26WPLJIziA97<5sFp
ztIFbfYFtkkrWqB5CG9&4=orK8()FK>Fb6?dHel6E`g{`8+Sv{f!eE@_1fu
z1{x>yE@;k+(j#&ucu^ZGEfrRd`k(W1w__tOoUw?zo}Op3L>)BPN)bQ4AfuaEt@>)_
z0ENauvIxA$&4|9Cgt&{99To8ghF{`^RH@2T{WY$$jMJjC!jm#^NqGsm#0*C(DczlBvI`Tyi|}?|_$T>o%6=b*Zwe-ke%kM6I)i^f>)rm(Uw
z{+yRN?v1?Q+o0T~jJ!RS>YxHQMSNgU#(_t*>bbQ86f(z{3Z9qs4FVm6+h*+F?Fr_C
zzMwMo9n+$d*C&ap9+#%)XP2sf^sf49AfU*sjEM_P2;Pf4ObpOmQ%XdA`ukY^d)_SdOf%ym}`b|mw)yg*+@i+}Wk?tf8VE
z`C^6D4$IGZnX*c&XS1W^Z7AxW#N-}2azEw|5cUKH^jJXV6sO}p{$Qbe9
zrO+3nFX@Xc;c1!rnZ~t;Yg)7-I!UzmDdQ#2lJRmBckEr8^Fq(CrERhc& }(2-X>
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 |