From e1111ca98e4c0443cc29c876e245cec59fc6fa54 Mon Sep 17 00:00:00 2001 From: Simon Hailes Date: Fri, 10 Sep 2021 13:56:24 +0100 Subject: [PATCH] Add driver xdrv_85_BLE_EQ3_TRV --- tasmota/xdrv_85_BLE_EQ3_TRV.ino | 1767 +++++++++++++++++++++++++++++++ 1 file changed, 1767 insertions(+) create mode 100644 tasmota/xdrv_85_BLE_EQ3_TRV.ino 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