diff --git a/tasmota/xdrv_85_BLE_EQ3_TRV.ino b/tasmota/xdrv_85_BLE_EQ3_TRV.ino
new file mode 100644
index 000000000..3427869ac
--- /dev/null
+++ b/tasmota/xdrv_85_BLE_EQ3_TRV.ino
@@ -0,0 +1,1767 @@
+/*
+ xdrv_85_BLE_EQ3_TRV.ino - EQ3 radiator valve sense and control via BLE_ESP32 support for Tasmota
+
+ Copyright (C) 2020 Simon Hailes
+
+ 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 .
+
+ --------------------------------------------------------------------------------------------
+ Version yyyymmdd Action Description
+ --------------------------------------------------------------------------------------------
+ 1.0.0.0 20210910 publish - renamed to xdrv_85, and checked with TAS latest dev branch
+ 0.0.0.0 20201213 created - initial version
+*/
+
+
+/*
+
+Commands:
+e.g.
+trv 001A22092EE0 settemp 22.5
+
+trvperiod n - set polling period in seconds (default teleperiod at boot)
+trvonlyaliased *0/1 - only hear devices with BLEAlias set
+trvMatchPrefix 0/*1 - if set, then it will add trvs to the seen list which have mac starting with :
+ macs in macprefixes, currently only 001a22
+Note: anything with BLEAlias starting "EQ3" will be added to the seen list.
+trvHideFailedPoll 0/*1 - if set, then failed polls will not be sent to EQ3
+trvMinRSSI -n - the minimum RSSI value at which to attempt to poll
+
+
+trv reset - clear device list
+trv devlist - report seen devices. Active scanning required, not passive, as it looks for names
+trv scan - same as devlist
+trv state - report general state (see below for MQTT)
+trv raw - send a raw command
+trv on - set temp to 30 -> display ON on EQ3
+trv off - set temp to 4.5 -> display OFF on EQ3
+trv boost - set boost
+trv unboost - turn off boost
+trv lock - manual lock of physical buttons
+trv unlock - manual unlock of physical buttons
+trv auto - set EQ3 to auto mode
+trv manual - set EQ3 to manual mode
+trv mode auto|manual - set EQ3 to mode auto|manual?
+trv day - set EQ3 to day temp
+trv night - set EQ3 to night temp
+trv settemp 20.5 - set EQ3 to temp
+trv settime - set time to Tasmota time (untested)
+trv settime - set time
+trv offset 1.5 - set offset temp
+trv setdaynight 22 17.5 - set day and night mode temps
+trv setwindowtempdur 12.5 30 - set window open temp and duration in mins
+
+trv reqprofile <0-6> - request a profile for a day fo the week.
+trv setprofile <0-6> 20.5-07:30,17-17:00,22.5-22:00,17-24:00 (up to 7 temp-HH:MM) - set a profile for a day fo the week.
+
+Responses:
+normal:
+stat/EQ3/001A22092C9A = {
+ "cmd":"state",
+ "result":"ok",
+ "RSSI":-83,
+ "stattime":1613814193,
+ "temp":21.0,
+ "posn":0,
+ "mode":"auto",
+ "boost":"inactive",
+ "dst":"set",
+ "window":"closed",
+ "state":"unlocked",
+ "battery":"GOOD"
+}
+
+holiday:
+as above, but adds ,"holidayend":"YY-MM-DD HH:MM"
+
+when trv reqprofile is used, adds:
+ "profiledayN":"20.5-07:30,17.0-17:00,22.5-22:00,17.0-24:00"
+where N is the day (0-6) (0 = saturday?).
+
+when trv setprofile is used, adds:
+"profiledayset":N
+where N is the day (0-6) (0 = saturday?).
+
+on error:
+ "result":"fail",
+
+The driver will try a command three times before reporting
+
+
+4 digit pin calculation: (just for info)
+serialno = "REQ0123456"
+pin = []
+
+x = str((ord(serialno[3]) ^ ord(serialno[7])) % 10)
+pin.append(x)
+x = str((ord(serialno[4]) ^ ord(serialno[8])) % 10)
+pin.append(x)
+x = str((ord(serialno[5]) ^ ord(serialno[9])) % 10)
+pin.append(x)
+x = str((ord(serialno[0]) - ord('A') ^ ord(serialno[6]) - ord('0')) % 10)
+pin.append(x)
+print("".join(pin))
+
+*/
+
+
+
+
+//#define VSCODE_DEV
+
+#ifdef VSCODE_DEV
+#define ESP32
+#define USE_BLE_ESP32
+#define USE_EQ3_ESP32
+#endif
+
+// for testing of BLE_ESP32, we remove xsns_62_MI_ESP32.ino completely, and instead add this modified xsns_52_ibeacon_BLE_ESP32.ino
+#if CONFIG_IDF_TARGET_ESP32
+#ifdef USE_EQ3_ESP32
+#ifdef ESP32 // ESP32 only. Use define USE_HM10 for ESP8266 support
+#ifdef USE_BLE_ESP32
+
+#define XDRV_85 85
+#define D_CMND_EQ3 "trv"
+
+// uncomment for more debug messages
+//#define EQ3_DEBUG
+
+namespace EQ3_ESP32 {
+
+void CmndTrv(void);
+void CmndTrvPeriod(void);
+void CmndTrvOnlyAliased(void);
+void CmndTrvMatchPrefix(void);
+void CmndTrvMinRSSI(void);
+void CmndTrvHideFailedPoll(void);
+
+const char kEQ3_Commands[] PROGMEM = D_CMND_EQ3"|"
+ "|"
+ "period|"
+ "onlyaliased|"
+ "MatchPrefix|"
+ "MinRSSI|"
+ "HideFailedPoll";
+
+void (*const EQ3_Commands[])(void) PROGMEM = {
+ &CmndTrv,
+ &CmndTrvPeriod,
+ &CmndTrvOnlyAliased,
+ &CmndTrvMatchPrefix,
+ &CmndTrvMinRSSI,
+ &CmndTrvHideFailedPoll
+};
+
+
+const char *cmdnames[] = {
+ "poll",
+ "raw",
+ "state",
+ "settime",
+ "settemp",
+ "offset",
+ "setdaynight",
+ "setwindowtempdur",
+ "setholiday",
+ "boost",
+ "unboost",
+ "unlock",
+ "auto",
+ "manual",
+ "eco",
+ "on",
+ "off",
+ "valve",
+ "mode",
+ "day",
+ "night",
+ "reqprofile",
+ "setprofile"
+};
+
+const uint8_t *macprefixes[1] = {
+ (uint8_t *)"\x00\x1a\x22"
+};
+
+int EQ3GenericOpCompleteFn(BLE_ESP32::generic_sensor_t *pStruct);
+
+const char EQ3_Svc[] PROGMEM = "3e135142-654f-9090-134a-a6ff5bb77046";
+const char EQ3_rw_Char[] PROGMEM = "3fa4585a-ce4a-3bad-db4b-b8df8179ea09";
+const char EQ3_notify_Char[] PROGMEM = "d0e8434d-cd29-0996-af41-6c90f4e0eb2a";
+
+struct eq3_device_tag{
+ uint8_t addr[7];
+ int8_t RSSI;
+ uint64_t timeoutTime;
+ uint8_t pairing;
+ uint8_t lastStatus[10]; // last received 02 stat
+ uint8_t lastStatusLen;
+ uint32_t lastStatusTime; // in utc
+ uint8_t nextDiscoveryData;
+} eq3_device_t;
+
+/*********************************************************************************************\
+ * variables to control operation
+\*********************************************************************************************/
+int retries = 0;
+// allow 240s before timeout of sa device - based on that we restart BLE if we don't see adverts for 120s
+#define EQ3_TIMEOUT 240L
+
+uint8_t pairingaddr[7] = {0,0,0,0,0,0};
+char pairingserial[20];
+uint8_t pairing = 0;
+
+#define EQ3_NUM_DEVICESLOTS 16
+eq3_device_tag EQ3Devices[EQ3_NUM_DEVICESLOTS];
+void *EQ3mutex = nullptr;
+
+int EQ3Period = 300;
+uint8_t EQ3OnlyAliased = 0;
+uint8_t EQ3MatchPrefix = 1;
+uint8_t opInProgress = 0;
+int seconds = 20;
+int EQ3CurrentSingleSlot = 0;
+
+uint8_t EQ3TopicStyle = 1;
+uint8_t EQ3HideFailedPoll = 1;
+int8_t trvMinRSSI = -99;
+
+// control of timing of sending polling.
+// we leave an interval between polls to allow scans to take place
+int intervalSeconds = 10; // min seconds between operations
+int intervalSecondsCounter = 0; // set when an operation is over to intervalSeconds
+int nextEQ3Poll = EQ3_NUM_DEVICESLOTS; // set to zero to start a poll cycle
+
+#pragma pack( push, 1 ) // aligned structures for size
+struct op_t {
+ uint8_t addr[7];
+ uint8_t towrite[16];
+ uint8_t writelen;
+ uint8_t cmdtype;
+};
+#pragma pack(pop)
+
+std::deque opQueue;
+
+
+/*********************************************************************************************\
+ * Functions
+\*********************************************************************************************/
+
+const char *addrStr(const uint8_t *addr, int useAlias = 0){
+ static char addrstr[32];
+
+ const char *id = nullptr;
+ if (useAlias){
+ id = BLE_ESP32::getAlias(addr);
+ }
+ if (!id || !(*id)){
+ id = addrstr;
+ BLE_ESP32::dump(addrstr, 13, addr, 6);
+ } else {
+ }
+
+ return id;
+}
+
+char *topicPrefix(int prefix, const uint8_t *addr, int useAlias){
+ static char stopic[TOPSZ];
+ const char *id = addrStr(addr, useAlias);
+ if (!EQ3TopicStyle){
+ GetTopic_P(stopic, prefix, TasmotaGlobal.mqtt_topic, PSTR(""));
+ strcat(stopic, PSTR("EQ3/"));
+ strcat(stopic, id);
+ } else {
+ char p[] = "EQ3";
+ GetTopic_P(stopic, prefix, p, id);
+ }
+ return stopic;
+}
+
+
+
+// return 0+ if we find the addr has one of our listed prefixes
+// return -1 if we don't recognise the mac
+int matchPrefix(const uint8_t *addr){
+ for (int i = 0; i < sizeof(macprefixes)/sizeof(*macprefixes); i++){
+ if (!memcmp(addr, macprefixes[i], 3)){
+ return i;
+ }
+ }
+ return -1;
+}
+
+
+bool EQ3Operation(const uint8_t *MAC, const uint8_t *data, int datalen, int cmdtype, int retries_in = 0) {
+ BLE_ESP32::generic_sensor_t *op = nullptr;
+
+ // ALWAYS use this function to create a new one.
+ int res = BLE_ESP32::newOperation(&op);
+ if (!res){
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 %s:Can't get a newOperation from BLE"), addrStr(MAC, cmdtype & 0x80));
+ retries = 0;
+ return 0;
+ } else {
+#ifdef EQ3_DEBUG
+ AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3 %s:got a newOperation from BLE"), addrStr(MAC, cmdtype & 0x80));
+#endif
+ }
+
+ NimBLEAddress addr((uint8_t *)MAC);
+ op->addr = addr;
+
+ bool havechar = false;
+ op->serviceUUID = NimBLEUUID(EQ3_Svc);
+ op->characteristicUUID = NimBLEUUID(EQ3_rw_Char);
+ op->notificationCharacteristicUUID = NimBLEUUID(EQ3_notify_Char);
+
+ if (data && datalen) {
+ op->writelen = datalen;
+ memcpy(op->dataToWrite, data, datalen);
+ } else {
+ op->writelen = 1;
+ op->dataToWrite[0] = 0x03; // just request status
+ }
+
+ // this op will call us back on complete or failure.
+ op->completecallback = (void *)EQ3GenericOpCompleteFn;
+ // store this away for later
+ op->context = (void *)cmdtype;
+
+ res = BLE_ESP32::extQueueOperation(&op);
+ if (!res){
+ // if it fails to add to the queue, do please delete it
+ BLE_ESP32::freeOperation(&op);
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 %s:Failed to queue new operation - deleted"), addrStr(MAC, cmdtype & 0x80));
+ retries = 0;
+ } else {
+ if (retries_in){
+ retries = retries_in;
+ }
+ }
+
+ return res;
+}
+
+int EQ3DoOp(){
+ if (!opInProgress){
+ if (opQueue.size()){
+ op_t* op = opQueue[0];
+ if (EQ3Operation(op->addr, op->towrite, op->writelen, op->cmdtype, 4)){
+ opQueue.pop_front();
+ opInProgress = 1;
+ AddLog(LOG_LEVEL_DEBUG, PSTR("EQ3 %s:Op dequeued len now %d"), addrStr(op->addr, (op->cmdtype & 0x80)), opQueue.size());
+ delete op;
+ return 1;
+ } else {
+ AddLog(LOG_LEVEL_ERROR, PSTR("EQ3 %s:Op BLE could not start op queue len %d"), addrStr(op->addr, (op->cmdtype & 0x80)), opQueue.size());
+ }
+ }
+ }
+ return 0;
+}
+
+int EQ3QueueOp(const uint8_t *MAC, const uint8_t *data, int datalen, int cmdtype, int useAlias) {
+ op_t* newop = new op_t;
+ memcpy(newop->addr, MAC, 6);
+ memcpy(newop->towrite, data, datalen);
+ newop->writelen = datalen;
+ newop->cmdtype = cmdtype | (useAlias?0x80:0);
+ opQueue.push_back(newop);
+ int qlen = opQueue.size();
+ AddLog(LOG_LEVEL_DEBUG, PSTR("EQ3 %s: Op queued len now %d"), addrStr(newop->addr, (newop->cmdtype & 0x80)), qlen);
+ EQ3DoOp();
+ return qlen;
+}
+
+int EQ3ParseOp(BLE_ESP32::generic_sensor_t *op, bool success, int retries){
+ int res = 0;
+ opInProgress = 0;
+ ResponseClear();
+
+ uint8_t addrev[7];
+ const uint8_t *native = op->addr.getNative();
+ memcpy(addrev, native, 6);
+ BLE_ESP32::ReverseMAC(addrev);
+
+ eq3_device_tag *eq3 = nullptr;
+
+ int free = -1;
+ for (int i = 0; i < EQ3_NUM_DEVICESLOTS; i++){
+ if (!memcmp(EQ3Devices[i].addr, addrev, 6)){
+ eq3 = &EQ3Devices[i];
+ break;
+ }
+ }
+
+ int cmdtype = (((uint32_t)op->context) & 0xff);
+ const char *cmdType = PSTR("invalid");
+ int useAlias = cmdtype & 0x80;
+ cmdtype &= 0x7f;
+ if ((cmdtype >= 0) && (cmdtype < sizeof(cmdnames)/sizeof(*cmdnames))){
+ cmdType = cmdnames[cmdtype];
+ }
+
+ ResponseAppend_P(PSTR("{"));
+ ResponseAppend_P(PSTR("\"cmd\":\"%s\""), cmdType);
+ ResponseAppend_P(PSTR(",\"result\":\"%s\""), success? "ok":"fail");
+ ResponseAppend_P(PSTR(",\"MAC\":\"%s\""), addrStr(addrev));
+ const char *host = NetworkHostname();
+ ResponseAppend_P(PSTR(",\"tas\":\"%s\""), host);
+ if (cmdtype == 1){
+ char raw[40];
+ BLE_ESP32::dump(raw, 40, op->dataNotify, op->notifylen);
+ ResponseAppend_P(PSTR(",\"raw\":\"%s\""), raw);
+ }
+
+ uint8_t *status = {0};
+ uint8_t statlen = 0;
+ uint32_t stattime = 0;
+
+ if (success){
+ if ((op->notifylen >= 6) && (op->dataNotify[0] == 2) && (op->dataNotify[1] == 1)){
+ if (eq3){
+ memcpy(eq3->lastStatus, op->dataNotify, (op->notifylen <= 10)?op->notifylen:10);
+ eq3->lastStatusLen = (op->notifylen <= 10)?op->notifylen:10;
+ eq3->lastStatusTime = UtcTime();
+ }
+ }
+
+ status = op->dataNotify;
+ statlen = op->notifylen;
+ stattime = UtcTime();
+ }
+
+ if (eq3){
+ status = eq3->lastStatus;
+ statlen = eq3->lastStatusLen;
+ stattime = eq3->lastStatusTime;
+ ResponseAppend_P(PSTR(",\"RSSI\":%d"), eq3->RSSI);
+ }
+
+ if ((statlen >= 6) && (status[0] == 2) && (status[1] == 1)){
+ ResponseAppend_P(PSTR(",\"stattime\":%u"), stattime);
+ ResponseAppend_P(PSTR(",\"temp\":%2.1f"), ((float)status[5])/2);
+ ResponseAppend_P(PSTR(",\"posn\":%d"), status[3]);
+ int stat = status[2];
+ ResponseAppend_P(PSTR(",\"mode\":"));
+ switch (stat & 3){
+ case 0:
+ ResponseAppend_P(PSTR("\"auto\""));
+ break;
+ case 1:
+ ResponseAppend_P(PSTR("\"manual\""));
+ break;
+ case 2:
+ ResponseAppend_P(PSTR("\"holiday\""));
+ break;
+ case 3:
+ ResponseAppend_P(PSTR("\"manualholiday\""));
+ break;
+ }
+
+ ResponseAppend_P(PSTR(",\"hassmode\":"));
+ do {
+ //0201283B042A
+ // its in auto
+ if ((stat & 3) == 0) { ResponseAppend_P(PSTR("\"auto\"")); break; }
+ // it's set to 'OFF'
+ if (((stat & 3) == 1) && (status[5] == 9)) { ResponseAppend_P(PSTR("\"off\"")); break; }
+ // it's actively heating (valve open)
+ if (((stat & 3) == 1) && (status[5] > 9) && (status[3] > 0)) { ResponseAppend_P(PSTR("\"heat\"")); break; }
+ // it's achieved temp (valve closed)
+ if (((stat & 3) == 1) && (status[5] > 9)) { ResponseAppend_P(PSTR("\"idle\"")); break; }
+ ResponseAppend_P(PSTR("\"idle\""));
+ break;
+ } while (0);
+
+ ResponseAppend_P(PSTR(",\"boost\":\"%s\""), (stat & 4)?"active":"inactive");
+ ResponseAppend_P(PSTR(",\"dst\":\"%s\""), (stat & 8)?"set":"unset");
+ ResponseAppend_P(PSTR(",\"window\":\"%s\""), (stat & 16)?"open":"closed");
+ ResponseAppend_P(PSTR(",\"state\":\"%s\""), (stat & 32)?"locked":"unlocked");
+ ResponseAppend_P(PSTR(",\"battery\":\"%s\""), (stat & 128)?"LOW":"GOOD");
+ }
+
+ if ((statlen >= 10) && (status[0] == 2) && (status[1] == 1)){
+ int mm = status[8] * 30;
+ int hh = mm/60;
+ mm = mm % 60;
+ ResponseAppend_P(PSTR(",\"holidayend\":\"%02d-%02d-%02d %02d:%02d\""),
+ status[7],
+ status[9],
+ status[6],
+ hh, mm
+ );
+ }
+
+ if (success) {
+ // now to parse other data - this may not have been a stat message
+ if ((op->notifylen >= 3) && (op->dataNotify[0] == 2) && (op->dataNotify[1] == 2)){
+ ResponseAppend_P(PSTR(",\"profiledayset\":%d"), op->dataNotify[2]);
+ }
+
+ if ((op->notifylen >= 16) && (op->dataNotify[0] == 0x21)){
+//YY is the time, coded as (minutes/10), up to which to maintain the temperature declared in XX
+//XX represents the temperature to be maintained until then, codified as (temperature*2)
+// byte 0: 21 (default value)
+// byte 1: 02 (Monday = 0x02)
+// byte (2,3): 22 24 (17°C up to 06:00)
+// byte (4,5): 2A 36 (21°C up to 09:00)
+// byte (6,7): 22 66 (17°C up to 17:00)
+// byte (8,9): 2A 8A (21°C up to 23:00)
+// byte (10,11): 22 90 (17°C up to 24:00)
+// byte (12,13): 22 90 (unused)
+// byte (14,15): 22 90 (unused)
+ ResponseAppend_P(PSTR(",\"profileday%d\":\""), op->dataNotify[1]);
+ uint8_t *data = op->dataNotify + 2;
+ for (int i = 0; i < 7; i++){
+ float t = *(data++);
+ t /= 2;
+ int mm = *(data++);
+ mm *= 10;
+ int hh = mm / 60;
+ mm = mm % 60;
+ ResponseAppend_P(PSTR("%2.1f-%02d:%02d"), t, hh, mm);
+ // stop if the last one is 24.
+ if (hh == 24){
+ break;
+ }
+
+ if (i < 6){
+ ResponseAppend_P(PSTR(","));
+ }
+ }
+ ResponseAppend_P(PSTR("\""));
+ }
+
+ res = 1;
+ }
+
+ ResponseAppend_P(PSTR("}"));
+
+ int type = STAT;
+ if (cmdtype){
+ type = STAT;
+ } else {
+ // it IS a poll command
+ if (EQ3HideFailedPoll){
+ if (!success){
+ AddLog(LOG_LEVEL_DEBUG, PSTR("EQ3 %s poll fail not sent because EQ3HideFailedPoll"), addrStr(addrev));
+ return res;
+ }
+ }
+ }
+
+ char *topic = topicPrefix(type, addrev, useAlias);
+ MqttPublish(topic, false);
+ return res;
+}
+
+int EQ3GenericOpCompleteFn(BLE_ESP32::generic_sensor_t *op){
+ uint32_t context = (uint32_t) op->context;
+ opInProgress = 0;
+
+ if (op->state <= GEN_STATE_FAILED){
+ uint8_t addrev[7];
+ const uint8_t *native = op->addr.getNative();
+ memcpy(addrev, native, 6);
+ BLE_ESP32::ReverseMAC(addrev);
+
+ if (retries > 1){
+ retries--;
+
+ if (EQ3Operation(addrev, op->dataToWrite, op->writelen, (int)op->context)){
+ //EQ3ParseOp(op, false, retries);
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 %s: trv operation failed - retrying %d"), addrStr(addrev), op->state);
+ opInProgress = 1;
+ } else {
+ retries = 0;
+ EQ3ParseOp(op, false, 0);
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 %s: trv operation failed to send op %d"), addrStr(addrev), op->state);
+ }
+ } else {
+ retries = 0;
+ EQ3ParseOp(op, false, 0);
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 %s: trv operation failed - no more retries %d"), addrStr(addrev), op->state);
+ }
+ return 0;
+ }
+
+ retries = 0;
+
+ EQ3ParseOp(op, true, 0);
+ return 0;
+}
+
+
+
+/*********************************************************************************************\
+ * Functons actualy called from within the BLE task
+\*********************************************************************************************/
+
+int ispairing2(const uint8_t *payload, int len, char *name, int namelen, char *serial, int seriallen ){
+ while (len){
+ int l = *payload;
+ //BLE_ESP32::dump(temp, 40, payload, l+1);
+ //AddLog(LOG_LEVEL_ERROR,PSTR("EQ3: %s"), temp);
+
+ payload++;
+ len--;
+ if (len < l){
+ //AddLog(LOG_LEVEL_ERROR,PSTR("EQ3: part len er %d<%d"),len, l);
+ return 0;
+ }
+ switch (*payload){
+ case 0xff: {// parse the EQ3 advert payload looking for nnFF01ssssssss
+ payload++;
+ len--;
+ l--;
+ if (*payload == 1){
+ payload++;
+ len--;
+ l--;
+ //char serialstr[20];
+ //strncpy(serialstr, (const char *)payload, l);
+ //AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3: adv part FF01 detected %s"), serialstr);
+ // we don;t use these, but that's what they seem to be....
+ uint8_t copylen = (l > seriallen)?seriallen:l;
+ strncpy(serial, (const char *)payload, copylen);
+ serial[seriallen-1] = 0;
+ payload += l;
+ len -= l;
+ return 1;
+ } else {
+ payload += l;
+ len -= l;
+ }
+ } break;
+ case 0x09: {
+ payload++;
+ len--;
+ l--;
+ if (*payload == 1){
+ payload++;
+ len--;
+ l--;
+ //char serialstr[20];
+ //strncpy(serialstr, (const char *)payload, l);
+ //AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3: adv part FF01 detected %s"), serialstr);
+ // we don;t use these, but that's what they seem to be....
+ uint8_t copylen = (l > namelen)?namelen:l;
+ strncpy(name, (const char *)payload, copylen);
+ name[namelen-1] = 0;
+ payload += l;
+ len -= l;
+ //return 1;
+ } else {
+ payload += l;
+ len -= l;
+ }
+ } break;
+ default:{
+ payload += l;
+ len -= l;
+ } break;
+ }
+ }
+ return 0;
+}
+
+int ispairing(const uint8_t *payload, int len){
+ //char temp[40];
+ //BLE_ESP32::dump(temp, 40, payload, len);
+ //AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3: pair%d %s"), len, temp);
+ while (len){
+ int l = *payload;
+ //BLE_ESP32::dump(temp, 40, payload, l+1);
+ //AddLog(LOG_LEVEL_ERROR,PSTR("EQ3: %s"), temp);
+
+ payload++;
+ len--;
+ if (len < l){
+ //AddLog(LOG_LEVEL_ERROR,PSTR("EQ3: part len er %d<%d"),len, l);
+ return 0;
+ }
+ if (*payload == 0xff){
+ payload++;
+ len--;
+ l--;
+ if (*payload == 1){
+ payload++;
+ len--;
+ l--;
+ //char serialstr[20];
+ //strncpy(serialstr, (const char *)payload, l);
+ //AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3: adv part FF01 detected %s"), serialstr);
+ // we don;t use these, but that's what they seem to be....
+ const uint8_t *serial = payload;
+ uint8_t seriallen = l;
+ payload += l;
+ len -= l;
+ return 1;
+ } else {
+ payload += l;
+ len -= l;
+ }
+ } else {
+ payload += l;
+ len -= l;
+ }
+ }
+ return 0;
+}
+
+int TaskEQ3AddDevice(int8_t RSSI, const uint8_t* addr, char *serial){
+ int free = -1;
+ int i = 0;
+ uint64_t now = esp_timer_get_time();
+
+ if (serial && serial[0] && !pairing){
+ memcpy(pairingaddr, addr, 6);
+ strncpy(pairingserial, serial, sizeof(pairingserial));
+ pairingserial[sizeof(pairingserial)-1] = 0;
+ pairing = 1;
+ }
+
+ for(i = 0; i < EQ3_NUM_DEVICESLOTS; i++){
+ if(memcmp(addr,EQ3Devices[i].addr,6)==0){
+ break;
+ }
+ if (EQ3Devices[i].timeoutTime && (EQ3Devices[i].timeoutTime < now)) {
+#ifdef EQ3_DEBUG
+ AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3 %s timeout at %d"), addrStr(EQ3Devices[i].addr), i);
+#endif
+ EQ3Devices[i].timeoutTime = 0L;
+ }
+ if (!EQ3Devices[i].timeoutTime){
+ if (free == -1){
+ free = i;
+ }
+ }
+ }
+
+ if (i == EQ3_NUM_DEVICESLOTS){
+ if (free >= 0){
+ i = free;
+ } else {
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 lost %s: > %d devices"), addrStr(addr), EQ3_NUM_DEVICESLOTS);
+ return 0;
+ }
+ }
+
+#ifdef EQ3_DEBUG
+ if (!EQ3Devices[i].timeoutTime)
+ AddLog(LOG_LEVEL_INFO,PSTR("EQ3 %s: added at %d"), addrStr(addr), i);
+#endif
+
+ EQ3Devices[i].timeoutTime = now + (1000L*1000L)*EQ3_TIMEOUT;
+ memcpy(EQ3Devices[i].addr, addr, 6);
+ EQ3Devices[i].RSSI = RSSI;
+
+ EQ3Devices[i].pairing = (serial && serial[0])?1:0;
+
+ return 1;
+}
+
+
+const char *EQ3Names[] = {
+ "CC-RT-BLE",
+ "CC-RT-BLE-EQ",
+ "CC-RT-M-BLE"
+};
+
+int TaskEQ3advertismentCallback(BLE_ESP32::ble_advertisment_t *pStruct)
+{
+ // we will try not to use this...
+ BLEAdvertisedDevice *advertisedDevice = pStruct->advertisedDevice;
+
+ std::string sname = advertisedDevice->getName();
+
+ bool found = false;
+ const char *nameStr = sname.c_str();
+ int8_t RSSI = pStruct->RSSI;
+ const uint8_t *addr = pStruct->addr;
+
+
+ const char *alias = BLE_ESP32::getAlias(addr);
+ if (EQ3OnlyAliased){
+ // ignore unless we have an alias.
+ if (!alias || !(*alias)){
+ return 0;
+ }
+ }
+ if (!alias) alias = "";
+
+ for (int i = 0; i < sizeof(EQ3Names)/sizeof(*EQ3Names); i++){
+ if (!strcmp(nameStr, EQ3Names[i])){
+ found = true;
+ break;
+ }
+ }
+
+ if (!found && !strncmp(alias, "EQ3", 3)){
+ found = true;
+ }
+
+ // if the addr matches the EQ2 mfg prefix, add it?
+ if (!found && EQ3MatchPrefix && (matchPrefix(addr) >= 0)){
+ found = true;
+ }
+
+ if (!found) return 0;
+
+#ifdef EQ3_DEBUG
+ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3Device: saw %s"),advertisedDevice->getAddress().toString().c_str());
+#endif
+
+ uint8_t* payload = advertisedDevice->getPayload();
+ size_t payloadlen = advertisedDevice->getPayloadLength();
+
+ char name[20] = {0};
+ char serial[20] = {0};
+ int pairing = 0;
+ ispairing2(payload, payloadlen, name, 20, serial, 20);
+
+ // this will take and keep the mutex until the function is over
+ TasAutoMutex localmutex(&EQ3mutex);
+ TaskEQ3AddDevice(RSSI, addr, serial);
+ return 0;
+}
+
+
+
+
+/*********************************************************************************************\
+ * Helper functions
+\*********************************************************************************************/
+
+
+
+/*********************************************************************************************\
+ * init
+\*********************************************************************************************/
+void EQ3Init(void) {
+ memset(&EQ3Devices, 0, sizeof(EQ3Devices));
+ BLE_ESP32::registerForAdvertismentCallbacks((const char *)"EQ3", TaskEQ3advertismentCallback);
+#ifdef EQ3_DEBUG
+ AddLog(LOG_LEVEL_INFO,PSTR("EQ3: init: request callbacks"));
+#endif
+
+ EQ3Period = Settings->tele_period;
+
+ return;
+}
+
+/***********************************************************************\
+ * Regular
+\***********************************************************************/
+
+void EQ3Every50mSecond(){
+
+}
+
+/**
+ * @brief Main loop of the driver, "high level"-loop
+ *
+ */
+int EQ3Send(const uint8_t* addr, const char *cmd, char* param, char* param2, int useAlias);
+
+void EQ3EverySecond(bool restart){
+ if (pairing){
+ char p[40]; // used in dump
+ BLE_ESP32::dump(p, 20, pairingaddr, 6);
+ Response_P(PSTR("{\"pairing\":\"%s\",\"serial\":\"%s\"}"), p, pairingserial);
+ char addrstr[4+8*2+2] = "EQ3/";
+ BLE_ESP32::dump(&addrstr[4], 8*2+2, pairingaddr, 6);
+ char *topic = topicPrefix(STAT, pairingaddr, 1);
+ MqttPublish(topic, false);
+ pairing = 0;
+ }
+
+ seconds --;
+ if (seconds <= 0){
+ if (EQ3Period){
+ if (nextEQ3Poll >= EQ3_NUM_DEVICESLOTS){
+ AddLog(LOG_LEVEL_DEBUG, PSTR("EQ3 poll cycle starting"));
+ nextEQ3Poll = 0;
+ } else {
+ AddLog(LOG_LEVEL_ERROR, PSTR("EQ3 poll overrun, deferred - last loop only got to %d, not %d"), nextEQ3Poll, EQ3_NUM_DEVICESLOTS);
+ }
+ }
+ seconds = EQ3Period;
+ }
+
+ if (EQ3Period){
+ int qlen = opQueue.size();
+ if ((nextEQ3Poll < EQ3_NUM_DEVICESLOTS) && (qlen == 0) && (!opInProgress)){
+ if (intervalSecondsCounter){
+ intervalSecondsCounter--;
+ } else {
+ // queue a EQ3Status op against each known EQ3.
+ // mark it as a regular stat rather than a use cmd.
+ for(int i = nextEQ3Poll; i < EQ3_NUM_DEVICESLOTS; i++){
+ if (!EQ3Devices[i].timeoutTime){
+ nextEQ3Poll = i+1;
+ continue;
+ }
+
+ // trvMinRSSI
+ // find the device in BLE to get RSSI
+ if (EQ3Devices[i].RSSI < trvMinRSSI){
+ AddLog(LOG_LEVEL_DEBUG, PSTR("EQ3 %s RSSI %d < min %d, poll suppressed"), addrStr(EQ3Devices[i].addr), EQ3Devices[i].RSSI, trvMinRSSI);
+ nextEQ3Poll = i+1;
+ continue;
+ }
+
+ EQ3Send(EQ3Devices[i].addr, PSTR("poll"), nullptr, nullptr, 1);
+ nextEQ3Poll = i+1;
+ intervalSecondsCounter = intervalSeconds;
+ break;
+ }
+ }
+ }
+ }
+
+ // start next op now, if we have any queued
+ EQ3DoOp();
+
+}
+
+
+/*********************************************************************************************\
+ * Presentation
+\*********************************************************************************************/
+int EQ3SendCurrentDevices(){
+ // send the active devices
+ ResponseClear();
+ ResponseAppend_P(PSTR("{\"devices\":{"));
+ int added = 0;
+ for(int i = 0; i < EQ3_NUM_DEVICESLOTS; i++){
+ char p[40];
+ if (!EQ3Devices[i].timeoutTime)
+ continue;
+ if (added){
+ ResponseAppend_P(PSTR(","));
+ }
+ BLE_ESP32::dump(p, 20, EQ3Devices[i].addr, 6);
+ ResponseAppend_P(PSTR("\"%s\":%d"), p, EQ3Devices[i].RSSI);
+ added = 1;
+ }
+ ResponseAppend_P(PSTR("}}"));
+ MqttPublishPrefixTopic_P(STAT, PSTR("EQ3"), false);
+ return 0;
+}
+
+int EQ3SendResult(char *requested, const char *result){
+ // send the result
+ Response_P(PSTR("{\"result\":\"%s\"}"), result);
+ static char stopic[TOPSZ];
+ GetTopic_P(stopic, STAT, TasmotaGlobal.mqtt_topic, PSTR(""));
+ strcat(stopic, PSTR("EQ3/"));
+ strcat(stopic, requested);
+ MqttPublish(stopic, false);
+ return 0;
+}
+
+
+/*********************************************************************************************\
+ * Commands
+\*********************************************************************************************/
+
+void simpletolower(char *p){
+ if (!p) return;
+ while (*p){
+ *p = *p | 0x20;
+ p++;
+ }
+}
+
+//
+// great description here:
+// https://reverse-engineering-ble-devices.readthedocs.io/en/latest/protocol_description/00_protocol_description.html
+// not all implemented yet.
+//
+int EQ3Send(const uint8_t* addr, const char *cmd, char* param, char* param2, int useAlias){
+
+ char p[] = "";
+ if (!param) param = p;
+ if (!param2) param2 = p;
+ uint8_t d[20];
+ memset(d, 0, sizeof(d));
+ int dlen = 0;
+#ifdef EQ3_DEBUG
+ AddLog(LOG_LEVEL_INFO,PSTR("EQ3 %s: cmd: [%s] [%s] [%s]"), addrStr(addr), cmd, param, param2);
+#endif
+
+/* done on whole string before here.
+ simpletolower(cmd);
+ simpletolower(param);
+ simpletolower(param2);
+*/
+
+ int cmdtype = 0;
+
+ do {
+ if (!strcmp(cmd, "raw")){
+ cmdtype = 1;
+ if (!param || param[0] == 0){
+ return -1;
+ }
+ int len = strlen(param) / 2;
+ if (len > 20){
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 raw len of %s = %d > 20"), param, len);
+ return -1;
+ }
+ BLE_ESP32::fromHex(d, param, len);
+ dlen = len;
+ break;
+ }
+
+/* if (!strcmp(cmd, "state")){
+ d[0] = 0x03;
+ dlen = 1;
+ break;
+ }
+*/
+ if (!strcmp(cmd, "settime") || !strcmp(cmd, "state") || !strcmp(cmd, "poll")){
+ if (!strcmp(cmd, "poll")){
+ cmdtype = 0;
+ }
+ if (!strcmp(cmd, "state")){
+ cmdtype = 2;
+ }
+ if (!strcmp(cmd, "settime")){
+ cmdtype = 3;
+ }
+ if (!param || param[0] == 0){
+
+ if (RtcTime.valid) {
+ d[0] = 0x03;
+ d[1] = (RtcTime.year % 100);
+ d[2] = RtcTime.month;
+ d[3] = RtcTime.day_of_month;
+ d[4] = RtcTime.hour;
+ d[5] = RtcTime.minute;
+ d[6] = RtcTime.second;
+ } else {
+ return -1;
+ }
+
+ // time_t now = 0;
+ // struct tm timeinfo = { 0 };
+ // time(&now);
+ // localtime_r(&now, &timeinfo);
+ // d[0] = 0x03;
+ // d[1] = timeinfo.tm_year % 100;
+ // d[2] = timeinfo.tm_mon + 1;
+ // d[3] = timeinfo.tm_mday;
+ // d[4] = timeinfo.tm_hour;
+ // d[5] = timeinfo.tm_min;
+ // d[6] = timeinfo.tm_sec;
+
+ } else {
+ d[0] = 0x03;
+ BLE_ESP32::fromHex(d+1, param, 6);
+ }
+ dlen = 7;
+ break;
+ }
+
+ if (!strcmp(cmd, "settemp")){
+ cmdtype = 4;
+ if (!param || param[0] == 0){
+ return -1;
+ }
+ float ftemp = 20;
+ sscanf(param, "%f", &ftemp);
+ if (ftemp < 4.5) ftemp = 4.5;
+ if (ftemp > 30) ftemp = 30;
+ ftemp *= 2;
+ uint8_t ctemp = (uint8_t) ftemp;
+ d[0] = 0x41; d[1] = ctemp; dlen = 2;
+ break;
+ }
+
+ if (!strcmp(cmd, "offset")){
+ cmdtype = 5;
+ if (!param || param[0] == 0){
+ return 0;
+ }
+ float ftemp = 20;
+ sscanf(param, "%f", &ftemp);
+ ftemp *= 2;
+ int8_t ctemp = (int8_t) ftemp;
+ ctemp += 7;
+ d[0] = 0x13; d[1] = ctemp; dlen = 2;
+ break;
+ }
+
+ if (!strcmp(cmd, "setdaynight")){
+ cmdtype = 6;
+ if (!param || param[0] == 0){
+ return -1;
+ }
+ if (!param2 || param2[0] == 0){
+ return -1;
+ }
+ float ftemp = 15;
+ sscanf(param, "%f", &ftemp);
+ if (ftemp < 5) ftemp = 5;
+ ftemp *= 2;
+ uint8_t dtemp = (uint8_t) ftemp;
+
+ ftemp = 20;
+ sscanf(param2, "%f", &ftemp);
+ if (ftemp < 5) ftemp = 5;
+ ftemp *= 2;
+ uint8_t ntemp = (uint8_t) ftemp;
+
+ d[0] = 0x11; d[1] = dtemp; d[2] = ntemp; dlen = 3;
+ break;
+ }
+
+ if (!strcmp(cmd, "setwindowtempdur")){
+ cmdtype = 7;
+ if (!param || param[0] == 0){
+ return -1;
+ }
+ if (!param2 || param2[0] == 0){
+ return -1;
+ }
+ float ftemp = 15;
+ sscanf(param, "%f", &ftemp);
+ if (ftemp < 5) ftemp = 5;
+ ftemp *= 2;
+ uint8_t temp = (uint8_t) ftemp;
+
+ int dur = 0;
+ sscanf(param2, "%d", &dur);
+ d[0] = 0x14; d[1] = temp; d[2] = (dur/5); dlen = 3;
+ break;
+ }
+
+ if (!strcmp(cmd, "setholiday")){
+ cmdtype = 8;
+ //40941C152402
+ // 40 94
+ if (!param || param[0] == 0){
+ return -1;
+ }
+ if (!param2 || param2[0] == 0){
+ return -1;
+ }
+
+ int yy = 0;
+ int mm = 0;
+ int dd = 0;
+ int hour = 0;
+ int min = 0;
+ char *p = param;
+ p = strtok(p, "-");
+ if (!p || p[0] == 0) return -1;
+ sscanf(p, "%d", &yy);
+ p = strtok(nullptr, "-");
+ if (!p || p[0] == 0) return -1;
+ sscanf(p, "%d", &mm);
+ p = strtok(nullptr, ",");
+ if (!p || p[0] == 0) return -1;
+ sscanf(p, "%d", &dd);
+ p = strtok(nullptr, ":");
+ if (!p || p[0] == 0) return -1;
+ sscanf(p, "%d", &hour);
+ p = strtok(nullptr, "");
+ if (!p || p[0] == 0) return -1;
+ sscanf(p, "%d", &min);
+
+ min += hour*60;
+ int tt = min / 30;
+
+ float ftemp = 15;
+ sscanf(param2, "%f", &ftemp);
+ if (ftemp < 5) ftemp = 5;
+ ftemp *= 2;
+ uint8_t temp = (uint8_t) ftemp + 128;
+
+ d[0] = 0x40;
+ d[1] = temp;
+ d[2] = dd;
+ d[3] = yy;
+ d[4] = tt;
+ d[5] = mm;
+ dlen = 6;
+ break;
+ }
+
+
+ if (!strcmp(cmd, "boost")) {
+ cmdtype = 9;
+ d[0] = 0x45; d[1] = 0x01;
+ if (param && (!strcmp(param, "off") || param[0] == '0')){
+ d[1] = 0x00;
+ }
+ dlen = 2; break;
+ }
+ if (!strcmp(cmd, "unboost")) {
+ cmdtype = 10;
+ d[0] = 0x45; d[1] = 0x00; dlen = 2; break; }
+ if (!strcmp(cmd, "lock")) { d[0] = 0x80; d[1] = 0x01;
+ if (param && (!strcmp(param, "off") || param[0] == '0')){
+ d[1] = 0x00;
+ }
+ dlen = 2; break;
+ }
+ if (!strcmp(cmd, "unlock")) { cmdtype = 11; d[0] = 0x80; d[1] = 0x00; dlen = 2; break; }
+ if (!strcmp(cmd, "auto")) { cmdtype = 12; d[0] = 0x40; d[1] = 0x00; dlen = 2; break; }
+ if (!strcmp(cmd, "manual")) { cmdtype = 13; d[0] = 0x40; d[1] = 0x40; dlen = 2; break; }
+ // this is basically 'cancel holiday' - mode auto does that.
+ //if (!strcmp(cmd, "eco")) { cmdtype = 14; d[0] = 0x40; d[1] = 0x80; dlen = 2; break; }
+ if (!strcmp(cmd, "on")) {
+ int res = EQ3Send(addr, "manual", nullptr, nullptr, useAlias);
+ char tmp[] = "30";
+ int res2 = EQ3Send(addr, "settemp", tmp, nullptr, useAlias);
+ return res2;
+ }
+ if (!strcmp(cmd, "off")) {
+ int res = EQ3Send(addr, "manual", nullptr, nullptr, useAlias);
+ char tmp[] = "4.5";
+ int res2 = EQ3Send(addr, "settemp", tmp, nullptr, useAlias);
+ return res2;
+ }
+ if (!strcmp(cmd, "valve")) { cmdtype = 17; d[0] = 0x41; d[1] = 0x3c;
+ if (!param || param[0] == 0){
+ return -1;
+ }
+ if ((!strcmp(param, "off") || param[0] == '0')){
+ d[1] = 0x09;
+ }
+ dlen = 2; break;
+ }
+ if (!strcmp(cmd, "mode")) { cmdtype = 18; d[0] = 0x40; d[1] = 0xff;// invlaid
+
+ if (!param || param[0] == 0){
+ return -1;
+ }
+ if (!strcmp(param, "auto")){
+ d[1] = 0x00;
+ }
+ if (!strcmp(param, "manual")){
+ d[1] = 0x40;
+ }
+ if (!strcmp(param, "on") || !strcmp(param, "heat")) {
+ int res = EQ3Send(addr, "manual", nullptr, nullptr, useAlias);
+ char tmp[] = "30";
+ int res2 = EQ3Send(addr, "settemp", tmp, nullptr, useAlias);
+ return res2;
+ }
+ if (!strcmp(param, "off") || !strcmp(param, "cool")) {
+ int res = EQ3Send(addr, "manual", nullptr, nullptr, useAlias);
+ char tmp[] = "4.5";
+ int res2 = EQ3Send(addr, "settemp", tmp, nullptr, useAlias);
+ return res2;
+ }
+
+ if (d[1] == 0xff){ // no valid mode selection found
+ return -1;
+ }
+ // this is basically 'cancel holiday' - mode auto does that.
+ //if (!strcmp(param, "eco")){
+ // d[1] = 0x80;
+ //}
+ dlen = 2; break;
+ }
+ if (!strcmp(cmd, "day")) { cmdtype = 19; d[0] = 0x43; dlen = 1; break; }
+ if (!strcmp(cmd, "night")) { cmdtype = 20; d[0] = 0x44; dlen = 1; break; }
+
+ if (!strcmp(cmd, "reqprofile")) { cmdtype = 21;
+ if (!param || param[0] == 0){
+ return -1;
+ }
+ d[0] = 0x20; d[1] = atoi(param); dlen = 2;
+ break;
+ }
+
+ if (!strcmp(cmd, "setprofile")) { cmdtype = 22;
+ if (!param || param[0] == 0){
+ return -1;
+ }
+ if (!param2 || param2[0] == 0){
+ return -1;
+ }
+ d[0] = 0x10; d[1] = atoi(param);
+
+ // default
+ uint8_t temps[7] = {0x22,0x22,0x22,0x22,0x22,0x22,0x22};
+ uint8_t times[7] = {0x90,0x90,0x90,0x90,0x90,0x90,0x90};
+
+ // 20.5-17:30,
+ const char *p = strtok(param2, ",");
+ int i = 0;
+ while (p){
+ float t = 17;
+ int mm = 0;
+ int hh = 24;
+ sscanf(p, "%f-%d:%d", &t, &hh, &mm);
+ t *= 2;
+ temps[i] = (uint8_t) t;
+ int time = hh*60+mm;
+ time = time / 10;
+ times[i] = time;
+ p = strtok(nullptr, ",");
+ i++;
+ if (i >= 7) break;
+ }
+
+ // remaining left at 00 00
+ for (int j = 0; j < 7; j++){
+ d[2+j*2] = temps[j];
+ d[2+j*2+1] = times[j];
+ }
+
+ dlen = 2+14;
+ break;
+ }
+
+ break;
+ } while(0);
+
+ if (dlen){
+ dlen = 16;
+ return EQ3QueueOp(addr, d, dlen, cmdtype, useAlias);
+
+ //return EQ3Operation(addr, d, dlen, 4);
+ }
+
+ return -1;
+}
+
+
+const char *responses[] = {
+ PSTR("Done"),
+ PSTR("queued"),
+ PSTR("ignoredbusy"),
+ PSTR("invcmd"),
+ PSTR("cmdfail"),
+ PSTR("invidx"),
+ PSTR("invaddr")
+};
+
+
+int CmndTrvNext(int index, char *data){
+ AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3 cmd index: %d"), index);
+ //simpletolower(data);
+
+ switch(index){
+ case 0:
+ case 1: {
+
+ char *p = strtok(data, " ");
+ bool trigger = false;
+ if (!strcmp(p, "reset")){
+ retries = 0;
+ for (int i = 0; i < EQ3_NUM_DEVICESLOTS; i++){
+ EQ3Devices[i].timeoutTime = 0L;
+ }
+ return 0;
+ }
+
+ if (!strcmp(p, "scan")){
+#ifdef EQ3_DEBUG
+ AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3 cmd: %s"), p);
+#endif
+ EQ3SendCurrentDevices();
+ return 0;
+ }
+ if (!strcmp(p, "devlist")){
+#ifdef EQ3_DEBUG
+ AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3 cmd: %s"), p);
+#endif
+ EQ3SendCurrentDevices();
+ return 0;
+ }
+
+ // only allow one command in progress
+ if (retries){
+ //return 2;
+ }
+
+
+ int useAlias = 0;
+ uint8_t addrbin[7];
+ int addrres = BLE_ESP32::getAddr(addrbin, p);
+ if (addrres){
+ if (addrres == 2){
+ AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3 addr used alias: %s"), p);
+ useAlias = 1;
+ }
+ NimBLEAddress addr(addrbin, addrbin[6]);
+
+#ifdef EQ3_DEBUG
+ //AddLog(LOG_LEVEL_INFO,PSTR("EQ3 cmd addr: %s -> %s"), p, addr.toString().c_str());
+#endif
+ } else {
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 addr invalid: %s"), p);
+ return 3;
+ }
+
+ // get next part of cmd
+ char *cmd = strtok(nullptr, " ");
+ if (!cmd){
+ return 3;
+ }
+
+ char *param = strtok(nullptr, " ");
+ char *param2 = nullptr;
+ if (param){
+ param2 = strtok(nullptr, " ");
+ }
+ int res = EQ3Send(addrbin, cmd, param, param2, useAlias);
+ if (res > 0) {
+ // succeeded to queue
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 queued"));
+ return 1;
+ }
+
+ if (res < 0) { // invalid in some way
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 invalid"));
+ return 3;
+ }
+
+ AddLog(LOG_LEVEL_ERROR,PSTR("EQ3 failed to queue"));
+ // failed to queue
+ return 4;
+ } break;
+
+ case 2:
+ retries = 0;
+ return 0;
+ break;
+ }
+
+ return 4;
+}
+
+void CmndTrv(void) {
+ int res = CmndTrvNext(XdrvMailbox.index, XdrvMailbox.data);
+ ResponseCmndChar(responses[res]);
+}
+
+void CmndTrvPeriod(void) {
+ if (XdrvMailbox.data_len > 0) {
+ if (1 == XdrvMailbox.payload){
+ seconds = 0;
+ } else {
+ EQ3Period = XdrvMailbox.payload;
+ if (seconds > EQ3Period){
+ seconds = EQ3Period;
+ }
+ }
+ }
+ ResponseCmndNumber(EQ3Period);
+}
+
+void CmndTrvOnlyAliased(void){
+ if (XdrvMailbox.data_len > 0) {
+ EQ3OnlyAliased = XdrvMailbox.payload;
+ }
+ ResponseCmndNumber(EQ3OnlyAliased);
+}
+
+void CmndTrvMatchPrefix(void){
+ if (XdrvMailbox.data_len > 0) {
+ EQ3MatchPrefix = XdrvMailbox.payload;
+ }
+ ResponseCmndNumber(EQ3MatchPrefix);
+}
+
+void CmndTrvMinRSSI(void){
+ if (XdrvMailbox.data_len > 0) {
+ trvMinRSSI = atoi(XdrvMailbox.data);
+ }
+ // signed number
+ Response_P(PSTR("{\"%s\":%d}"), XdrvMailbox.command, trvMinRSSI);
+}
+
+void CmndTrvHideFailedPoll(void){
+ if (XdrvMailbox.data_len > 0) {
+ EQ3HideFailedPoll = XdrvMailbox.payload;
+ }
+ ResponseCmndNumber(EQ3HideFailedPoll);
+}
+
+
+#define EQ3_TOPIC "EQ3"
+static char tmp[120];
+
+bool mqtt_direct(){
+ char stopic[TOPSZ];
+ strncpy(stopic, XdrvMailbox.topic, TOPSZ);
+ XdrvMailbox.topic[TOPSZ-1] = 0;
+
+ AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3 mqtt: %s:%s"), stopic, XdrvMailbox.data);
+
+ char *items[10];
+ char *p = stopic;
+ int cnt = 0;
+ do {
+ items[cnt] = strtok(p, "/");
+ cnt++;
+ p = nullptr;
+ } while (items[cnt-1]);
+ cnt--; // repreents the number of items
+
+ if (cnt < 4){ // not for us?
+ //AddLog(LOG_LEVEL_INFO,PSTR("cnt: %d < 4"), cnt);
+ return false;
+ }
+
+ for (int i = 0; i < cnt; i++){
+ //AddLog(LOG_LEVEL_INFO,PSTR("cnt %d:%s"), i, items[i]);
+ }
+
+
+ int EQ3index = 0;
+ int MACindex = 0;
+ int CMDindex = 0;
+ if (strcasecmp_P(items[cnt-3], PSTR(EQ3_TOPIC)) != 0) {
+ //AddLog(LOG_LEVEL_INFO,PSTR("cnt-3 not %s"), PSTR(EQ3_TOPIC));
+ if (strcasecmp_P(items[cnt-2], PSTR(EQ3_TOPIC)) != 0) {
+ //AddLog(LOG_LEVEL_INFO,PSTR("cnt-2 not %s"), PSTR(EQ3_TOPIC));
+ return false; // not for us
+ } else {
+ EQ3index = cnt-2;
+ MACindex = cnt-1;
+ }
+ } else {
+ EQ3index = cnt-3;
+ MACindex = cnt-2;
+ CMDindex = cnt-1;
+ }
+
+ int remains = 120;
+ memset(tmp, 0, sizeof(tmp));
+ p = tmp;
+ uint8_t addr[7];
+ int useAlias = BLE_ESP32::getAddr(addr, items[MACindex]);
+ int res = 6; // invalid address/alias
+
+ // if address or alias valid
+ if (useAlias){
+ strncpy(p, items[MACindex], remains-6);
+ p += strlen(p);
+ *(p++) = 0x20;
+ remains = 120 - (p-tmp);
+
+ if (CMDindex){
+ strncpy(p, items[CMDindex], remains-6);
+ p += strlen(p);
+ *(p++) = 0x20;
+ remains = 120 - (p-tmp);
+ }
+
+ strncpy(p, XdrvMailbox.data, remains-6);
+ p += strlen(p);
+ *(p++) = 0x20;
+ remains = 120 - (p-tmp);
+ *(p++) = 0;
+
+ AddLog(LOG_LEVEL_DEBUG,PSTR("EQ3:mqtt->cmdstr %s"), tmp);
+ res = CmndTrvNext(1, tmp);
+ }
+
+ // post result to stat/tas/EQ3/ {"result":""}
+ EQ3SendResult(items[MACindex], responses[res]);
+
+ return true;
+}
+
+
+///////////////////////////////////////////////
+// starts a completely fresh MQTT message.
+// sends ONE sensor's worth of HA discovery msg
+const char EQ3_HA_DISCOVERY_TEMPLATE[] PROGMEM =
+ "{\"availability\":[],\"device\":"
+ "{\"identifiers\":[\"BLE%s\"],"
+ "\"name\":\"%s\","
+ "\"manufacturer\":\"tas\","
+ "\"model\":\"%s\","
+ "\"via_device\":\"%s\""
+ "},"
+ "\"dev_cla\":\"%s\","
+ "\"expire_after\":600,"
+ "\"json_attr_t\":\"%s\","
+ "\"name\":\"%s_%s\","
+ "\"state_topic\":\"%s\","
+ "\"uniq_id\":\"%s_%s\","
+ "\"unit_of_meas\":\"%s\","
+ "\"val_tpl\":\"{{ value_json.%s }}\"}";
+
+///////////TODO - unfinished.....
+void EQ3DiscoveryOneEQ3(){
+ // don't detect half-added ones here
+ if (EQ3CurrentSingleSlot >= EQ3_NUM_DEVICESLOTS){
+ // if we got to the end of the sensors, then don't send more
+ return;
+ }
+
+#ifdef USE_HOME_ASSISTANT
+ if(Settings->flag.hass_discovery){
+ eq3_device_tag *p;
+ do {
+ p = &EQ3Devices[EQ3CurrentSingleSlot];
+ if (0 == p->timeoutTime){
+ EQ3CurrentSingleSlot++;
+ }
+ } while ((0 == p->timeoutTime) && (EQ3CurrentSingleSlot <= EQ3_NUM_DEVICESLOTS));
+
+ if (EQ3CurrentSingleSlot >= EQ3_NUM_DEVICESLOTS){
+ return;
+ }
+
+ // careful - a missing comma causes a crash!!!!
+ // because of the way we loop?
+ const char *classes[] = {
+ "temperature",
+ "temp",
+ "°C",
+ "signal_strength",
+ "RSSI",
+ "dB"
+ };
+
+ int datacount = (sizeof(classes)/sizeof(*classes))/3;
+
+ if (p->nextDiscoveryData >= datacount){
+ p->nextDiscoveryData = 0;
+ }
+
+ char DiscoveryTopic[80];
+ const char *host = NetworkHostname();
+ const char *devtype = PSTR("EQ3");
+ char idstr[32];
+ const char *alias = BLE_ESP32::getAlias(p->addr);
+ const char *id = idstr;
+ if (alias && *alias){
+ id = alias;
+ } else {
+ sprintf(idstr, PSTR("%s%02x%02x%02x"),
+ devtype,
+ p->addr[3], p->addr[4], p->addr[5]);
+ }
+
+ char SensorTopic[60];
+ sprintf(SensorTopic, "stat/%s/EQ3/%s",
+ host, id);
+
+ //int i = p->nextDiscoveryData*3;
+ for (int i = 0; i < datacount*3; i += 3){
+ if (!classes[i] || !classes[i+1] || !classes[i+2]){
+ return;
+ }
+
+ ResponseClear();
+
+ /*
+ {"availability":[],"device":{"identifiers":["TasmotaBLEa4c1387fc1e1"],"manufacturer":"simon","model":"someBLEsensor","name":"TASBLEa4c1387fc1e1","sw_version":"0.0.0"},"dev_cla":"temperature","json_attr_t":"stat/tasmota_esp32/SENSOR","name":"TASLYWSD037fc1e1Temp","state_topic":"tele/tasmota_esp32/SENSOR","uniq_id":"Tasmotaa4c1387fc1e1temp","unit_of_meas":"°C","val_tpl":"{{ value_json.LYWSD037fc1e1.Temperature }}"}
+ {"availability":[],"device":{"identifiers":["TasmotaBLEa4c1387fc1e1"],
+ "name":"TASBLEa4c1387fc1e1"},"dev_cla":"temperature",
+ "json_attr_t":"tele/tasmota_esp32/SENSOR",
+ "name":"TASLYWSD037fc1e1Temp","state_topic": "tele/tasmota_esp32/SENSOR",
+ "uniq_id":"Tasmotaa4c1387fc1e1temp","unit_of_meas":"°C",
+ "val_tpl":"{{ value_json.LYWSD037fc1e1.Temperature }}"}
+ */
+
+ ResponseAppend_P(EQ3_HA_DISCOVERY_TEMPLATE,
+ //"{\"identifiers\":[\"BLE%s\"],"
+ id,
+ //"\"name\":\"%s\"},"
+ id,
+ //\"model\":\"%s\",
+ devtype,
+ //\"via_device\":\"%s\"
+ host,
+ //"\"dev_cla\":\"%s\","
+ classes[i],
+ //"\"json_attr_t\":\"%s\"," - the topic the sensor publishes on
+ SensorTopic,
+ //"\"name\":\"%s_%s\"," - the name of this DATA
+ id, classes[i+1],
+ //"\"state_topic\":\"%s\"," - the topic the sensor publishes on?
+ SensorTopic,
+ //"\"uniq_id\":\"%s_%s\"," - unique for this data,
+ id, classes[i+1],
+ //"\"unit_of_meas\":\"%s\"," - the measure of this type of data
+ classes[i+2],
+ //"\"val_tpl\":\"{{ value_json.%s }}") // e.g. Temperature
+ classes[i+1]
+ //
+ );
+
+ sprintf(DiscoveryTopic, "homeassistant/sensor/%s/%s/config",
+ id, classes[i+1]);
+
+ MqttPublish(DiscoveryTopic);
+ p->nextDiscoveryData++;
+ //vTaskDelay(100/ portTICK_PERIOD_MS);
+ }
+ } // end if hass discovery
+ //AddLog_P(LOG_LEVEL_DEBUG,PSTR("M32: %s: show some %d %s"),D_CMND_MI32, MI32.mqttCurrentSlot, TasmotaGlobal.mqtt_data);
+#endif //USE_HOME_ASSISTANT
+
+}
+
+
+
+
+} // end namespace EQ3_ESP32
+
+/*********************************************************************************************\
+ * Interface
+\*********************************************************************************************/
+
+bool Xdrv85(uint8_t function)
+{
+ bool result = false;
+
+ switch (function) {
+ case FUNC_INIT:
+ EQ3_ESP32::EQ3Init();
+ break;
+ case FUNC_EVERY_50_MSECOND:
+ EQ3_ESP32::EQ3Every50mSecond();
+ break;
+ case FUNC_EVERY_SECOND:
+ EQ3_ESP32::EQ3EverySecond(false);
+ break;
+ case FUNC_COMMAND:
+ result = DecodeCommand(EQ3_ESP32::kEQ3_Commands, EQ3_ESP32::EQ3_Commands);
+ break;
+ case FUNC_MQTT_DATA:
+ //AddLog(LOG_LEVEL_INFO,PSTR("topic %s"), XdrvMailbox.topic);
+ result = EQ3_ESP32::mqtt_direct();
+ break;
+ case FUNC_JSON_APPEND:
+ break;
+#ifdef USE_WEBSERVER
+ case FUNC_WEB_SENSOR:
+ break;
+#endif // USE_WEBSERVER
+ }
+ return result;
+}
+#endif //
+#endif // ESP32
+
+#endif
+#endif // CONFIG_IDF_TARGET_ESP32