/* xsns_62_esp32_mi_ble.ino - MI-BLE-sensors via ESP32 support for Tasmota enabled by ESP32 && USE_BLE_ESP32 if (ESP32 && !USE_BLE_ESP32) then xsns_62_esp32_mi.ino is used - the older driver Copyright (C) 2020 Christian Baars and Theo Arends Also Simon Hailes and Robert Klauco 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 -------------------------------------------------------------------------------------------- 0.9.2.1 20210217 changed - make features alos depend on received data - i.e. 'unknown' devices will show what they send. Add MI32Option6 1 to switch to tele/tasmota_ble/ style MQTT independent of HASS discovery. ------- 0.9.2.0 20210127 changed - Officially includes as the mi driver when using USE_BLE_ESP32. ------- 0.9.1.9 20201226 changed - All change now. ------- 0.9.1.7 20201116 changed - small bugfixes, add BLOCK and OPTION command, send BLE scan via MQTT ------- 0.9.1.6 20201022 changed - Beacon support, RSSI at TELEPERIOD, refactoring ------- 0.9.1.5 20201021 changed - HASS related ('null', hold back discovery), number of found sensors for RULES ------- 0.9.1.4 20201020 changed - use BearSSL for decryption, revert to old TELEPERIOD-cycle as default ------- 0.9.1.3 20200926 changed - Improve HA discovery, make key+MAC case insensitive ------- 0.9.1.3 20200916 changed - add ATC (custom FW for LYWSD03MMC), API adaption for NimBLE-Arduino 1.0.2 ------- 0.9.1.2 20200802 changed - add MHO-C303 ------- 0.9.1.1 20200715 changed - add MHO-C401, refactoring ------- 0.9.1.0 20200712 changed - add lights and yeerc, add pure passive mode with decryption, lots of refactoring ------- 0.9.0.1 20200706 changed - adapt to new NimBLE-API, tweak scan process ------- 0.9.0.0 20200413 started - initial development by Christian Baars forked - from arendst/tasmota - https://github.com/arendst/Tasmota */ //#define VSCODE_DEV /* #ifdef VSCODE_DEV #define ESP32 #define USE_BLE_ESP32 #define USE_MI_ESP32 #endif */ //#undef USE_MI_ESP32 // for testing of BLE_ESP32, we remove xsns_62_MI_ESP32.ino completely, and instead add this modified xsns_52_ibeacon_BLE_ESP32.ino #ifdef USE_BLE_ESP32 #ifdef ESP32 // ESP32 family only. Use define USE_HM10 for ESP8266 support #if defined CONFIG_IDF_TARGET_ESP32 || defined CONFIG_IDF_TARGET_ESP32C3 #ifdef USE_MI_ESP32 #define XSNS_62 62 #define USE_MI_DECRYPTION #include #ifdef USE_MI_DECRYPTION #include #endif //USE_MI_DECRYPTION void MI32scanEndedCB(NimBLEScanResults results); void MI32notifyCB(NimBLERemoteCharacteristic* pRemoteCharacteristic, uint8_t* pData, size_t length, bool isNotify); struct { uint16_t perPage = 4; uint8_t mqttCurrentSlot = 0; uint8_t mqttCurrentSingleSlot = 0; uint32_t period; // set manually in addition to TELE-period, is set to TELE-period after start int secondsCounter = 0; // counts up in MI32EverySecond to period int secondsCounter2 = 0; // counts up in MI32EverySecond to period union { struct { uint32_t init:1; uint32_t shallClearResults:1; // BLE scan results uint32_t shallShowStatusInfo:1; // react to amount of found sensors via RULES uint32_t firstAutodiscoveryDone:1; uint32_t shallTriggerTele:1; uint32_t triggeredTele:1; }; uint32_t all = 0; } mode; struct { // the slot currently having it's battery read // set to 0 to start a battery read cycle uint8_t slot = 255; uint8_t active = 0; } batteryreader; struct { // the slot currently having it's battery read // set to 0 to start a battery read cycle uint8_t slot = 255; uint8_t active = 0; } sensorreader; struct { uint32_t allwaysAggregate:1; // always show all known values of one sensor in brdigemode uint32_t noSummary:1; // no sensor values at TELE-period uint32_t directBridgeMode:1; // send every received BLE-packet as a MQTT-message in real-time uint32_t holdBackFirstAutodiscovery:1; // allows to trigger it later uint32_t showRSSI:1; uint32_t ignoreBogusBattery:1; uint32_t minimalSummary:1; // DEPRECATED!! uint32_t onlyAliased:1; // only include sensors that are aliased uint32_t MQTTType:1; } option; } MI32; #pragma pack(1) // byte-aligned structures to read the sensor data struct { int16_t temp; uint8_t hum; uint16_t volt; // LYWSD03 only } LYWSD0x_HT; struct { uint8_t spare; int16_t temp; uint16_t hum; } CGD1_HT; struct { int16_t temp; uint8_t spare; uint32_t lux; uint8_t moist; uint16_t fert; } Flora_TLMF; // temperature, lux, moisture, fertility //////////////////////////////////////////////////////////// // from https://github.com/Magalex2x14/LYWSD03MMC-info struct mi_beacon_frame_data_t{ // data from byte 0 - e.g. 30 uint8_t meshflag; //Byte 0: x....... uint8_t dataflag; //Byte 0: .x...... uint8_t compatibilityflag; //Byte 0: ..x..... - indicates compatibility data present uint8_t MACFlag; //Byte 0: ...x.... uint8_t isencrypted; //Byte 0: ....x... uint8_t reserved; //Byte 0: .....xxx // data from byte 1 - e.g. 58 uint8_t version; //Byte 0: xxxx.... uint8_t authMode; //Byte 0: ....xx.. // e.g. 2 uint8_t bindingvalidreq; //Byte 0: ......x. uint8_t registeredflag; //Byte 0: .......x }; struct mi_beacon_compatibility_data_t{ // e.g. 28/08 uint8_t reserved; //Byte 0: xx...... uint8_t IOcap; //Byte 0: ..x..... uint8_t bondability; //Byte 0: ...xx... uint8_t unused; //Byte 0: .....xxx uint16_t IOCapability; // bytes 1-2, e.g. 01 00 -> 0001 }; struct mi_beacon_mac_data_t{ // e.g. 28/08 uint8_t mac[6]; }; struct mi_beacon_payload_data_t{ // uint16_t type; uint8_t size; uint8_t data[16]; }; struct mi_beacon_data_t { // mi_beacon_frame_data_t framedata; uint16_t devicetype; uint8_t framecnt; mi_beacon_mac_data_t macdata; mi_beacon_compatibility_data_t compatibility; uint8_t payloadpresent; uint8_t needkey; // we need a (new) encryption key? mi_beacon_payload_data_t payload; }; struct mi_beacon_data_payload_data_t { // union { struct{ //01 uint16_t num; uint8_t longPress; } Btn; int16_t temp; //04 uint16_t hum; //06 uint32_t lux; //07 uint8_t moist; //08 uint16_t fert; //09 uint8_t bat; //0a struct{ //0d int16_t temp; uint16_t hum; } HT; uint32_t NMT; //17 }; }; /////////////////////////////////////////////////////////// union mi_bindKey_t{ struct{ uint8_t key[16]; uint8_t MAC[6]; }; uint8_t buf[22]; }; struct ATCPacket_t{ //uint8_t size; // = 16? //uint8_t uid; // = 0x16, 16-bit UUID //uint16_t UUID; // = 0x181A, GATT Service 0x181A Environmental Sensing uint8_t MAC[6]; // [0] - hi, .. [6] - lo digits uint16_t temp; //sadly this is in wrong endianess uint8_t hum; uint8_t batPer; uint16_t batMV; uint8_t frameCnt; }; // GATT Service 0x181A Environmental Sensing // All data little-endian struct PVVXPacket_t { //uint8_t size; // = 19 //uint8_t uid; // = 0x16, 16-bit UUID //uint16_t UUID; // = 0x181A, GATT Service 0x181A Environmental Sensing uint8_t MAC[6]; // [0] - lo, .. [6] - hi digits int16_t temperature; // x 0.1 degree uint16_t humidity; // x 0.01 % uint16_t battery_mv; // mV uint8_t battery_level; // 0..100 % uint8_t counter; // measurement count uint8_t flags; }; struct MiScaleV1Packet_t { //uint8_t size; // = 14 //uint8_t uid; // = 0x16, 16-bit UUID //uint16_t UUID; // = 0x181D uint8_t status; // bit 0 lbs, 4 jin, 5, stabilized, 7, weight removed uint16_t weight; uint16_t year; uint8_t month; uint8_t day; uint8_t hour; uint8_t minute; uint8_t second; }; struct MiScaleV2Packet_t { //uint8_t size; // = 17 //uint8_t uid; // = 0x16, 16-bit UUID //uint16_t UUID; // = 0x181B uint8_t weight_unit; uint8_t status; uint16_t year; uint8_t month; uint8_t day; uint8_t hour; uint8_t minute; uint8_t second; uint16_t impedance; uint16_t weight; }; #pragma pack(0) struct mi_sensor_t{ uint8_t type; //MI_Flora = 1; MI_MI-HT_V1=2; MI_LYWSD02=3; MI_LYWSD03=4; MI_CGG1=5; MI_CGD1=6 uint8_t needkey; // tells http to display needkey message with link uint8_t lastCnt; //device generated counter of the packet uint8_t nextDiscoveryData; // used to lkimit discovery to one MQTT per sec uint8_t shallSendMQTT; uint8_t MAC[6]; union { struct { uint32_t temp:1; uint32_t hum:1; uint32_t tempHum:1; //every hum sensor has temp too, easier to use Tasmota dew point functions uint32_t lux:1; uint32_t moist:1; uint32_t fert:1; uint32_t bat:1; uint32_t NMT:1; uint32_t PIR:1; uint32_t Btn:1; uint32_t events:1; uint32_t pairing:1; uint32_t light:1; // binary light sensor uint32_t scale:1; }; uint32_t raw; } feature; union { struct { uint32_t temp:1; uint32_t hum:1; uint32_t tempHum:1; //can be combined from the sensor uint32_t lux:1; uint32_t moist:1; uint32_t fert:1; uint32_t bat:1; uint32_t NMT:1; uint32_t motion:1; uint32_t noMotion:1; uint32_t Btn:1; uint32_t PairBtn:1; uint32_t light:1; // binary light sensor uint32_t scale:1; }; uint32_t raw; } eventType; int RSSI; uint8_t pairing; int8_t light; // binary light sensor - initialise to -1 int16_t Btn; // moved so we can initialise to -1 uint32_t lastTime; uint32_t lux; float temp; //Flora, MJ_HT_V1, LYWSD0x, CGx union { struct { uint8_t moisture; uint16_t fertility; char firmware[6]; // actually only for FLORA but hopefully we can add for more devices }; // Flora struct { float hum; }; // MJ_HT_V1, LYWSD0x struct { uint16_t events; //"alarms" since boot uint32_t NMT; // no motion time in seconds for the MJYD2S }; }; union { uint8_t bat; // many values seem to be hard-coded garbage (LYWSD0x, GCD1) }; union { struct { uint8_t has_impedance; uint8_t stabilized; uint8_t weight_removed; char weight_unit[4]; // kg, lbs, jin or empty when unknown float weight; uint16_t impedance; }; }; }; struct MAC_t { uint8_t buf[7]; }; std::vector MIBLEsensors; std::vector MIBLEbindKeys; std::vector MIBLEBlockList; SemaphoreHandle_t slotmutex = (SemaphoreHandle_t) nullptr; /*********************************************************************************************\ * constants \*********************************************************************************************/ #define D_CMND_MI32 "MI32" const char kMI32_Commands[] PROGMEM = D_CMND_MI32 "|" #ifdef USE_MI_DECRYPTION "Key|" "Keys|" #endif // USE_MI_DECRYPTION "Period|Time|Page|Battery|Unit|Block|Option"; void (*const MI32_Commands[])(void) PROGMEM = { #ifdef USE_MI_DECRYPTION &CmndMi32Key, &CmndMi32Keys, #endif // USE_MI_DECRYPTION &CmndMi32Period, &CmndMi32Time, &CmndMi32Page, &CmndMi32Battery, &CmndMi32Unit, &CmndMi32Block, &CmndMi32Option }; #define MI_UNKOWN 1 #define MI_FLORA 2 #define MI_MJ_HT_V1 3 #define MI_LYWSD02 4 #define MI_LYWSD03MMC 5 #define MI_CGG1 6 #define MI_CGD1 7 #define MI_NLIGHT 8 #define MI_MJYD2S 9 #define MI_YEERC 10 #define MI_MHOC401 11 #define MI_MHOC303 12 #define MI_ATC 13 #define MI_DOOR 14 #define MI_SCALE_V1 15 #define MI_SCALE_V2 16 #define MI_MI32_TYPES 16 //count this manually const uint16_t kMI32DeviceID[MI_MI32_TYPES]={ 0x0000, // Unkown 0x0098, // Flora 0x01aa, // MJ_HT_V1 0x045b, // LYWSD02 0x055b, // LYWSD03 0x0347, // CGG1 0x0576, // CGD1 0x03dd, // NLIGHT 0x07f6, // MJYD2S 0x0153, // yee-rc 0x0387, // MHO-C401 0x06d3, // MHO-C303 0x0a1c, // ATC -> this is a fake ID 0x098b, // door/window sensor 0x181d, // Mi Scale V1 0x181b // Mi Scale V2 }; const char kMI32DeviceType0[] PROGMEM = "Unknown"; const char kMI32DeviceType1[] PROGMEM = "Flora"; const char kMI32DeviceType2[] PROGMEM = "MJ_HT_V1"; const char kMI32DeviceType3[] PROGMEM = "LYWSD02"; const char kMI32DeviceType4[] PROGMEM = "LYWSD03"; const char kMI32DeviceType5[] PROGMEM = "CGG1"; const char kMI32DeviceType6[] PROGMEM = "CGD1"; const char kMI32DeviceType7[] PROGMEM = "NLIGHT"; const char kMI32DeviceType8[] PROGMEM = "MJYD2S"; const char kMI32DeviceType9[] PROGMEM = "YEERC"; const char kMI32DeviceType10[] PROGMEM ="MHOC401"; const char kMI32DeviceType11[] PROGMEM ="MHOC303"; const char kMI32DeviceType12[] PROGMEM ="ATC"; const char kMI32DeviceType13[] PROGMEM ="DOOR"; const char kMI32DeviceType14[] PROGMEM ="MISCALEV1"; const char kMI32DeviceType15[] PROGMEM ="MISCALEV2"; const char * kMI32DeviceType[] PROGMEM = {kMI32DeviceType0,kMI32DeviceType1,kMI32DeviceType2,kMI32DeviceType3,kMI32DeviceType4,kMI32DeviceType5,kMI32DeviceType6,kMI32DeviceType7,kMI32DeviceType8,kMI32DeviceType9,kMI32DeviceType10,kMI32DeviceType11,kMI32DeviceType12,kMI32DeviceType13,kMI32DeviceType14,kMI32DeviceType15}; typedef int BATREAD_FUNCTION(int slot); typedef int UNITWRITE_FUNCTION(int slot, int unit); typedef int TIMEWRITE_FUNCTION(int slot); int genericOpCompleteFn(BLE_ESP32::generic_sensor_t *pStruct); int genericBatReadFn(int slot); int genericUnitWriteFn(int slot, int unit); int genericTimeWriteFn(int slot); int MI32scanCompleteCallback(NimBLEScanResults results); const char LYWSD02_Svc[] PROGMEM = "EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6"; const char LYWSD02_BattChar[] PROGMEM = "EBE0CCC4-7A0A-4B0C-8A1A-6FF2997DA3A6"; const char LYWSD02_UnitChar[] PROGMEM = "EBE0CCBE-7A0A-4B0C-8A1A-6FF2997DA3A6"; const char LYWSD02_TimeChar[] PROGMEM = "EBE0CCB7-7A0A-4B0C-8A1A-6FF2997DA3A6"; const char LYWSD02_BattNotifyChar[] PROGMEM = "EBE0CCC1-7A0A-4B0C-8A1A-6FF2997DA3A6"; const char *LYWSD03_Svc = LYWSD02_Svc; const char *LYWSD03_BattNotifyChar = LYWSD02_BattNotifyChar; const char *MHOC303_Svc = LYWSD02_Svc; const char *MHOC303_UnitChar = LYWSD02_UnitChar; const char *MHOC303_TimeChar = LYWSD02_TimeChar; const char *MHOC401_Svc = LYWSD02_Svc; const char *MHOC401_BattNotifyChar = LYWSD02_BattNotifyChar; const char CGD1_Svc[] PROGMEM = "180F"; const char CGD1_BattChar[] PROGMEM = "2A19"; const char FLORA_Svc[] PROGMEM = "00001204-0000-1000-8000-00805F9B34FB"; const char FLORA_BattChar[] PROGMEM = "00001A02-0000-1000-8000-00805F9B34FB"; /*********************************************************************************************\ * enumerations \*********************************************************************************************/ // types of operation performed, included in context enum MI32_MI_OP_TYPES { OP_TIME_WRITE = 0, OP_BATT_READ = 1, OP_UNIT_WRITE = 2, OP_UNIT_READ = 3, OP_UNIT_TOGGLE = 4, OP_READ_HT_LY = 5, }; enum MI32_MI_KEY_REQ { KEY_REQUIREMENT_UNKNOWN = 0, // we don't know if a key is needed KEY_NOT_REQUIRED = 1, // we got an unencrypted payload KEY_REQUIRED_BUT_NOT_FOUND = 2, // we got an encrypted packet, but had not key KEY_REQUIRED_AND_FOUND = 3, // we got an encrypted packet, and could decrypt KEY_REQUIRED_AND_INVALID = 4, // we got an encrypted packet, and could not decrypt }; /*********************************************************************************************\ * Classes \*********************************************************************************************/ // fn type READ_CALLBACK // NOTE!!!: this callback is called DIRECTLY from the operation task, so be careful about cross-thread access of data // if is called after read, so that you can do a read/modify/write operation on a characteristic. int toggleUnit(BLE_ESP32::generic_sensor_t *op){ uint32_t context = (uint32_t) op->context; int opType = context >> 24; // we only need to op type int devType = (context >> 16) & 0xff; int slot = (context) & 0xff; switch (opType){ case OP_UNIT_TOGGLE:{ uint8_t curUnit = 0; if( op->dataRead[0] != 0 && op->dataRead[0] < 101 ){ curUnit = op->dataRead[0]; } curUnit = curUnit == 0x01?0xFF:0x01; // C/F // copy in ALL of the data, because we don't know how long this is from the existing src code. memcpy(op->dataToWrite, op->dataRead, op->readlen); op->writelen = op->readlen; op->dataToWrite[0] = curUnit; } break; case OP_UNIT_WRITE:{ uint8_t curUnit = op->dataToWrite[0]; // copy in ALL of the data, because we don't know how long this is from the existing src code. memcpy(op->dataToWrite, op->dataRead, op->readlen); op->writelen = op->readlen; op->dataToWrite[0] = curUnit; } break; } return 0; } bool MI32Operation(int slot, int optype, const char *svc, const char *charactistic, const char *notifychar = nullptr, const uint8_t *data = nullptr, int datalen = 0, uint8_t *addr = nullptr ) { if (!svc || !svc[0]){ AddLog(LOG_LEVEL_ERROR, PSTR("M32: Op inv svc")); return 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("M32: Can't get a newOperation")); return 0; } else { if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: Got a newOperation")); } if (slot >= 0){ op->addr = NimBLEAddress(MIBLEsensors[slot].MAC); } else { if (!addr){ AddLog(LOG_LEVEL_ERROR, PSTR("M32: No addr")); BLE_ESP32::freeOperation(&op); return 0; } op->addr = NimBLEAddress(addr); } bool havechar = false; op->serviceUUID = NimBLEUUID(svc); if (!op->serviceUUID.bitSize()){ BLE_ESP32::freeOperation(&op); AddLog(LOG_LEVEL_ERROR, PSTR("M32: MI Bad service string %s"), svc); return 0; } if (charactistic && charactistic[0]){ havechar = true; op->characteristicUUID = NimBLEUUID(charactistic); if (!op->characteristicUUID.bitSize()){ BLE_ESP32::freeOperation(&op); AddLog(LOG_LEVEL_ERROR, PSTR("M32: MI Bad characteristic string %s"), charactistic); return 0; } } if (notifychar && notifychar[0]){ op->notificationCharacteristicUUID = NimBLEUUID(notifychar); if (!op->notificationCharacteristicUUID.bitSize()){ BLE_ESP32::freeOperation(&op); AddLog(LOG_LEVEL_ERROR, PSTR("M32: MI Bad notifycharacteristic string %s"), notifychar); return 0; } } if (data && datalen) { op->writelen = datalen; memcpy(op->dataToWrite, data, datalen); } else { if (!datalen && havechar){ op->readlen = 1; // if we don't set readlen, then it won't read } } // the only times we intercept between read abnd write if ((optype == OP_UNIT_WRITE) || (optype == OP_UNIT_TOGGLE)){ op->readlen = 1; // if we don't set readlen, then it won't read op->readmodifywritecallback = (void *)toggleUnit; } // this op will call us back on complete or failure. op->completecallback = (void *)genericOpCompleteFn; uint32_t context = (optype << 24) | (MIBLEsensors[slot].type << 16) | slot; op->context = (void *)context; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: MI s:%d op:%s"), slot, BLE_ESP32::BLETriggerResponse(op).c_str()); 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("M32: Failed to queue new operation - deleted")); } return res; } int genericBatReadFn(int slot){ int res = 0; switch(MIBLEsensors[slot].type) { // these use notify for battery read, and it comes in the temp packet case MI_LYWSD03MMC: res = MI32Operation(slot, OP_BATT_READ, LYWSD03_Svc, nullptr, LYWSD03_BattNotifyChar); break; case MI_MHOC401: res = MI32Operation(slot, OP_BATT_READ, MHOC401_Svc, nullptr, MHOC401_BattNotifyChar); break; // these read a characteristic case MI_FLORA: res = MI32Operation(slot, OP_BATT_READ, FLORA_Svc, FLORA_BattChar); break; case MI_LYWSD02: res = MI32Operation(slot, OP_BATT_READ, LYWSD02_Svc, LYWSD02_BattChar); break; case MI_CGD1: res = MI32Operation(slot, OP_BATT_READ, CGD1_Svc, CGD1_BattChar); break; // this was for testing only - it does work, but no need to read as we get good bat in advert // case MI_MJ_HT_V1: // res = MI32Operation(slot, OP_BATT_READ, CGD1_Svc, CGD1_BattChar); // break; default: res = -10; // no need to read break; } if (res > 0){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_INFO, PSTR("M32: Req batt read slot %d type %d queued"), slot, MIBLEsensors[slot].type); } else { if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_INFO, PSTR("M32: Req batt read slot %d type %d non-queued res %d"), slot, MIBLEsensors[slot].type, res); } return res; } int genericSensorReadFn(int slot, int force){ int res = 0; switch(MIBLEsensors[slot].type) { /* seen notify timeout consistently with MI_LYWSD02, so although the characteristic seems to exist, it does not work? further dev required with sensor to hand. case MI_LYWSD02: // don't read if key present and we've decoded at least one advert if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND) return -2; res = MI32Operation(slot, OP_READ_HT_LY, LYWSD02_Svc, nullptr, LYWSD02_BattNotifyChar); break;*/ case MI_LYWSD03MMC: // don't read if key present and we've decoded at least one advert if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND && !force) return -2; res = MI32Operation(slot, OP_READ_HT_LY, LYWSD03_Svc, nullptr, LYWSD03_BattNotifyChar); break; case MI_MHOC401: // don't read if key present and we've decoded at least one advert if (MIBLEsensors[slot].needkey == KEY_REQUIRED_AND_FOUND && !force) return -2; res = MI32Operation(slot, OP_READ_HT_LY, MHOC401_Svc, nullptr, MHOC401_BattNotifyChar); break; default: res = -1; break; } return res; } // called once per second int readOneSensor(){ if (MI32.sensorreader.active){ AddLog(LOG_LEVEL_DEBUG, PSTR("M32: readOneSensor - already active reading %d"), MI32.sensorreader.slot-1); return 0; } // loop if the sensor at the slot does not need to be read // i.e. drop out of loop when we start a read, or hit the end int res = -1; do { // MI32.sensorreader.slot is reset to zero to trigger a read sequence if (MI32.sensorreader.slot >= MIBLEsensors.size()){ //AddLog(LOG_LEVEL_DEBUG, PSTR("BLE: readOneSensor past end of slots - %d > %d"), MI32.sensorreader.slot, MIBLEsensors.size()); return 0; } res = genericSensorReadFn(MI32.sensorreader.slot, 0); if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: genericSensorReadFn slot %d res %d"), MI32.sensorreader.slot, res); // if this sensor in this slot does not need to be read via notify, just move on top the next one if (res < 0){ MI32.sensorreader.slot++; } else { break; } } while (1); if (res == 0){ // can't read at the moment (no operations available?) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: readOneSensor no ops available slot %d res %d"), MI32.sensorreader.slot, res); return 0; } // setup next slot to read MI32.sensorreader.slot++; // and make it wait until the read/notify is complete // this is cleared in the response callback. MI32.sensorreader.active = 1; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: readOneSensor reading for slot %d res %d"), MI32.sensorreader.slot-1, res); // started one return 1; } // called once per second int readOneBat(){ if (MI32.batteryreader.active){ return 0; } //MI32.batteryreader.slot is rest to zero to trigger a read... if (MI32.batteryreader.slot >= MIBLEsensors.size()){ return 0; } int res = genericBatReadFn(MI32.batteryreader.slot); // if this sensor in this slot does not support battery read, just move on top the next one if (res < 0){ MI32.batteryreader.slot++; if (MI32.batteryreader.slot >= MIBLEsensors.size()){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_INFO, PSTR("M32: Batt loop complete at %d"), MI32.batteryreader.slot); } return 0; } if (res == 0){ // can't read at the moment (no operations available?) return 0; } // setup next slot to read MI32.batteryreader.slot++; // and make it wait until the read/notify is complete // this is cleared in the response callback. MI32.batteryreader.active = 1; if (MI32.batteryreader.slot >= MIBLEsensors.size()){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_INFO, PSTR("M32: Batt loop will complete at %d"), MI32.batteryreader.slot); } // started one return 1; } ///////////////////////////////////////////////////// // change the unit of measurement? // call with unit == -1 to cause the unit to be toggled. int genericUnitWriteFn(int slot, int unit){ int res = 0; int op = OP_UNIT_WRITE; if (unit == -1){ op = OP_UNIT_TOGGLE; } uint8_t writeData[1]; writeData[0] = unit; switch (MIBLEsensors[slot].type){ case MI_LYWSD02: res = MI32Operation(slot, op, LYWSD02_Svc, LYWSD02_UnitChar, nullptr, writeData, 1); break; case MI_MHOC303: // actually, EXACTLY the same as above, including the sevice and characteristic... res = MI32Operation(slot, op, MHOC303_Svc, MHOC303_UnitChar, nullptr, writeData, 1); break; default: res = -1; break; } return res; } ///////////////////////////////////////////////////// // read the unit of measurement. genericOpCompleteFn int genericUnitReadFn(int slot){ int res = 0; switch (MIBLEsensors[slot].type){ case MI_LYWSD02: res = MI32Operation(slot, OP_UNIT_READ, LYWSD02_Svc, LYWSD02_UnitChar); break; case MI_MHOC303: // actually, EXACTLY the same as above, including the sevice and characteristic... res = MI32Operation(slot, OP_UNIT_READ, MHOC303_Svc, MHOC303_UnitChar); break; default: res = -1; break; } return res; } ///////////////////////////////////////////////////// // write time to a device. genericOpCompleteFn int genericTimeWriteFn(int slot){ int res = 0; switch (MIBLEsensors[slot].type){ case MI_LYWSD02: { union { uint8_t buf[5]; uint32_t time; } _utc; _utc.time = Rtc.utc_time; _utc.buf[4] = Rtc.time_timezone / 60; res = MI32Operation(slot, OP_TIME_WRITE, LYWSD02_Svc, LYWSD02_TimeChar, nullptr, _utc.buf, sizeof(_utc.buf)); } break; case MI_MHOC303: // actually, EXACTLY the same as above, including the sevice and characteristic... union { uint8_t buf[5]; uint32_t time; } _utc; _utc.time = Rtc.utc_time; _utc.buf[4] = Rtc.time_timezone / 60; res = MI32Operation(slot, OP_TIME_WRITE, MHOC303_Svc, MHOC303_TimeChar, nullptr, _utc.buf, sizeof(_utc.buf)); break; default: res = -1; break; } return res; } int genericOpCompleteFn(BLE_ESP32::generic_sensor_t *op){ uint32_t context = (uint32_t) op->context; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: MI op complete context %x"), context); int opType = context >> 24; int devType = (context >> 16) & 0xff; int slot = (context) & 0xff; char slotMAC[13]; BLE_ESP32::dump(slotMAC, sizeof(slotMAC), MIBLEsensors[slot].MAC, 6) ; uint8_t addrrev[6]; memcpy(addrrev, MIBLEsensors[slot].MAC, 6); //BLE_ESP32::ReverseMAC(addrrev); NimBLEAddress addr(addrrev); bool fail = false; if (op->addr != addr){ // slot changed during operation? AddLog(LOG_LEVEL_ERROR, PSTR("M32: Slot mac changed during an operation")); fail = true; } if (op->state <= GEN_STATE_FAILED){ AddLog(LOG_LEVEL_ERROR, PSTR("M32: Operation failed %d for %s"), op->state, slotMAC); fail = true; } if (fail){ switch(opType){ case OP_BATT_READ:{ // allow another... MI32.batteryreader.active = 0; } break; case OP_READ_HT_LY: { // allow another... MI32.sensorreader.active = 0; } break; } return 0; } switch(opType){ case OP_TIME_WRITE: AddLog(LOG_LEVEL_DEBUG, PSTR("M32: Time write for %s complete"), slotMAC); return 0; // nothing to do case OP_BATT_READ:{ uint8_t *data = nullptr; int len = 0; if (op->notifylen){ data = op->dataNotify; len = op->notifylen; // note: the only thingas that have battery in notify FOR THE MOMENT read it like this. MI32notifyHT_LY(slot, (char*)op->dataNotify, op->notifylen); } if (op->readlen){ data = op->dataRead; len = op->readlen; MIParseBatt(slot, data, len); } // allow another... MI32.batteryreader.active = 0; AddLog(LOG_LEVEL_INFO, PSTR("M32: Batt read slot %d done state %x"), slot, op->state); } return 0; case OP_UNIT_WRITE: // nothing more to do? AddLog(LOG_LEVEL_DEBUG, PSTR("M32: Unit write for %s complete"), slotMAC); return 0; case OP_UNIT_READ: { uint8_t currUnit = op->dataRead[0]; AddLog(LOG_LEVEL_DEBUG, PSTR("M32: Unit read for %s complete %d"), slotMAC, currUnit); } return 0; case OP_UNIT_TOGGLE: { uint8_t currUnit = op->dataToWrite[0]; AddLog(LOG_LEVEL_DEBUG, PSTR("M32: Unit toggle for %s complete %d->%d; datasize was %d"), slotMAC, op->dataRead[0], op->dataToWrite[0], op->readlen); } return 0; case OP_READ_HT_LY: { // allow another... MI32.sensorreader.active = 0; MI32notifyHT_LY(slot, (char*)op->dataNotify, op->notifylen); if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: HT_LY notify for %s complete"), slotMAC); } return 0; default: AddLog(LOG_LEVEL_ERROR, PSTR("M32: OpType %d not recognised?"), opType); return 0; } return 0; } int MI32advertismentCallback(BLE_ESP32::ble_advertisment_t *pStruct) { // we will try not to use this... BLEAdvertisedDevice *advertisedDevice = pStruct->advertisedDevice; // AddLog(LOG_LEVEL_DEBUG, PSTR("M32: Advertised Device: %s Buffer: %u"),advertisedDevice->getAddress().toString().c_str(),advertisedDevice->getServiceData(0).length()); int RSSI = pStruct->RSSI; const uint8_t *addr = pStruct->addr; if(MI32isInBlockList(addr) == true) return 0; if (MI32.option.onlyAliased){ const char *alias = BLE_ESP32::getAlias(addr); if (!alias || !(*alias)){ return 0; } } int svcdataCount = advertisedDevice->getServiceDataCount(); if (svcdataCount == 0) { return 0; } NimBLEUUID UUIDBig = advertisedDevice->getServiceDataUUID(0);//.getNative()->u16.value; const ble_uuid_any_t* native = UUIDBig.getNative(); if (native->u.type != 16){ //not interested in 128 bit; return 0; } uint16_t UUID = native->u16.value; char temp[60]; BLE_ESP32::dump(temp, 13, addr, 6); if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("M32: MI:%s svc[0] UUID (%x)"), temp, UUID); std::string ServiceDataStr = advertisedDevice->getServiceData(0); uint32_t ServiceDataLength = ServiceDataStr.length(); const uint8_t *ServiceData = (const uint8_t *)ServiceDataStr.data(); BLE_ESP32::dump(temp, 60, ServiceData, ServiceDataLength); if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("M32: MI:%s"), temp); if (UUID){ // this will take and keep the mutex until the function is over TasAutoMutex localmutex(&slotmutex, "Mi32AdCB2"); switch(UUID){ case 0xfe95: // std MI? case 0xfdcd: // CGD1? { MI32ParseResponse(ServiceData, ServiceDataLength, addr, RSSI); } break; case 0x181a: { //ATC MI32ParseATCPacket(ServiceData, ServiceDataLength, addr, RSSI); } break; case 0x181d: // Mi Scale V1 case 0x181b: // Mi Scale V2 { MI32ParseMiScalePacket(ServiceData, ServiceDataLength, addr, RSSI, UUID); } break; default:{ } break; } } return 0; } /*********************************************************************************************\ * Helper functions \*********************************************************************************************/ /** * @brief Remove all colons from null terminated char array * * @param _string Typically representing a MAC-address like AA:BB:CC:DD:EE:FF */ void MI32stripColon(char* _string){ uint32_t _length = strlen(_string); uint32_t _index = 0; while (_index < _length) { char c = _string[_index]; if(c==':'){ memmove(_string+_index,_string+_index+1,_length-_index); } _index++; } _string[_index] = 0; } /** * @brief Convert string that repesents a hexadecimal number to a byte array * * @param _string input string in format: AABBCCDDEEFF or AA:BB:CC:DD:EE:FF, caseinsensitive * @param _mac target byte array must match the correct size (i.e. AA:BB -> uint8_t bytes[2]) */ void MI32HexStringToBytes(char* _string, uint8_t* _byteArray) { MI32stripColon(_string); UpperCase(_string,_string); uint32_t index = 0; uint32_t _end = strlen(_string); memset(_byteArray,0,_end/2); while (index < _end) { char c = _string[index]; uint8_t value = 0; if(c >= '0' && c <= '9') value = (c - '0'); else if (c >= 'A' && c <= 'F') value = (10 + (c - 'A')); _byteArray[(index/2)] += value << (((index + 1) % 2) * 4); index++; } } /** * @brief Reverse an array of 6 bytes * * @param _mac a byte array of size 6 (typicalliy representing a MAC address) */ void MI32_ReverseMAC(uint8_t _mac[]){ uint8_t _reversedMAC[6]; for (uint8_t i=0; i<6; i++){ _reversedMAC[5-i] = _mac[i]; } memcpy(_mac,_reversedMAC, sizeof(_reversedMAC)); } #ifdef USE_MI_DECRYPTION int MI32AddKey(char* payload, char* key = nullptr){ mi_bindKey_t keyMAC; if (!key){ MI32HexStringToBytes(payload,keyMAC.buf); } else { MI32HexStringToBytes(payload,keyMAC.MAC); MI32HexStringToBytes(key,keyMAC.key); } bool unknownKey = true; for(uint32_t i=0; i 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: Search key for MAC: %02x%02x%02x%02x%02x%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); for(uint32_t i=0; i 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Decryption Key found")); foundNoKey = false; break; } } if(foundNoKey){ AddLog(LOG_LEVEL_DEBUG,PSTR("M32: No Key found")); return -2; // indicates needs key } br_aes_small_ctrcbc_keys keyCtx; br_aes_small_ctrcbc_init(&keyCtx, _bindkey, 16); br_ccm_context ctx; br_ccm_init(&ctx, &keyCtx.vtable); br_ccm_reset(&ctx, nonce, 12, 1, len, 4); br_ccm_aad_inject(&ctx, authData, 1); br_ccm_flip(&ctx); memcpy(payload, data, len); //we want to be sure about 4-byte alignement br_ccm_run(&ctx, 0, payload, len); memcpy(data, payload, len); //back to the packet // crashed in here - why?, so give it more space to work with? // returns 1 if matched, else 0 int ret = br_ccm_check_tag(&ctx, &tag); if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Err:%i, Decrypted : %02x %02x %02x %02x %02x"), ret, payload[1],payload[2],payload[3],payload[4],payload[5]); return ret-1; // -> -1=fail, 0=success } #endif // USE_MI_DECRYPTION // packet examples: // MJ_HT_V1 // 5020 AA01 41 3AF4DAA8654C 0A100109 // 5020 AA01 43 3AF4DAA8654C 061002E901 // 5020 AA01 48 3AF4DAA8654C 041002BF00 // 5020 AA01 4A 3AF4DAA8654C 0D1004BF00E901 // 7122 AA01 15 3AF4DAA8654C 0D 0200020D10 // LYWSD03 encrypted data: // 5858 5B05 2F B3E30838C1A4 [69A9FBDF67] ,060000 0791C39A - 23bytes // 23-9 = 14 // -> nonce B3E30838C1A4|5B02|2F|060000 // 23-6 = 17 // -> tag 0791C39A // datalen = 23 - 9 - 4 - 3 - 1 - 1 = 5 // CGD1 reconstructed from src: (svcdata on fdcd) // xxyy FFEEDDCCBBAA MMMM TTTTHHHH|BB // xxyy FFEEDDCCBBAA 0104 TTTTHHHH // xxyy FFEEDDCCBBAA 0201 BB const char *MIaddrStr(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; } int MIParsePacket(const uint8_t* slotmac, struct mi_beacon_data_t *parsed, const uint8_t *datain, int len){ uint8_t data[32]; memcpy(data, datain, len); if (!parsed){ return 0; } if (len < 5){ return 0; } int byteindex = 0; // 58 58 = 0x5858 = data|comp|mac|enc, v5|auth2 // 30 58 = 0x5830 = comp|mac, v5|auth2 // 30 50 = 0x5030 = comp|mac, v5|auth0 // 48 59 = 0x5948 = data|enc, v5|auth2|registered // 10 59 = 0x5910 = mac, v5|auth2|registered // 71 22 = 0x2271 = data|comp|mac v2|bind // 50 20 = 0x2050 = data|mac v2 - MJ_HT_V1 data // 71 22 = 0x2271 = data|comp|mac|reserved1 v2|bind - MJ_HT_V1 pair // data from byte 0 - e.g. 30 parsed->framedata.meshflag = (data[byteindex] & 0x80)>>7; //Byte 0: x....... parsed->framedata.dataflag = (data[byteindex] & 0x40)>>6; //Byte 0: .x...... parsed->framedata.compatibilityflag = (data[byteindex] & 0x20)>>5; //Byte 0: ..x..... - indicates compatibility data present parsed->framedata.MACFlag = (data[byteindex] & 0x10)>>4; //Byte 0: ...x.... parsed->framedata.isencrypted = (data[byteindex] & 0x08)>>3; //Byte 0: ....x... parsed->framedata.reserved = (data[byteindex] & 0x03)>>6; //Byte 0: .....xxx // data from byte 1 - e.g. 58 byteindex++; parsed->framedata.version = (data[byteindex] & 0xf0)>>4; //Byte 0: xxxx.... parsed->framedata.authMode = (data[byteindex] & 0x0C)>>6; //Byte 0: ....xx.. // e.g. 2 parsed->framedata.bindingvalidreq = (data[byteindex] & 0x02)>>1; //Byte 0: ......x. parsed->framedata.registeredflag = (data[byteindex] & 0x01); //Byte 0: .......x // note: // if bindingvalidreq, we should connect and establish a key. // However, how do we determine WHICH TAS should do this? byteindex++; parsed->devicetype = *((uint16_t *)(data + byteindex)); byteindex += 2; parsed->framecnt = data[byteindex]; //if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: MI frame %d"), parsed->framecnt); byteindex++; if (parsed->framedata.version <= 3){ // e.g. MJ_HT_V1 } if (parsed->framedata.MACFlag){ if (len < byteindex + 6){ return 0; } memcpy(parsed->macdata.mac, &data[byteindex], 6); byteindex += 6; } int decres = 1; // everything after MAC is encrypted if specified? if (parsed->framedata.isencrypted){ if (len < byteindex + 3+4+1){ return 0; } const uint8_t* mac = slotmac; if (parsed->framedata.MACFlag){ mac = parsed->macdata.mac; } uint8_t nonce[12]; uint8_t *p = nonce; memcpy(p, mac, 6); p += 6; memcpy(p, &parsed->devicetype, 2); p += 2; *(p++) = parsed->framecnt; uint8_t *extCnt = data +(len-7); memcpy(p, extCnt, 3); p += 3; uint32_t tag = *(uint32_t *)(data + (len-4)); // decrypt the data in place decres = MIDecryptPayload(mac, nonce, tag, data + byteindex, len - byteindex - 7); // no longer need the nonce data. len -= 7; } switch(decres){ case 1: // decrypt not requested break; case 0: // suceeded parsed->needkey = KEY_REQUIRED_AND_FOUND; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32 %s: Payload decrypted"), MIaddrStr(slotmac)); break; case -1: // key failed to work parsed->needkey = KEY_REQUIRED_AND_INVALID; AddLog(LOG_LEVEL_ERROR,PSTR("M32 %s: Payload decrypt failed"), MIaddrStr(slotmac)); parsed->payloadpresent = 0; return 0; break; case -2: // key not present parsed->needkey = KEY_REQUIRED_BUT_NOT_FOUND; AddLog(LOG_LEVEL_ERROR,PSTR("M32 %s: Payload encrypted but no key"), MIaddrStr(slotmac)); parsed->payloadpresent = 0; return 0; break; } // if set, there could be 1 or 3 bytes here. if (parsed->framedata.compatibilityflag) { if (len < byteindex + 1){ return 0; } // e.g. in pair: 7122 AA01 15 3AF4DAA8654C [0D] 0200020D10 -> bond|unused2 parsed->compatibility.reserved = (data[byteindex] & 0xc0) >> 6; //Byte 0: xx...... parsed->compatibility.IOcap = (data[byteindex] & 0x20) >> 5; //Byte 0: ..x..... parsed->compatibility.bondability = (data[byteindex] & 0x18) >> 3; //Byte 0: ...xx... parsed->compatibility.unused = (data[byteindex] & 0x07) >> 0; //Byte 0: .....xxx byteindex ++; if (parsed->compatibility.IOcap) { if (len < byteindex + 2){ return 0; } parsed->compatibility.IOCapability = *((uint16_t *)(data + byteindex)); // bytes 1-2, e.g. 01 00 -> 0001 byteindex += 2; } } // rest is payload int rem = (len - byteindex); if (rem > sizeof(parsed->payload)){ rem = sizeof(parsed->payload); return 0; } if ((len - byteindex) == 0){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32 %s: No payload"), MIaddrStr(slotmac)); parsed->payload.size = 0; parsed->payloadpresent = 0; return 0; } // we have payload which did not need decrypt. if (decres == 1){ parsed->needkey = KEY_NOT_REQUIRED; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32 %s: Payload unencrypted"), MIaddrStr(slotmac)); } // already decrypted if required parsed->payloadpresent = 1; memcpy(&parsed->payload, (data + byteindex), (len - byteindex)); if (parsed->payload.size != (len - byteindex) - 3){ AddLog(LOG_LEVEL_DEBUG,PSTR("M32 %s: Payload length mismatch"), MIaddrStr(slotmac)); } return 1; } /*********************************************************************************************\ * common functions \*********************************************************************************************/ /** * @brief Return the slot number of a known sensor or return create new sensor slot * * @param _MAC BLE address of the sensor * @param _type Type number of the sensor * @return uint32_t Known or new slot in the sensors-vector */ uint32_t MIBLEgetSensorSlot(const uint8_t *mac, uint16_t _type, uint8_t counter){ //AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: will test ID-type: %x"),D_CMND_MI32, _type); bool _success = false; for (uint32_t i=0; i < MI_MI32_TYPES; i++){ // i < sizeof(kMI32DeviceID) gives compiler warning if(_type == kMI32DeviceID[i]){ _type = i+1; _success = true; break; } else { //AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: ID-type is not: %x"),D_CMND_MI32,kMI32DeviceID[i]); } } if(!_success) { _type = 1; // unknown } //AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: vector size %u"),D_CMND_MI32, MIBLEsensors.size()); for(uint32_t i=0; i 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: slot: %u/%u - ign repeat"),D_CMND_MI32, i, MIBLEsensors.size()); //return 0xff; // packet received before, stop here } if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Frame %d, last %d"), counter, MIBLEsensors[i].lastCnt); MIBLEsensors[i].lastCnt = counter; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: slot: %u/%u"),D_CMND_MI32, i, MIBLEsensors.size()); if (MIBLEsensors[i].type != _type){ // this happens on incorrectly configured pvvx ATC firmware AddLog(LOG_LEVEL_ERROR,PSTR("M32: %s: slot: %u - device type 0x%04x(%s) -> 0x%04x(%s) - check device is only sending one type of advert."),D_CMND_MI32, i, kMI32DeviceID[MIBLEsensors[i].type-1], kMI32DeviceType[MIBLEsensors[i].type-1], kMI32DeviceID[_type-1], kMI32DeviceType[_type-1]); MIBLEsensors[i].type = _type; } return i; } //AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: i: %x %x %x %x %x %x"),D_CMND_MI32, MIBLEsensors[i].MAC[5], MIBLEsensors[i].MAC[4],MIBLEsensors[i].MAC[3],MIBLEsensors[i].MAC[2],MIBLEsensors[i].MAC[1],MIBLEsensors[i].MAC[0]); //AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: n: %x %x %x %x %x %x"),D_CMND_MI32, mac[5], mac[4], mac[3],mac[2],mac[1],mac[0]); } //AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: new sensor -> slot: %u"),D_CMND_MI32, MIBLEsensors.size()); //AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: found new sensor"),D_CMND_MI32); mi_sensor_t _newSensor; memset(&_newSensor, 0 , sizeof(_newSensor)); memcpy(_newSensor.MAC, mac, 6); _newSensor.type = _type; _newSensor.eventType.raw = 0; _newSensor.feature.raw = 0; _newSensor.temp = NAN; _newSensor.needkey = KEY_REQUIREMENT_UNKNOWN; _newSensor.bat = 0x00; _newSensor.RSSI = 0xffff; _newSensor.lux = 0x00ffffff; _newSensor.light = -1; _newSensor.Btn = -1; switch (_type) { case MI_FLORA: _newSensor.moisture =0xff; _newSensor.fertility =0xffff; _newSensor.firmware[0]='\0'; _newSensor.feature.temp=1; _newSensor.feature.moist=1; _newSensor.feature.fert=1; _newSensor.feature.lux=1; _newSensor.feature.bat=1; break; case MI_NLIGHT: _newSensor.events=0x00; _newSensor.feature.PIR=1; _newSensor.feature.NMT=1; _newSensor.feature.events=1; break; case MI_MJYD2S: _newSensor.NMT=0; _newSensor.events=0x00; _newSensor.feature.PIR=1; _newSensor.feature.NMT=1; _newSensor.feature.lux=1; _newSensor.feature.bat=1; _newSensor.feature.events=1; break; case MI_YEERC: _newSensor.feature.Btn=1; break; case MI_DOOR: // MCCGQ02HL _newSensor.feature.Btn=1; _newSensor.feature.light=1; _newSensor.feature.bat=1; break; case MI_SCALE_V1: case MI_SCALE_V2: _newSensor.feature.scale=1; break; default: _newSensor.hum=NAN; _newSensor.feature.temp=1; _newSensor.feature.hum=1; _newSensor.feature.tempHum=1; _newSensor.feature.bat=1; break; } MIBLEsensors.push_back(_newSensor); AddLog(LOG_LEVEL_DEBUG,PSTR("M32: %s: new %s at slot: %u"),D_CMND_MI32, kMI32DeviceType[_type-1],MIBLEsensors.size()-1); MI32.mode.shallShowStatusInfo = 1; return MIBLEsensors.size()-1; }; /** * @brief trigger real-time message for PIR or RC * */ void MI32triggerTele(void){ MI32.mode.triggeredTele = 1; MI32ShowTriggeredSensors(); MI32.mode.triggeredTele = 0; } /** * @brief Is called after every finding of new BLE sensor * */ void MI32StatusInfo() { MI32.mode.shallShowStatusInfo = 0; Response_P(PSTR("{\"%s\":{\"found\":%u}}"), D_CMND_MI32, MIBLEsensors.size()); XdrvRulesProcess(0); } /*********************************************************************************************\ * BLE callbacks section * These are called from main thread only. \*********************************************************************************************/ int MI32scanCompleteCallback(NimBLEScanResults results){ // we actually don't need to do anything here.... if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Scan complete")); return 0; } /*********************************************************************************************\ * init BLE_32 \*********************************************************************************************/ void MI32Init(void) { MIBLEsensors.reserve(10); MIBLEbindKeys.reserve(10); MI32.mode.init = false; //test section for options MI32.option.allwaysAggregate = 1; MI32.option.noSummary = 0; MI32.option.minimalSummary = 0; MI32.option.directBridgeMode = 0; MI32.option.showRSSI = 1; MI32.option.ignoreBogusBattery = 1; // from advertisements MI32.option.holdBackFirstAutodiscovery = 1; BLE_ESP32::registerForAdvertismentCallbacks((const char *)"MI32", MI32advertismentCallback); BLE_ESP32::registerForScanCallbacks((const char *)"MI32", MI32scanCompleteCallback); // note: for operations, we will set individual callbacks in the operations we request //void registerForOpCallbacks(const char *tag, BLE_ESP32::OPCOMPLETE_CALLBACK* pFn); AddLog(LOG_LEVEL_INFO,PSTR("M32: init: request callbacks")); MI32.period = Settings->tele_period; MI32.mode.init = 1; return; } /*********************************************************************************************\ * Task section \*********************************************************************************************/ int MIParseBatt(int slot, uint8_t *data, int len){ int value = data[0]; char slotMAC[13]; BLE_ESP32::dump(slotMAC, sizeof(slotMAC), MIBLEsensors[slot].MAC, 6) ; if ((value != 0) && (value < 101)){ MIBLEsensors[slot].bat = value; if(MIBLEsensors[slot].type==MI_FLORA){ if (len < 7){ AddLog(LOG_LEVEL_ERROR,PSTR("M32: FLORA: not enough bytes read for firmware?")); } else { memcpy(MIBLEsensors[slot].firmware, data+2, 5); MIBLEsensors[slot].firmware[5] = '\0'; AddLog(LOG_LEVEL_DEBUG,PSTR("M32: %s: FLORA Firmware: %s"),D_CMND_MI32,MIBLEsensors[slot].firmware); } } MIBLEsensors[slot].eventType.bat = 1; MIBLEsensors[slot].shallSendMQTT = 1; MI32.mode.shallTriggerTele = 1; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Batt read for %s complete %d"), slotMAC, value); } else { AddLog(LOG_LEVEL_ERROR,PSTR("M32: Batt read for %s complete but out of range 1-101 (%d)"), slotMAC, value); } return 0; } /*********************************************************************************************\ * parse the response from advertisements \*********************************************************************************************/ void MI32ParseATCPacket(const uint8_t * _buf, uint32_t length, const uint8_t *addr, int RSSI){ ATCPacket_t *_packet = (ATCPacket_t*)_buf; PVVXPacket_t *ppv_packet = (PVVXPacket_t*)_buf; if (length == 15){ // 19-1-1-2 uint8_t addrrev[6]; memcpy(addrrev, addr, 6); MI32_ReverseMAC(addrrev); if (!memcmp(addrrev, ppv_packet->MAC, 6)){ //int16_t temperature; // x 0.1 degree //uint16_t humidity; // x 0.01 % //uint16_t battery_mv; // mV //uint8_t battery_level; // 0..100 % //uint8_t counter; // measurement count //uint8_t flags; uint32_t _slot = MIBLEgetSensorSlot(addr, 0x0a1c, ppv_packet->counter); // This must be a hard-coded fake ID if(_slot==0xff) return; if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: %s:pvvx at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); MIBLEsensors[_slot].RSSI=RSSI; MIBLEsensors[_slot].needkey=KEY_NOT_REQUIRED; MIBLEsensors[_slot].temp = (float)(ppv_packet->temperature)/100.0f; MIBLEsensors[_slot].hum = (float)(ppv_packet->humidity)/100.0f; MIBLEsensors[_slot].eventType.tempHum = 1; MIBLEsensors[_slot].bat = ppv_packet->battery_level; MIBLEsensors[_slot].eventType.bat = 1; if(MI32.option.directBridgeMode) { MIBLEsensors[_slot].shallSendMQTT = 1; MI32.mode.shallTriggerTele = 1; } } return; } else { AddLog(LOG_LEVEL_ERROR, PSTR("M32: PVVX packet mac mismatch - ignored?")); return; } } uint8_t addrrev[6]; memcpy(addrrev, addr, 6); //MI32_ReverseMAC(addrrev); // if packet tell a different address to origin, use the different address if (memcmp(addrrev, _packet->MAC, 6)){ MI32_ReverseMAC(_packet->MAC); if (!memcmp(addrrev, _packet->MAC, 6)){ AddLog(LOG_LEVEL_ERROR, PSTR("M32: ATC packet with reversed MAC addr?")); } else { AddLog(LOG_LEVEL_ERROR, PSTR("M32: ATC packet with MAC addr mismatch - is this mesh?")); memcpy(addrrev, _packet->MAC, 6); } addr = addrrev; } uint32_t _slot = MIBLEgetSensorSlot(addr, 0x0a1c, _packet->frameCnt); // This must be a hard-coded fake ID if(_slot==0xff) return; if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: %s at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); MIBLEsensors[_slot].RSSI=RSSI; MIBLEsensors[_slot].needkey=KEY_NOT_REQUIRED; MIBLEsensors[_slot].temp = (float)(int16_t(__builtin_bswap16(_packet->temp)))/10.0f; MIBLEsensors[_slot].hum = (float)_packet->hum; MIBLEsensors[_slot].eventType.tempHum = 1; MIBLEsensors[_slot].bat = _packet->batPer; MIBLEsensors[_slot].eventType.bat = 1; if(MI32.option.directBridgeMode) { MIBLEsensors[_slot].shallSendMQTT = 1; MI32.mode.shallTriggerTele = 1; } } else { } } void MI32ParseMiScalePacket(const uint8_t * _buf, uint32_t length, const uint8_t *addr, int RSSI, int UUID){ MiScaleV1Packet_t *_packetV1 = (MiScaleV1Packet_t*)_buf; MiScaleV2Packet_t *_packetV2 = (MiScaleV2Packet_t*)_buf; // Mi Scale V1 if (length == 10 && UUID == 0x181d){ // 14-1-1-2 uint32_t _slot = MIBLEgetSensorSlot(addr, UUID, 0); if(_slot==0xff) return; if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: %s: at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); MIBLEsensors[_slot].RSSI=RSSI; MIBLEsensors[_slot].needkey=KEY_NOT_REQUIRED; MIBLEsensors[_slot].eventType.scale = 1; MIBLEsensors[_slot].stabilized = (_packetV1->status & (1 << 5)) ? 1 : 0; MIBLEsensors[_slot].weight_removed = (_packetV1->status & (1 << 7)) ? 1 : 0; if (_packetV1->status & (1 << 0)) { strcpy(MIBLEsensors[_slot].weight_unit, PSTR("lbs")); MIBLEsensors[_slot].weight = (float)_packetV1->weight / 100.0f; } else if(_packetV1->status & (1 << 4)) { strcpy(MIBLEsensors[_slot].weight_unit, PSTR("jin")); MIBLEsensors[_slot].weight = (float)_packetV1->weight / 100.0f; } else { strcpy(MIBLEsensors[_slot].weight_unit, PSTR("kg")); MIBLEsensors[_slot].weight = (float)_packetV1->weight / 200.0f; } if (MIBLEsensors[_slot].weight_removed) { MIBLEsensors[_slot].weight = 0.0f; } if(MI32.option.directBridgeMode) { MIBLEsensors[_slot].shallSendMQTT = 1; MI32.mode.shallTriggerTele = 1; } } } // Mi Scale V2 else if (length == 13 && UUID == 0x181b){ // 17-1-1-2 uint32_t _slot = MIBLEgetSensorSlot(addr, UUID, 0); if(_slot==0xff) return; if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: %s: at slot %u"), kMI32DeviceType[MIBLEsensors[_slot].type-1],_slot); MIBLEsensors[_slot].RSSI=RSSI; MIBLEsensors[_slot].needkey=KEY_NOT_REQUIRED; MIBLEsensors[_slot].eventType.scale = 1; MIBLEsensors[_slot].has_impedance = (_packetV2->status & (1 << 1)) ? 1 : 0; MIBLEsensors[_slot].stabilized = (_packetV2->status & (1 << 5)) ? 1 : 0; MIBLEsensors[_slot].weight_removed = (_packetV2->status & (1 << 7)) ? 1 : 0; if (_packetV2->weight_unit & (1 << 4)) { strcpy(MIBLEsensors[_slot].weight_unit, PSTR("jin")); MIBLEsensors[_slot].weight = (float)_packetV2->weight / 100.0f; } else if(_packetV2->weight_unit == 3) { strcpy(MIBLEsensors[_slot].weight_unit, PSTR("lbs")); MIBLEsensors[_slot].weight = (float)_packetV2->weight / 100.0f; } else if(_packetV2->weight_unit == 2) { strcpy(MIBLEsensors[_slot].weight_unit, PSTR("kg")); MIBLEsensors[_slot].weight = (float)_packetV2->weight / 200.0f; } else { strcpy(MIBLEsensors[_slot].weight_unit, PSTR("")); MIBLEsensors[_slot].weight = (float)_packetV2->weight / 100.0f; } if (MIBLEsensors[_slot].weight_removed) { MIBLEsensors[_slot].weight = 0.0f; } if(MI32.option.directBridgeMode) { MIBLEsensors[_slot].shallSendMQTT = 1; MI32.mode.shallTriggerTele = 1; } } } } //////////////////////////////////////////////////////////// // this SHOULD parse any MI payload. int MI32parseMiPayload(int _slot, struct mi_beacon_data_t *parsed){ struct mi_beacon_data_payload_data_t *pld = (struct mi_beacon_data_payload_data_t *) &parsed->payload.data; int res = 1; if (!parsed->payloadpresent){ return 0; } char tmp[20]; BLE_ESP32::dump(tmp, 20, (uint8_t*)&(parsed->payload), parsed->payload.size+3); if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: MI%d payload %s"), _slot, tmp); // clear this for every payload MIBLEsensors[_slot].pairing = 0; MIBLEsensors[_slot].eventType.PairBtn = 0; //https://iot.mi.com/new/doc/embedded-development/ble/object-definition switch(parsed->payload.type){ case 0x0002: // related to pair button? 'easypairing' MIBLEsensors[_slot].pairing = 1; MIBLEsensors[_slot].eventType.PairBtn = 1; MIBLEsensors[_slot].feature.pairing = 1; MI32.mode.shallTriggerTele = 1; MIBLEsensors[_slot].shallSendMQTT = 1; break; case 0x0003: {// motion? 1 byte 'near' uint8_t motion = parsed->payload.data[0]; res = 0; }break; case 0x000f: // 'Someone is moving (with light)' MIBLEsensors[_slot].eventType.motion = 1; MIBLEsensors[_slot].lastTime = millis(); MIBLEsensors[_slot].events++; MIBLEsensors[_slot].lux = pld->lux; MIBLEsensors[_slot].eventType.lux = 1; MIBLEsensors[_slot].NMT = 0; MI32.mode.shallTriggerTele = 1; MIBLEsensors[_slot].shallSendMQTT = 1; MIBLEsensors[_slot].feature.lux = 1; MIBLEsensors[_slot].feature.NMT = 1; MIBLEsensors[_slot].feature.events=1; // AddLog(LOG_LEVEL_DEBUG,PSTR("M32: PIR: primary"),MIBLEsensors[_slot].lux ); break; case 0x1001: // button press MIBLEsensors[_slot].Btn = pld->Btn.num + (pld->Btn.longPress/2)*6; MIBLEsensors[_slot].feature.Btn = 1; MIBLEsensors[_slot].eventType.Btn = 1; MI32.mode.shallTriggerTele = 1; MIBLEsensors[_slot].shallSendMQTT = 1; break; //case 0x1002: // 'sleep' //case 0x1003: // 'RSSI' case 0x1004:{ // 'temperature' float _tempFloat=(float)(pld->temp)/10.0f; if(_tempFloat<60){ MIBLEsensors[_slot].temp=_tempFloat; MIBLEsensors[_slot].feature.temp = 1; MIBLEsensors[_slot].eventType.temp = 1; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 4: temp updated")); } else { if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 4: temp ignored > 60 (%f)"), _tempFloat); } // AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Mode 4: U16: %u Temp"), _beacon.temp ); } break; // 0x1005 - not documented case 0x1006: { // 'humidity' float _tempFloat=(float)(pld->hum)/10.0f; if(_tempFloat<101){ MIBLEsensors[_slot].hum=_tempFloat; MIBLEsensors[_slot].feature.hum = 1; MIBLEsensors[_slot].eventType.hum = 1; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 6: hum updated")); } else { if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 6: hum ignored > 101 (%f)"), _tempFloat); } // AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Mode 6: U16: %u Hum"), _beacon.hum); } break; case 0x1007: // 'Light illuminance' MIBLEsensors[_slot].lux=pld->lux & 0x00ffffff; if(MIBLEsensors[_slot].type==MI_MJYD2S){ MIBLEsensors[_slot].eventType.noMotion = 1; } MIBLEsensors[_slot].eventType.lux = 1; MIBLEsensors[_slot].feature.lux = 1; // AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Mode 7: U24: %u Lux"), _beacon.lux & 0x00ffffff); break; case 0x1008: //'Soil moisture' MIBLEsensors[_slot].moisture=pld->moist; MIBLEsensors[_slot].eventType.moist = 1; MIBLEsensors[_slot].feature.moist = 1; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 8: moisture updated")); // AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Mode 8: U8: %u Moisture"), _beacon.moist); break; case 0x1009: // 'conductivity' / 'Soil EC value' MIBLEsensors[_slot].fertility=pld->fert; MIBLEsensors[_slot].eventType.fert = 1; MIBLEsensors[_slot].feature.fert = 1; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode 9: fertility updated")); // AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Mode 9: U16: %u Fertility"), _beacon.fert); break; case 0x100a:// 'Electricity' if(MI32.option.ignoreBogusBattery){ if(MIBLEsensors[_slot].type==MI_LYWSD03MMC || MIBLEsensors[_slot].type==MI_MHOC401){ res = 0; break; } } MIBLEsensors[_slot].feature.bat = 1; if(pld->bat<101){ MIBLEsensors[_slot].bat = pld->bat; MIBLEsensors[_slot].eventType.bat = 1; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode a: bat updated")); } else { MIBLEsensors[_slot].bat = 100; MIBLEsensors[_slot].eventType.bat = 1; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode a: bat > 100 (%d)"), pld->bat); } // AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Mode a: U8: %u %%"), _beacon.bat); break; // 100b-100d -> undefioend in docs. case 0x100d:{ // is this right???? MIBLEsensors[_slot].feature.tempHum = 1; float _tempFloat=(float)(pld->HT.temp)/10.0f; if(_tempFloat < 60){ MIBLEsensors[_slot].temp = _tempFloat; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode d: temp updated")); } else { if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode d: temp ignored > 60 (%f)"), _tempFloat); } _tempFloat=(float)(pld->HT.hum)/10.0f; if(_tempFloat < 100){ MIBLEsensors[_slot].hum = _tempFloat; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode d: hum updated")); } else { if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: Mode d: hum ignored > 100 (%f)"), _tempFloat); } MIBLEsensors[_slot].eventType.tempHum = 1; // AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Mode d: U16: %x Temp U16: %x Hum"), _beacon.HT.temp, _beacon.HT.hum); } break; // 100e = 'lock' // 100f = 'door' case 0x1010:{ // 'formaldehide' const uint16_t f = uint16_t(parsed->payload.data[0]) | (uint16_t(parsed->payload.data[1]) << 8); float formaldehyde = (float)f / 100.0f; res = 0; } break; // 1011 = 'bind' case 0x1012:{ // 'switch' int active = parsed->payload.data[0]; res = 0; } break; case 0x1013:{ // 'Remaining amount of consumables' - mosquito tablet int tablet = parsed->payload.data[0]; res = 0; } break; //Flooding 0x1014 1 1 //smoke 0x1015 1 1 //Gas 0x1016 case 0x1017:{ // 'No one moves' const uint32_t idle_time = uint32_t(parsed->payload.data[0]) | (uint32_t(parsed->payload.data[1]) << 8) | (uint32_t(parsed->payload.data[2]) << 16) | (uint32_t(parsed->payload.data[2]) << 24); float idlemins = (float)idle_time / 60.0f; int has_motion = (idle_time) ? 0 : 0; MIBLEsensors[_slot].NMT = pld->NMT; MIBLEsensors[_slot].eventType.NMT = 1; MI32.mode.shallTriggerTele = 1; MIBLEsensors[_slot].shallSendMQTT = 1; MIBLEsensors[_slot].feature.NMT = 1; // AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Mode 17: NMT: %u seconds"), _beacon.NMT); } break; //Light intensity 0x1018 case 0x1018:{ //'Light intensity' - 0=dark, 1=light? - MCCGQ02HL MIBLEsensors[_slot].light = parsed->payload.data[0]; MIBLEsensors[_slot].eventType.light = 1; MI32.mode.shallTriggerTele = 1; MIBLEsensors[_slot].shallSendMQTT = 1; MIBLEsensors[_slot].feature.light = 1; } break; case 0x1019:{ //'Door sensor' - 0=open, 1=closed, 2=timeout? - MCCGQ02HL MIBLEsensors[_slot].Btn = (uint8_t) parsed->payload.data[0]; // just an 8 bit value in a union. MIBLEsensors[_slot].eventType.Btn = 1; MI32.mode.shallTriggerTele = 1; MIBLEsensors[_slot].shallSendMQTT = 1; MIBLEsensors[_slot].feature.Btn = 1; } break; //Weight attributes 0x101A 600 0 //No one moves over time 0x101B 1 1 //Smart pillow 0x101C 60 1 //Formaldehyde (new) 0x101D default: { AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Unknown MI pld type %x %s"), parsed->payload.type, tmp); res = 0; } break; } if(res && MI32.option.directBridgeMode) { MIBLEsensors[_slot].shallSendMQTT = 1; MI32.mode.shallTriggerTele = 1; } return res; } //////////////////////////////////////////////////////////// // this SHOULD parse any MI packet, including encrypted. void MI32ParseResponse(const uint8_t *buf, uint16_t bufsize, const uint8_t* addr, int RSSI) { struct mi_beacon_data_t parsed; memset(&parsed, 0, sizeof(parsed)); int res = MIParsePacket(addr, &parsed, buf, bufsize); uint8_t addrrev[6]; memcpy(addrrev, addr, 6); MI32_ReverseMAC(addrrev); if (memcmp(addrrev, parsed.macdata.mac, 6)){ AddLog(LOG_LEVEL_ERROR, PSTR("M32: MI packet with MAC addr mismatch - is this mesh?")); memcpy(addrrev, parsed.macdata.mac, 6); MI32_ReverseMAC(addrrev); addr = addrrev; } uint16_t _slot = MIBLEgetSensorSlot( addr, parsed.devicetype, parsed.framecnt ); if(_slot==0xff) return; if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ if (parsed.needkey != KEY_REQUIREMENT_UNKNOWN){ MIBLEsensors[_slot].needkey = parsed.needkey; } MIBLEsensors[_slot].RSSI=RSSI; if (!res){ // - if the payload is not valid if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: MIParsePacket returned %d"), res); return; } else { } MI32parseMiPayload(_slot, &parsed); } } bool MI32isInBlockList(const uint8_t* MAC){ bool isBlocked = false; for(auto &_blockedMAC : MIBLEBlockList){ if(memcmp(_blockedMAC.buf,MAC,6) == 0) isBlocked = true; } return isBlocked; } void MI32removeMIBLEsensor(uint8_t* MAC){ // this will take and keep the mutex until the function is over TasAutoMutex localmutex(&slotmutex, "Mi32Rem"); MIBLEsensors.erase( std::remove_if( MIBLEsensors.begin() , MIBLEsensors.end(), [MAC]( mi_sensor_t _sensor )->bool { return (memcmp(_sensor.MAC,MAC,6) == 0); } ), end( MIBLEsensors ) ); } /***********************************************************************\ * Read data from connections \***********************************************************************/ void MI32notifyHT_LY(int slot, char *_buf, int len){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: %s: raw data: %x%x%x%x%x%x%x"),D_CMND_MI32,_buf[0],_buf[1],_buf[2],_buf[3],_buf[4],_buf[5],_buf[6]); // the value 0b00 is 28.16 C? if(_buf[0] != 0 || _buf[1] != 0){ memcpy(&LYWSD0x_HT,(void *)_buf,sizeof(LYWSD0x_HT)); if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: %s: T * 100: %u, H: %u, V: %u"),D_CMND_MI32,LYWSD0x_HT.temp,LYWSD0x_HT.hum, LYWSD0x_HT.volt); uint32_t _slot = slot; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: MIBLE: Sensor slot: %u"), _slot); static float _tempFloat; _tempFloat=(float)(LYWSD0x_HT.temp)/100.0f; if(_tempFloat<60){ MIBLEsensors[_slot].temp=_tempFloat; // MIBLEsensors[_slot].showedUp=255; // this sensor is real } _tempFloat=(float)LYWSD0x_HT.hum; if(_tempFloat<100){ MIBLEsensors[_slot].hum = _tempFloat; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG_MORE,PSTR("M32: LYWSD0x: hum updated")); } MIBLEsensors[_slot].eventType.tempHum = 1; if (MIBLEsensors[_slot].type == MI_LYWSD03MMC || MIBLEsensors[_slot].type == MI_MHOC401){ // ok, so CR2032 is 3.0v, but drops immediately to ~2.9. // so we'll go with the 2.1 min, 2.95 max. float minVolts = 2100.0; //float maxVolts = 2950.0; //float range = maxVolts - minVolts; //float divisor = range/100; // = 8.5 float percent = (((float)LYWSD0x_HT.volt) - minVolts)/ 8.5; //divisor; if (percent > 100) percent = 100; MIBLEsensors[_slot].bat = (int) percent; MIBLEsensors[_slot].eventType.bat = 1; } if(MI32.option.directBridgeMode) { MIBLEsensors[_slot].shallSendMQTT = 1; MI32.mode.shallTriggerTele = 1; } } } /** * @brief Launch functions from Core 1 to make race conditions less likely * */ void MI32Every50mSecond(){ if(MI32.mode.shallTriggerTele){ MI32.mode.shallTriggerTele = 0; MI32triggerTele(); } } /** * @brief Main loop of the driver, "high level"-loop * */ void MI32EverySecond(bool restart){ // AddLog(LOG_LEVEL_DEBUG_MORE, PSTR("M32: onesec")); MI32TimeoutSensors(); if (MI32.option.MQTTType == 0){ // show tas style sensor MQTT MI32ShowSomeSensors(); } if (MI32.option.MQTTType == 1 #ifdef USE_HOME_ASSISTANT || Settings->flag.hass_discovery #endif ) { // these two share a counter // discovery only sent if hass_discovery MI32DiscoveryOneMISensor(); // show independent style sensor MQTT // note - if !MQTTType, then this is IN ADDITION to 'normal' MI32ShowOneMISensor(); } // read a battery if // MI32.batteryreader.slot < filled and !MI32.batteryreader.active readOneBat(); // read a sensor if // MI32.sensorreader.slot < filled and !MI32.sensorreader.active // for sensors which need to get data through notify... readOneSensor(); if (MI32.secondsCounter >= MI32.period){ // only if we finished the last read if (MI32.sensorreader.slot >= MIBLEsensors.size()){ AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Kick off readOneSensor")); // kick off notification sensor reading every period. MI32.sensorreader.slot = 0; MI32.secondsCounter = 0; } } MI32.secondsCounter ++; if (MI32.secondsCounter2 >= MI32.period){ if (MI32.mqttCurrentSlot >= MIBLEsensors.size()){ AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Kick off tele sending")); MI32.mqttCurrentSlot = 0; MI32.secondsCounter2 = 0; MI32.mqttCurrentSingleSlot = 0; } else { AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Hit tele time, restarted but not finished last - lost from slot %d")+MI32.mqttCurrentSlot); MI32.mqttCurrentSlot = 0; MI32.secondsCounter2 = 0; MI32.mqttCurrentSingleSlot = 0; } } MI32.secondsCounter2++; static uint32_t _counter = MI32.period - 15; static uint32_t _nextSensorSlot = 0; uint32_t _idx = 0; int numsensors = MIBLEsensors.size(); for (uint32_t i = 0; i < numsensors; i++) { if(MIBLEsensors[i].type==MI_NLIGHT || MIBLEsensors[i].type==MI_MJYD2S){ MIBLEsensors[i].NMT++; } } if(MI32.mode.shallShowStatusInfo == 1){ MI32StatusInfo(); } } /*********************************************************************************************\ * Commands \*********************************************************************************************/ void CmndMi32Period(void) { if (XdrvMailbox.data_len > 0) { if (1 == XdrvMailbox.payload) { MI32EverySecond(true); } else { MI32.period = XdrvMailbox.payload; } } ResponseCmndNumber(MI32.period); } int findSlot(char *addrOrAlias){ uint8_t mac[7]; int res = BLE_ESP32::getAddr(mac, addrOrAlias); if (!res) return -1; for (int i = MIBLEsensors.size()-1; i >= 0 ; i--) { if (!memcmp(MIBLEsensors[i].MAC, mac, 6)){ return i; } } return -1; } void CmndMi32Time(void) { if (XdrvMailbox.data_len > 0) { int slot = findSlot(XdrvMailbox.data); if (slot < 0) { slot = XdrvMailbox.payload; } if (MIBLEsensors.size() > slot) { int res = genericTimeWriteFn(slot); if (res > 0){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: will set Time")); ResponseCmndNumber(slot); return; } if (res < 0) { AddLog(LOG_LEVEL_ERROR, PSTR("M32: cannot set Time on sensor type")); } if (res == 0) { AddLog(LOG_LEVEL_ERROR, PSTR("M32: cannot set Time right now")); } } } ResponseCmndChar_P("fail"); } void CmndMi32Page(void) { if (XdrvMailbox.payload > 0) { MI32.perPage = XdrvMailbox.payload; } ResponseCmndNumber(MI32.perPage); } // read ALL battery values where we can? void CmndMi32Battery(void) { // trigger a read cycle MI32.batteryreader.slot = 0; ResponseCmndDone(); } void CmndMi32Unit(void) { if (XdrvMailbox.data_len > 0) { int slot = findSlot(XdrvMailbox.data); if (slot < 0) { slot = XdrvMailbox.payload; } if (MIBLEsensors.size() > slot) { // TOGGLE unit? int res = genericUnitWriteFn(slot, -1); if (res > 0){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG, PSTR("M32: will toggle Unit")); ResponseCmndNumber(slot); return; } if (res < 0) { AddLog(LOG_LEVEL_ERROR, PSTR("M32: cannot toggle Unit on sensor type")); } if (res == 0) { AddLog(LOG_LEVEL_ERROR, PSTR("M32: cannot toggle Unit right now")); } } } ResponseCmndIdxChar(PSTR("Invalid")); } #ifdef USE_MI_DECRYPTION void CmndMi32Key(void) { if (44 == XdrvMailbox.data_len) { // a KEY-MAC-string MI32AddKey(XdrvMailbox.data, nullptr); MI32KeyListResp(); } else { ResponseCmndIdxChar(PSTR("Invalid")); } } #endif // USE_MI_DECRYPTION void MI32BlockListResp(){ Response_P(PSTR("{\"MI32Block\":{")); for (int i = 0; i < MIBLEBlockList.size(); i++){ if (i){ ResponseAppend_P(PSTR(",")); } char tmp[20]; ToHex_P(MIBLEBlockList[i].buf,6,tmp,20,0); ResponseAppend_P(PSTR("\"%s\":1"), tmp); } ResponseAppend_P(PSTR("}}")); } void CmndMi32Block(void){ if (XdrvMailbox.data_len == 0) { switch (XdrvMailbox.index) { case 0: { //TasAutoMutex localmutex(&slotmutex, "Mi32Block1"); MIBLEBlockList.clear(); } break; default: case 1: break; } MI32BlockListResp(); return; } MAC_t _MACasBytes; int res = BLE_ESP32::getAddr(_MACasBytes.buf, XdrvMailbox.data); if (!res){ ResponseCmndIdxChar(PSTR("Addr invalid")); return; } //MI32HexStringToBytes(XdrvMailbox.data,_MACasBytes.buf); switch (XdrvMailbox.index) { case 0: { //TasAutoMutex localmutex(&slotmutex, "Mi32Block2"); MIBLEBlockList.erase( std::remove_if( begin( MIBLEBlockList ), end( MIBLEBlockList ), [_MACasBytes]( MAC_t& _entry )->bool { return (memcmp(_entry.buf,_MACasBytes.buf,6) == 0); } ), end( MIBLEBlockList ) ); } break; case 1: { //TasAutoMutex localmutex(&slotmutex, "Mi32Block3"); bool _notYetInList = true; for (auto &_entry : MIBLEBlockList) { if (memcmp(_entry.buf,_MACasBytes.buf,6) == 0){ _notYetInList = false; } } if (_notYetInList) { MIBLEBlockList.push_back(_MACasBytes); MI32removeMIBLEsensor(_MACasBytes.buf); } // AddLog(LOG_LEVEL_INFO,PSTR("M32: size of ilist: %u"), MIBLEBlockList.size()); } break; } MI32BlockListResp(); } void CmndMi32Option(void){ bool set = false; if (strlen(XdrvMailbox.data)){ set = true; } int onOff = atoi(XdrvMailbox.data); switch(XdrvMailbox.index) { case 0: if (set){ MI32.option.allwaysAggregate = onOff; } else { onOff = MI32.option.allwaysAggregate; } break; case 1: if (set){ MI32.option.noSummary = onOff; } else { onOff = MI32.option.noSummary; } break; case 2: if (set){ MI32.option.directBridgeMode = onOff; } else { onOff = MI32.option.directBridgeMode; } break; case 4:{ if (set){ MI32.option.ignoreBogusBattery = onOff; } else { onOff = MI32.option.ignoreBogusBattery; } } break; case 5:{ if (set){ MI32.option.onlyAliased = onOff; if (MI32.option.onlyAliased){ // discard all sensors for a restart MIBLEsensors.clear(); } } else { onOff = MI32.option.onlyAliased; } } break; case 6:{ if (set){ MI32.option.MQTTType = onOff; } else { onOff = MI32.option.MQTTType; } } break; default:{ ResponseCmndIdxError(); return; } break; } ResponseCmndIdxNumber(onOff); return; } void MI32KeyListResp(){ Response_P(PSTR("{\"MIKeys\":{")); for (int i = 0; i < MIBLEbindKeys.size(); i++){ if (i){ ResponseAppend_P(PSTR(",")); } char tmp[20]; ToHex_P(MIBLEbindKeys[i].MAC,6,tmp,20,0); char key[16*2+1]; ToHex_P(MIBLEbindKeys[i].key,16,key,33,0); ResponseAppend_P(PSTR("\"%s\":\"%s\""), tmp, key); } ResponseAppend_P(PSTR("}}")); } void CmndMi32Keys(void){ #ifdef BLE_ESP32_ALIASES int op = XdrvMailbox.index; if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Key %d %s"), op, XdrvMailbox.data); int res = -1; switch(op){ case 0: case 1:{ char *p = strtok(XdrvMailbox.data, " ,="); bool trigger = false; int added = 0; do { if (!p || !(*p)){ break; } uint8_t addr[7]; char *mac = p; int addrres = BLE_ESP32::getAddr(addr, p); if (!addrres){ ResponseCmndChar("invalidmac"); return; } p = strtok(nullptr, " ,="); char *key = p; if (!p || !(*p)){ int i = 0; for (i = 0; i < MIBLEbindKeys.size(); i++){ mi_bindKey_t *key = &MIBLEbindKeys[i]; if (!memcmp(key->MAC, addr, 6)){ MIBLEbindKeys.erase(MIBLEbindKeys.begin() + i); MI32KeyListResp(); return; } } ResponseCmndChar("invalidmac"); return; } AddLog(LOG_LEVEL_ERROR,PSTR("M32: Add key mac %s = key %s"), mac, key); char tmp[20]; // convert mac back to string ToHex_P(addr,6,tmp,20,0); if (MI32AddKey(tmp, key)){ added++; } p = strtok(nullptr, " ,="); } while (p); if (added){ if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Added %d Keys"), added); MI32KeyListResp(); } else { MI32KeyListResp(); } return; } break; case 2:{ // clear if (BLE_ESP32::BLEDebugMode > 0) AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Keys clearing %d"), MIBLEbindKeys.size()); for (int i = MIBLEbindKeys.size()-1; i >= 0; i--){ MIBLEbindKeys.pop_back(); } MI32KeyListResp(); return; } break; } ResponseCmndChar("invalididx"); #endif } /*********************************************************************************************\ * Presentation \*********************************************************************************************/ const char HTTP_MI32[] PROGMEM = "{s}MI ESP32 v0921{m}%u%s / %u{e}"; const char HTTP_MI32_ALIAS[] PROGMEM = "{s}%s Alias {m}%s{e}"; const char HTTP_MI32_MAC[] PROGMEM = "{s}%s %s{m}%s{e}"; const char HTTP_RSSI[] PROGMEM = "{s}%s " D_RSSI "{m}%d dBm{e}"; const char HTTP_BATTERY[] PROGMEM = "{s}%s" " Battery" "{m}%u %%{e}"; const char HTTP_LASTBUTTON[] PROGMEM = "{s}%s Last Button{m}%u {e}"; const char HTTP_EVENTS[] PROGMEM = "{s}%s Events{m}%u {e}"; const char HTTP_NMT[] PROGMEM = "{s}%s No motion{m}> %u seconds{e}"; const char HTTP_MI32_FLORA_DATA[] PROGMEM = "{s}%s" " Fertility" "{m}%u us/cm{e}"; const char HTTP_MI32_HL[] PROGMEM = "{s}
{m}
{e}"; const char HTTP_MI32_LIGHT[] PROGMEM = "{s}%s" " Light" "{m}%d{e}"; const char HTTP_MISCALE_WEIGHT[] PROGMEM = "{s}%s" " Weight" "{m}%*_f %s{e}"; const char HTTP_MISCALE_IMPEDANCE[] PROGMEM = "{s}%s" " Impedance" "{m}%u{e}"; const char HTTP_MISCALE_WEIGHT_REMOVED[] PROGMEM = "{s}%s" " Weight removed" "{m}%s{e}"; const char HTTP_MISCALE_STABILIZED[] PROGMEM = "{s}%s" " Stabilized" "{m}%s{e}"; //const char HTTP_NEEDKEY[] PROGMEM = "{s}%s %s{m} {e}"; //const char HTTP_NEEDKEY[] PROGMEM = "{s}%s %s{m} {e}"; const char HTTP_NEEDKEY[] PROGMEM = "{s}%s %s{m} {e}"; const char HTTP_PAIRING[] PROGMEM = "{s}%s Pair Button Pressed{m} {e}"; const char HTTP_KEY_ERROR[] PROGMEM = "Key error %s"; const char HTTP_MAC_ERROR[] PROGMEM = "MAC error %s"; const char HTTP_KEY_ADDED[] PROGMEM = "Cmnd: MI32Keys %s=%s"; const char HTTP_MI_KEY_STYLE[] PROGMEM = ""; #define D_MI32_KEY "MI32 Set Key" void HandleMI32Key(){ AddLog(LOG_LEVEL_DEBUG, PSTR("M32: HandleMI32Key hit")); if (!HttpCheckPriviledgedAccess()) { AddLog(LOG_LEVEL_DEBUG, PSTR("M32: !HttpCheckPriviledgedAccess()")); return; } WSContentStart_P(PSTR(D_MI32_KEY)); WSContentSendStyle_P(HTTP_MI_KEY_STYLE); char key[64] = {0}; WebGetArg("key", key, sizeof(key)); if (strlen(key) != 16*2){ WSContentSend_P(HTTP_KEY_ERROR, key); WSContentStop(); return; } char mac[13] = {0}; WebGetArg("mac", mac, sizeof(mac)); if (strlen(mac) != 12){ WSContentSend_P(HTTP_MAC_ERROR, mac); WSContentStop(); return; } WSContentSend_P(HTTP_KEY_ADDED, mac, key); strncat(key, mac, sizeof(key)); MI32AddKey(key, nullptr); // WSContentSpaceButton(BUTTON_CONFIGURATION); WSContentStop(); } void MI32TimeoutSensors(){ // whatever, this function access all the arrays.... // so block for as long as it takes. // PROBLEM: when we take this, it hangs the BLE loop. // BUT, devicePresent uses the // remove devices for which the adverts have timed out for (int i = MIBLEsensors.size()-1; i >= 0 ; i--) { //if (MIBLEsensors[i].MAC[2] || MIBLEsensors[i].MAC[3] || MIBLEsensors[i].MAC[4] || MIBLEsensors[i].MAC[5]){ if (!BLE_ESP32::devicePresent(MIBLEsensors[i].MAC)){ uint8_t *mac = MIBLEsensors[i].MAC; AddLog(LOG_LEVEL_DEBUG,PSTR("M32: Dev no longer present MAC: %02x%02x%02x%02x%02x%02x"), mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); TasAutoMutex localmutex(&slotmutex, "Mi32Timeout"); MIBLEsensors.erase(MIBLEsensors.begin() + i); } //} } } // this assumes that we're adding to a ResponseTime_P void MI32GetOneSensorJson(int slot, int hidename){ mi_sensor_t *p; p = &MIBLEsensors[slot]; // remove hyphen - make it difficult to configure HASS if (!hidename) { ResponseAppend_P(PSTR("\"%s%02x%02x%02x\":{"), kMI32DeviceType[p->type-1], p->MAC[3], p->MAC[4], p->MAC[5]); } const char *alias = BLE_ESP32::getAlias(p->MAC); if (alias && alias[0]){ ResponseAppend_P(PSTR("\"alias\":\"%s\","), alias); } ResponseAppend_P(PSTR("\"mac\":\"%02x%02x%02x%02x%02x%02x\""), p->MAC[0], p->MAC[1], p->MAC[2], p->MAC[3], p->MAC[4], p->MAC[5]); if((!MI32.mode.triggeredTele && !MI32.option.minimalSummary)||MI32.mode.triggeredTele){ bool tempHumSended = false; if(p->feature.tempHum){ if(p->eventType.tempHum || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ if (!isnan(p->hum) && !isnan(p->temp) #ifdef USE_HOME_ASSISTANT ||(hass_mode!=-1) #endif //USE_HOME_ASSISTANT ) { ResponseAppend_P(PSTR(",")); ResponseAppendTHD(p->temp, p->hum); tempHumSended = true; } } } if(p->feature.temp && !tempHumSended){ if(p->eventType.temp || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate) { if (!isnan(p->temp) #ifdef USE_HOME_ASSISTANT ||(hass_mode!=-1) #endif //USE_HOME_ASSISTANT ) { ResponseAppend_P(PSTR(",\"" D_JSON_TEMPERATURE "\":%*_f"), Settings->flag2.temperature_resolution, &p->temp); } } } if(p->feature.hum && !tempHumSended){ if(p->eventType.hum || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate) { if (!isnan(p->hum) #ifdef USE_HOME_ASSISTANT ||(hass_mode!=-1) #endif //USE_HOME_ASSISTANT ) { char hum[FLOATSZ]; dtostrfd(p->hum, Settings->flag2.humidity_resolution, hum); ResponseAppend_P(PSTR(",\"" D_JSON_HUMIDITY "\":%s"), hum); } } } if (p->feature.lux){ if(p->eventType.lux || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ #ifdef USE_HOME_ASSISTANT if ((hass_mode != -1) && (p->lux == 0x0ffffff)) { ResponseAppend_P(PSTR(",\"" D_JSON_ILLUMINANCE "\":null")); } else #endif //USE_HOME_ASSISTANT if ((p->lux != 0x0ffffff) #ifdef USE_HOME_ASSISTANT || (hass_mode != -1) #endif //USE_HOME_ASSISTANT ) { // this is the error code -> no lux ResponseAppend_P(PSTR(",\"" D_JSON_ILLUMINANCE "\":%u"), p->lux); } } } if (p->feature.light){ if(p->eventType.light || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate #ifdef USE_HOME_ASSISTANT ||(hass_mode==2) #endif //USE_HOME_ASSISTANT ){ ResponseAppend_P(PSTR(",\"Light\":%d"), p->light); } } if (p->feature.moist){ if(p->eventType.moist || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ #ifdef USE_HOME_ASSISTANT if ((hass_mode != -1) && (p->moisture == 0xff)) { ResponseAppend_P(PSTR(",\"" D_JSON_MOISTURE "\":null")); } else #endif //USE_HOME_ASSISTANT if ((p->moisture != 0xff) #ifdef USE_HOME_ASSISTANT || (hass_mode != -1) #endif //USE_HOME_ASSISTANT ) { ResponseAppend_P(PSTR(",\"" D_JSON_MOISTURE "\":%u"), p->moisture); } } } if (p->feature.fert){ if(p->eventType.fert || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ #ifdef USE_HOME_ASSISTANT if ((hass_mode != -1) && (p->fertility == 0xffff)) { ResponseAppend_P(PSTR(",\"Fertility\":null")); } else #endif //USE_HOME_ASSISTANT if ((p->fertility != 0xffff) #ifdef USE_HOME_ASSISTANT || (hass_mode != -1) #endif //USE_HOME_ASSISTANT ) { ResponseAppend_P(PSTR(",\"Fertility\":%u"), p->fertility); } } } if (p->feature.scale){ if(p->eventType.scale || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate #ifdef USE_HOME_ASSISTANT ||(hass_mode==2) #endif //USE_HOME_ASSISTANT ){ ResponseAppend_P(PSTR(",\"weight_removed\":%u"), p->weight_removed); ResponseAppend_P(PSTR(",\"stabilized\":%u"), p->stabilized); ResponseAppend_P(PSTR(",\"weight_unit\":\"%s\""), p->weight_unit); ResponseAppend_P(PSTR(",\"weight\":%*_f"), Settings->flag2.weight_resolution, &p->weight); if (p->has_impedance){ ResponseAppend_P(PSTR(",\"impedance\":%u"), p->impedance); } } } if (p->feature.Btn){ if(p->eventType.Btn || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate #ifdef USE_HOME_ASSISTANT ||(hass_mode==2) #endif //USE_HOME_ASSISTANT ){ ResponseAppend_P(PSTR(",\"Btn\":%d"),p->Btn); } } if(p->eventType.PairBtn && p->pairing){ ResponseAppend_P(PSTR(",\"Pair\":%u"),p->pairing); } } // minimal summary if (p->feature.PIR){ if(p->eventType.motion || !MI32.mode.triggeredTele){ if(MI32.mode.triggeredTele) ResponseAppend_P(PSTR(",\"PIR\":1")); // only real-time ResponseAppend_P(PSTR(",\"Events\":%u"),p->events); } else if(p->eventType.noMotion && MI32.mode.triggeredTele){ ResponseAppend_P(PSTR(",\"PIR\":0")); } } if (p->type == MI_FLORA && !MI32.mode.triggeredTele) { if (p->firmware[0] != '\0') { // this is the error code -> no firmware ResponseAppend_P(PSTR(",\"Firmware\":\"%s\""), p->firmware); } } if (p->feature.NMT || !MI32.mode.triggeredTele){ if(p->eventType.NMT){ ResponseAppend_P(PSTR(",\"NMT\":%u"), p->NMT); } } if (p->feature.bat){ if(p->eventType.bat || !MI32.mode.triggeredTele || MI32.option.allwaysAggregate){ #ifdef USE_HOME_ASSISTANT if ((hass_mode != -1) && (p->bat == 0x00)) { ResponseAppend_P(PSTR(",\"Battery\":null")); } else #endif //USE_HOME_ASSISTANT if ((p->bat != 0x00) #ifdef USE_HOME_ASSISTANT || (hass_mode != -1) #endif //USE_HOME_ASSISTANT ) { ResponseAppend_P(PSTR(",\"Battery\":%u"), p->bat); } } } if (MI32.option.showRSSI) ResponseAppend_P(PSTR(",\"RSSI\":%d"), p->RSSI); if (!hidename) { ResponseAppend_P(PSTR("}")); } p->eventType.raw = 0; p->shallSendMQTT = 0; } /////////////////////////////////////////////// // starts a completely fresh MQTT message. // sends up to 4 sensors // triggered by setting MI32.mqttCurrentSlot = 0 void MI32ShowSomeSensors(){ // don't detect half-added ones here int numsensors = MIBLEsensors.size(); if (MI32.mqttCurrentSlot >= numsensors){ // if we got to the end of the sensors, then don't send more return; } #ifdef USE_HOME_ASSISTANT bool _noSummarySave = MI32.option.noSummary; bool _minimalSummarySave = MI32.option.minimalSummary; if(hass_mode==2){ if(MI32.option.holdBackFirstAutodiscovery){ if(!MI32.mode.firstAutodiscoveryDone){ MI32.mode.firstAutodiscoveryDone = 1; return; } } MI32.option.noSummary = false; MI32.option.minimalSummary = false; } #endif //USE_HOME_ASSISTANT ResponseTime_P(PSTR("")); int cnt = 0; int maxcnt = 4; mi_sensor_t *p; for (; (MI32.mqttCurrentSlot < numsensors) && (cnt < maxcnt); MI32.mqttCurrentSlot++, cnt++) { ResponseAppend_P(PSTR(",")); p = &MIBLEsensors[MI32.mqttCurrentSlot]; MI32GetOneSensorJson(MI32.mqttCurrentSlot, (maxcnt == 1)); int mlen = ResponseLength(); // if we ran out of room, leave here. if (ResponseSize() - mlen < 100){ MI32.mqttCurrentSlot++; break; } cnt++; } ResponseAppend_P(PSTR("}")); MqttPublishPrefixTopicRulesProcess_P(TELE, PSTR(D_RSLT_SENSOR), Settings->flag.mqtt_sensor_retain); //AddLog(LOG_LEVEL_DEBUG,PSTR("M32: %s: show some %d %s"),D_CMND_MI32, MI32.mqttCurrentSlot, ResponseData()); #ifdef USE_HOME_ASSISTANT if(hass_mode==2){ MI32.option.noSummary = _noSummarySave; MI32.option.minimalSummary = _minimalSummarySave; } #endif //USE_HOME_ASSISTANT } /////////////////////////////////////////////// // starts a completely fresh MQTT message. // sends ONE sensor on a dedicated topic NOT related to this TAS // triggered by setting MI32.mqttCurrentSingleSlot = 0 void MI32ShowOneMISensor(){ // don't detect half-added ones here int numsensors = MIBLEsensors.size(); if (MI32.mqttCurrentSingleSlot >= numsensors){ // if we got to the end of the sensors, then don't send more return; } if( #ifdef USE_HOME_ASSISTANT Settings->flag.hass_discovery || #endif //USE_HOME_ASSISTANT MI32.option.MQTTType == 1 ){ ResponseTime_P(PSTR(",")); MI32GetOneSensorJson(MI32.mqttCurrentSingleSlot, 1); mi_sensor_t *p; p = &MIBLEsensors[MI32.mqttCurrentSingleSlot]; ResponseAppend_P(PSTR("}")); char idstr[32]; const char *alias = BLE_ESP32::getAlias(p->MAC); const char *id = idstr; if (alias && *alias){ id = alias; } else { sprintf(idstr, PSTR("%s%02x%02x%02x"), kMI32DeviceType[p->type-1], p->MAC[3], p->MAC[4], p->MAC[5]); } char SensorTopic[60]; sprintf(SensorTopic, "tele/tasmota_ble/%s", id); MqttPublish(SensorTopic); //AddLog(LOG_LEVEL_DEBUG,PSTR("M32: %s: show some %d %s"),D_CMND_MI32, MI32.mqttCurrentSlot, ResponseData()); } MI32.mqttCurrentSingleSlot++; } /////////////////////////////////////////////// // starts a completely fresh MQTT message. // sends ONE sensor's worth of HA discovery msg const char MI_HA_DISCOVERY_TEMPLATE[] PROGMEM = "{\"availability\":[],\"device\":" "{\"identifiers\":[\"BLE%s\"]," "\"name\":\"%s\"," "\"manufacturer\":\"tas\"," "\"model\":\"%s\"," "\"via_device\":\"%s\"" "}," "%s%s%s" "\"expire_after\":600," "\"json_attr_t\":\"%s\"," "\"name\":\"%s_%s\"," "\"state_topic\":\"%s\"," "\"uniq_id\":\"%s_%s\"," "%s%s%s" "\"val_tpl\":\"{{ %s%s%s }}\"}"; // careful - a missing comma causes a crash!!!! // because of the way we loop? const char *classes[] = { // 0 "temperature", "Temperature", "°C", // 1 "humidity", "Humidity", "%", // 2 "temperature", "DewPoint", "°C", // 3 "battery", "Battery", "%", // 4 "signal_strength", "RSSI", "dB", // 5 "",//- empty device class "Btn", "", // 6 "", //- empty device class "Light", "", // 7 "", //- empty device class "Moisture", "%", // 8 "", //- empty device class "Illuminance", "lx", // 9 "", //- empty device class "Fertility", "µS/cm", // 10 "", //- empty device class "Firmware", "", }; void MI32DiscoveryOneMISensor(){ // don't detect half-added ones here int numsensors = MIBLEsensors.size(); if (MI32.mqttCurrentSingleSlot >= numsensors){ // if we got to the end of the sensors, then don't send more return; } #ifdef USE_HOME_ASSISTANT if(Settings->flag.hass_discovery){ mi_sensor_t *p; p = &MIBLEsensors[MI32.mqttCurrentSingleSlot]; int datacount = (sizeof(classes)/sizeof(*classes))/3; if (p->nextDiscoveryData >= datacount){ p->nextDiscoveryData = 0; } char DiscoveryTopic[80]; const char *host = NetworkHostname(); const char *devtype = kMI32DeviceType[p->type-1]; char idstr[32]; const char *alias = BLE_ESP32::getAlias(p->MAC); const char *id = idstr; if (alias && *alias){ id = alias; } else { sprintf(idstr, PSTR("%s%02x%02x%02x"), devtype, p->MAC[3], p->MAC[4], p->MAC[5]); } char SensorTopic[60]; sprintf(SensorTopic, "tele/tasmota_ble/%s", 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; } uint8_t isBinary = 0; ResponseClear(); switch(i/3){ case 0: // temp if (!p->feature.temp && !p->feature.tempHum){ continue; } break; case 1:// hum if (!p->feature.hum && !p->feature.tempHum){ continue; } break; case 2: //dew if (!p->feature.tempHum && !(p->feature.temp && p->feature.hum)){ continue; } break; case 3: //bat if (!p->feature.bat){ continue; } break; case 4: //rssi - all break; case 5: // button if (!p->feature.Btn){ continue; } //isBinary = 2; // invert payload break; case 6: // binary light sense if (!p->feature.light){ continue; } //isBinary = 1; break; case 7: // moisture if (!p->feature.moist){ continue; } //isBinary = 1; break; case 8: // lux if (!p->feature.lux){ continue; } break; case 9: // fertility if (!p->feature.fert){ continue; } break; case 10: // firmware if (!p->feature.fert){ // Flora only continue; } break; } /* {"availability":[],"device":{"identifiers":["TasmotaBLEa4c1387fc1e1"],"manufacturer":"simon","model":"someBLEsensor","name":"TASBLEa4c1387fc1e1","sw_version":"0.0.0"},"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 }}"} {"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(MI_HA_DISCOVERY_TEMPLATE, //"{\"identifiers\":[\"BLE%s\"]," id, //"\"name\":\"%s\"}," id, //\"model\":\"%s\", devtype, //\"via_device\":\"%s\" host, //"\"dev_cla\":\"%s\"," (classes[i][0]?"\"dev_cla\":\"":""), classes[i], (classes[i][0]?"\",":""), //"\"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][0]?"\"unit_of_meas\":\"":""), classes[i+2], (classes[i+2][0]?"\",":""), //"\"val_tpl\":\"{{ %s%s }}") // e.g. Temperature // inverted binary - {{ 'off' if value_json.posn else 'on' }} // binary - {{ 'on' if value_json.posn else 'off' }} ((isBinary < 1)?"value_json.": ((isBinary < 2)?"value_json.":"'off' if value_json.") ), classes[i+1], ((isBinary < 1)?"": ((isBinary < 2)?"":" else 'on'") ) // ); sprintf(DiscoveryTopic, "homeassistant/%ssensor/%s/%s/config", (isBinary? "binary_":""), id, classes[i+1]); MqttPublish(DiscoveryTopic); p->nextDiscoveryData++; //vTaskDelay(100/ portTICK_PERIOD_MS); } } // end if hass discovery //AddLog(LOG_LEVEL_DEBUG,PSTR("M32: %s: show some %d %s"),D_CMND_MI32, MI32.mqttCurrentSlot, ResponseData()); #endif //USE_HOME_ASSISTANT } /////////////////////////////////////////////// // starts a completely fresh MQTT message. // sends up to 4 sensors pe5r msg // sends only those which are raw and triggered. // triggered by setting MI32.mode.triggeredTele = 1 void MI32ShowTriggeredSensors(){ if (!MI32.mode.triggeredTele) return; // none to show MI32.mode.triggeredTele = 0; // don't detect half-added ones here int numsensors = MIBLEsensors.size(); int sensor = 0; int maxcnt = 4; if( #ifdef USE_HOME_ASSISTANT Settings->flag.hass_discovery || #endif //USE_HOME_ASSISTANT MI32.option.MQTTType == 1 ){ maxcnt = 1; } do { ResponseTime_P(PSTR("")); int cnt = 0; mi_sensor_t *p; for (; (sensor < numsensors) && (cnt < maxcnt); sensor++) { p = &MIBLEsensors[sensor]; if(p->eventType.raw == 0) continue; if(p->shallSendMQTT==0) continue; cnt++; ResponseAppend_P(PSTR(",")); // hide sensor name if HASS or option6 MI32GetOneSensorJson(sensor, (maxcnt == 1)); int mlen = ResponseLength(); // if we ran out of room, leave here. if (ResponseSize() - mlen < 100){ sensor++; break; } } if (cnt){ // if we got one, then publish ResponseAppend_P(PSTR("}")); if( #ifdef USE_HOME_ASSISTANT Settings->flag.hass_discovery || #endif //USE_HOME_ASSISTANT MI32.option.MQTTType == 1 ){ char SensorTopic[60]; char idstr[32]; const char *alias = BLE_ESP32::getAlias(p->MAC); const char *id = idstr; if (alias && *alias){ id = alias; } else { snprintf_P(idstr, sizeof(idstr), PSTR("%s%02x%02x%02x"), kMI32DeviceType[p->type-1], p->MAC[3], p->MAC[4], p->MAC[5]); } snprintf_P(SensorTopic, sizeof(SensorTopic), PSTR("tele/tasmota_ble/%s"), id); MqttPublish(SensorTopic, Settings->flag.mqtt_sensor_retain); AddLog(LOG_LEVEL_DEBUG, PSTR("M32: triggered %d %s"), sensor, ResponseData()); XdrvRulesProcess(0); } else { MqttPublishPrefixTopicRulesProcess_P(STAT, PSTR(D_RSLT_SENSOR), Settings->flag.mqtt_sensor_retain); } } else { // else don't and clear ResponseClear(); } } while (sensor < numsensors); } void MI32Show(bool json) { // don't detect half-added ones here int numsensors = MIBLEsensors.size(); if (json) { // TELE JSON messages now do nothing here, apart from set MI32.mqttCurrentSlot // which will trigger send next second of up to 4 sensors, then the next four in the next second, etc. //MI32.mqttCurrentSlot = 0; #ifdef USE_WEBSERVER } else { static uint16_t _page = 0; static uint16_t _counter = 0; int32_t i = _page * MI32.perPage; uint32_t j = i + MI32.perPage; if (j+1 > numsensors){ j = numsensors; } char stemp[5] ={0}; if (numsensors-(_page*MI32.perPage)>1 && MI32.perPage!=1) { sprintf_P(stemp,"-%u",j); } if (numsensors==0) i=-1; // only for the GUI WSContentSend_PD(HTTP_MI32, i+1,stemp,numsensors); for (i; itype-1]; const char *alias = BLE_ESP32::getAlias(p->MAC); if (alias && *alias){ WSContentSend_PD(HTTP_MI32_ALIAS, typeName, alias); } char _MAC[18]; ToHex_P(p->MAC,6,_MAC,18);//,':'); WSContentSend_PD(HTTP_MI32_MAC, typeName, D_MAC_ADDRESS, _MAC); WSContentSend_PD(HTTP_RSSI, typeName, p->RSSI); // for some reason, display flora differently switch(p->type){ case MI_FLORA:{ if (!isnan(p->temp)) { WSContentSend_Temp(typeName, p->temp); } if (p->moisture!=0xff) { WSContentSend_PD(HTTP_SNS_MOISTURE, typeName, p->moisture); } if (p->fertility!=0xffff) { WSContentSend_PD(HTTP_MI32_FLORA_DATA, typeName, p->fertility); } } break; default:{ if (!isnan(p->hum) && !isnan(p->temp)) { WSContentSend_THD(typeName, p->temp, p->hum); } } } #ifdef USE_MI_DECRYPTION bool showkey = false; char tmp[40]; strcpy(tmp, PSTR("KeyRqd")); switch(p->needkey) { default:{ snprintf(tmp, 39, PSTR("?%d?"), p->needkey ); showkey = true; } break; case KEY_REQUIREMENT_UNKNOWN: { strcpy(tmp, PSTR("WAIT")); showkey = true; } break; case KEY_NOT_REQUIRED: { strcpy(tmp, PSTR("NOTKEY")); //showkey = true; } break; case KEY_REQUIRED_BUT_NOT_FOUND: { strcpy(tmp, PSTR("NoKey")); showkey = true; } break; case KEY_REQUIRED_AND_FOUND: { strcpy(tmp, PSTR("KeyOk")); showkey = true; } break; case KEY_REQUIRED_AND_INVALID: { strcpy(tmp, PSTR("KeyInv")); showkey = true; } break; } // adds the link to get the key. // provides mac and callback address to receive the key, if we had a website which did this // (future work) if (showkey){ BLE_ESP32::dump(_MAC, 13, p->MAC,6); WSContentSend_PD(HTTP_NEEDKEY, typeName, _MAC, Webserver->client().localIP().toString().c_str(), tmp ); } #endif //USE_MI_DECRYPTION if (p->feature.events){ WSContentSend_PD(HTTP_EVENTS, typeName, p->events); } if (p->feature.NMT){ // no motion time if(p->NMT>0) WSContentSend_PD(HTTP_NMT, typeName, p->NMT); } if (p->feature.lux){ if (p->lux!=0x00ffffff) { // this is the error code -> no valid value WSContentSend_PD(HTTP_SNS_ILLUMINANCE, typeName, p->lux); } } if (p->feature.light){ WSContentSend_PD(HTTP_MI32_LIGHT, typeName, p->light); } if (p->feature.scale){ WSContentSend_PD(HTTP_MISCALE_WEIGHT, typeName, Settings->flag2.weight_resolution, &p->weight, p->weight_unit); if (p->has_impedance) { WSContentSend_PD(HTTP_MISCALE_IMPEDANCE, typeName, p->impedance); } WSContentSend_PD(HTTP_MISCALE_WEIGHT_REMOVED, typeName, p->weight_removed? PSTR("yes") : PSTR("no")); WSContentSend_PD(HTTP_MISCALE_STABILIZED, typeName, p->stabilized ? PSTR("yes") : PSTR("no")); } if(p->bat!=0x00){ WSContentSend_PD(HTTP_BATTERY, typeName, p->bat); } if (p->feature.Btn){ WSContentSend_PD(HTTP_LASTBUTTON, typeName, p->Btn); } if (p->pairing){ WSContentSend_PD(HTTP_PAIRING, typeName); } } _counter++; if(_counter>3) { _page++; _counter=0; } if (MIBLEsensors.size()%MI32.perPage==0 && _page==MIBLEsensors.size()/MI32.perPage) { _page = 0; } if (_page>MIBLEsensors.size()/MI32.perPage) { _page = 0; } #endif // USE_WEBSERVER } } /*********************************************************************************************\ * Interface \*********************************************************************************************/ #define WEB_HANDLE_MI32 "mikey" bool Xsns62(uint8_t function) { // if (!Settings->flag5.mi32_enable) { return false; } // SetOption115 - Enable ESP32 MI32 BLE // return false; bool result = false; switch (function) { case FUNC_INIT: MI32Init(); break; case FUNC_EVERY_50_MSECOND: MI32Every50mSecond(); break; case FUNC_EVERY_SECOND: MI32EverySecond(false); break; case FUNC_COMMAND: result = DecodeCommand(kMI32_Commands, MI32_Commands); break; case FUNC_JSON_APPEND: // we are not in control of when this is called... //MI32Show(1); break; #ifdef USE_WEBSERVER case FUNC_WEB_ADD_HANDLER: WebServer_on(PSTR("/" WEB_HANDLE_MI32), HandleMI32Key); break; case FUNC_WEB_SENSOR: MI32Show(0); break; #endif // USE_WEBSERVER } return result; } #endif // USE_MI_ESP32 #endif // CONFIG_IDF_TARGET_ESP32 or CONFIG_IDF_TARGET_ESP32C3 #endif // ESP32 #endif