From 696e33a6772bb928eb93939f108dd7815319fd3f Mon Sep 17 00:00:00 2001 From: Max Date: Thu, 9 Dec 2021 11:26:54 +0300 Subject: [PATCH] MI_HM10 added support Mi Scale 1. Added Mi Scale v1/Mi Scale v2 support via BLE advertising packets 2. Added features "scale" and "impedance" to features list --- tasmota/xsns_62_MI_HM10.ino | 246 +++++++++++++++++++++++++++++++++++- 1 file changed, 239 insertions(+), 7 deletions(-) diff --git a/tasmota/xsns_62_MI_HM10.ino b/tasmota/xsns_62_MI_HM10.ino index f9afa1d84..fcded93cd 100644 --- a/tasmota/xsns_62_MI_HM10.ino +++ b/tasmota/xsns_62_MI_HM10.ino @@ -131,6 +131,37 @@ struct cg_packet_t { uint8_t bat; }; }; + +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 scan_entry_t { @@ -175,6 +206,8 @@ struct mi_sensor_t{ uint32_t NMT:1; uint32_t PIR:1; uint32_t Btn:1; + uint32_t scale:1; + uint32_t impedance:1; }; uint32_t raw; } feature; @@ -191,6 +224,7 @@ struct mi_sensor_t{ uint32_t motion:1; uint32_t noMotion:1; uint32_t Btn:1; + uint32_t scale:1; }; uint32_t raw; } eventType; @@ -213,10 +247,46 @@ struct mi_sensor_t{ uint32_t NMT; // no motion time in seconds for the MJYD2S }; uint16_t Btn; + struct { + uint8_t has_impedance; + uint8_t impedance_stabilized; + uint8_t weight_stabilized; + uint8_t weight_removed; + char weight_unit[4]; // kg, lbs, jin or empty when unknown + float weight; + uint16_t impedance; + struct { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + } datetime; + }; }; union { uint8_t bat; // many values seem to be hard-coded garbage (LYWSD0x, GCD1) }; +/* union { + struct { + uint8_t has_impedance; + uint8_t impedance_stabilized; + uint8_t weight_stabilized; + uint8_t weight_removed; + char weight_unit[4]; // kg, lbs, jin or empty when unknown + float weight; + uint16_t impedance; + struct { + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; + } datetime; + }; + } scale;*/ }; struct { @@ -284,6 +354,12 @@ const char S_JSON_HM10_COMMAND[] PROGMEM = "{\"" D_CMND_HM10 "%s%s\"}"; const char kHM10_Commands[] PROGMEM = D_CMND_HM10"|" "Scan|AT|Period|Baud|Time|Auto|Page|Beacon|Block|Option"; +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}"; + + void (*const HM10_Commands[])(void) PROGMEM = { &CmndHM10Scan, &CmndHM10AT, &CmndHM10Period, &CmndHM10Baud, &CmndHM10Time, &CmndHM10Auto, &CmndHM10Page, &CmndHM10Beacon, &CmndHM10Block, &CmndHM10Option }; @@ -299,8 +375,10 @@ void (*const HM10_Commands[])(void) PROGMEM = { &CmndHM10Scan, &CmndHM10AT, &Cmn #define MHOC401 10 #define MHOC303 11 #define ATC 12 +#define MI_SCALE_V1 13 +#define MI_SCALE_V2 14 -#define HM10_TYPES 12 //count this manually +#define HM10_TYPES 14 //count this manually const uint16_t kHM10SlaveID[HM10_TYPES]={ 0x0098, // Flora @@ -314,7 +392,9 @@ const uint16_t kHM10SlaveID[HM10_TYPES]={ 0x0153, // yee-rc 0x0387, // MHO-C401 0x06d3, // MHO-C303 - 0x0a1c // ATC -> this is a fake ID + 0x0a1c, // ATC -> this is a fake ID + 0x181d, // Mi Scale V1 + 0x181b // Mi Scale V2 }; const char kHM10DeviceType1[] PROGMEM = "Flora"; @@ -329,8 +409,10 @@ const char kHM10DeviceType9[] PROGMEM = "YEERC"; const char kHM10DeviceType10[] PROGMEM ="MHOC401"; const char kHM10DeviceType11[] PROGMEM ="MHOC303"; const char kHM10DeviceType12[] PROGMEM ="ATC"; +const char kHM10DeviceType13[] PROGMEM ="MISCALEV1"; +const char kHM10DeviceType14[] PROGMEM ="MISCALEV2"; -const char * kHM10DeviceType[] PROGMEM = {kHM10DeviceType1,kHM10DeviceType2,kHM10DeviceType3,kHM10DeviceType4,kHM10DeviceType5,kHM10DeviceType6,kHM10DeviceType7,kHM10DeviceType8,kHM10DeviceType9,kHM10DeviceType10,kHM10DeviceType11,kHM10DeviceType12}; +const char * kHM10DeviceType[] PROGMEM = {kHM10DeviceType1,kHM10DeviceType2,kHM10DeviceType3,kHM10DeviceType4,kHM10DeviceType5,kHM10DeviceType6,kHM10DeviceType7,kHM10DeviceType8,kHM10DeviceType9,kHM10DeviceType10,kHM10DeviceType11,kHM10DeviceType12,kHM10DeviceType13,kHM10DeviceType14}; /*********************************************************************************************\ * enumerations @@ -599,6 +681,13 @@ uint32_t MIBLEgetSensorSlot(uint8_t (&_MAC)[6], uint16_t _type, int _rssi){ case YEERC: _newSensor.feature.Btn=1; break; + case MI_SCALE_V1: + _newSensor.feature.scale=1; + break; + case MI_SCALE_V2: + _newSensor.feature.scale=1; + _newSensor.feature.impedance=1; + break; default: _newSensor.hum=NAN; _newSensor.feature.temp=1; @@ -804,6 +893,102 @@ void HM10parseCGD1Packet(char * _buf, uint32_t _slot){ // no MiBeacon if(HM10.option.directBridgeMode) HM10.mode.shallTriggerTele = 1; } +void HM10ParseMiScalePacket(char * _buf, uint32_t _slot, uint16_t _type){ +//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 (_type == 0x181d){ // 14-1-1-2 + + if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ + DEBUG_SENSOR_LOG(PSTR("HM10: %s: at slot %u"), kHM10DeviceType[MIBLEsensors[_slot].type-1],_slot); + + MIBLEsensors[_slot].eventType.scale = 1; + + MIBLEsensors[_slot].weight_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; + } + // Can be changed to memcpy or smthng else ? + MIBLEsensors[_slot].datetime.year = _packetV2->year; + MIBLEsensors[_slot].datetime.month = _packetV2->month; + MIBLEsensors[_slot].datetime.day = _packetV2->day; + MIBLEsensors[_slot].datetime.hour = _packetV2->hour; + MIBLEsensors[_slot].datetime.minute = _packetV2->minute; + MIBLEsensors[_slot].datetime.second = _packetV2->second; + + MIBLEsensors[_slot].shallSendMQTT = 1; + bool triggerTele = MIBLEsensors[_slot].weight_stabilized && ! MIBLEsensors[_slot].weight_removed && MIBLEsensors[_slot].weight > 0; + + if(HM10.option.directBridgeMode || triggerTele) HM10.mode.shallTriggerTele = 1; + } + } + + // Mi Scale V2 + else if (_type == 0x181b){ // 17-1-1-2 + + if ((_slot >= 0) && (_slot < MIBLEsensors.size())){ + DEBUG_SENSOR_LOG(PSTR("HM10: %s: at slot %u"), kHM10DeviceType[MIBLEsensors[_slot].type-1],_slot); + + MIBLEsensors[_slot].eventType.scale = 1; + + MIBLEsensors[_slot].has_impedance = (_packetV2->status & (1 << 1)) ? 1 : 0; + MIBLEsensors[_slot].weight_stabilized = (_packetV2->status & (1 << 5)) ? 1 : 0; + MIBLEsensors[_slot].impedance_stabilized = (_packetV2->status & (1 << 1)) ? 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; + MIBLEsensors[_slot].impedance = 0; + } + else if (MIBLEsensors[_slot].has_impedance) { + MIBLEsensors[_slot].impedance = _packetV2->impedance; + } + // Can be changed to memcpy or smthng else ? + MIBLEsensors[_slot].datetime.year = _packetV2->year; + MIBLEsensors[_slot].datetime.month = _packetV2->month; + MIBLEsensors[_slot].datetime.day = _packetV2->day; + MIBLEsensors[_slot].datetime.hour = _packetV2->hour; + MIBLEsensors[_slot].datetime.minute = _packetV2->minute; + MIBLEsensors[_slot].datetime.second = _packetV2->second; + + MIBLEsensors[_slot].shallSendMQTT = 1; + + bool triggerTele = MIBLEsensors[_slot].weight_stabilized && ! MIBLEsensors[_slot].weight_removed && MIBLEsensors[_slot].weight > 0; + if(HM10.option.directBridgeMode || triggerTele) HM10.mode.shallTriggerTele = 1; + } + } +} + + void HM10ParseResponse(char *buf, uint16_t bufsize) { if (!strncmp(buf,"HMSoft",6)) { //8 const char* _fw = "000"; @@ -1168,12 +1353,15 @@ bool HM10SerialHandleFeedback(){ // every 50 milliseconds } uint16_t _type = (uint8_t)HM10.rxAdvertisement.svcData[5]*256 + (uint8_t)HM10.rxAdvertisement.svcData[4]; // AddLog(LOG_LEVEL_DEBUG, PSTR("%04x %02x %04x %04x %04x"),HM10.rxAdvertisement.UUID,HM10.rxAdvertisement.TX,HM10.rxAdvertisement.CID,HM10.rxAdvertisement.SVC, _type); + DEBUG_SENSOR_LOG(PSTR("HM10: UUID %04x, TX: %02x, CID: %04x, SVC: %04x"), HM10.rxAdvertisement.UUID,HM10.rxAdvertisement.TX,HM10.rxAdvertisement.CID,HM10.rxAdvertisement.SVC); if(HM10.rxAdvertisement.SVC==0x181a) _type = 0xa1c; else if(HM10.rxAdvertisement.SVC==0xfdcd) _type = 0x0576; + else if(HM10.rxAdvertisement.SVC==0x181b || HM10.rxAdvertisement.SVC==0x181d) _type = 0x181b; uint16_t _slot = MIBLEgetSensorSlot(HM10.rxAdvertisement.MAC, _type, HM10.rxAdvertisement.RSSI); if(_slot!=0xff){ if (_type==0xa1c) HM10parseATC((char*)HM10.rxAdvertisement.svcData+2,_slot); else if (_type==0x0576) HM10parseCGD1Packet((char*)HM10.rxAdvertisement.svcData+2,_slot); + else if (_type==0x181b) HM10ParseMiScalePacket((char*)HM10.rxAdvertisement.svcData+2,_slot, _type); else HM10parseMiBeacon((char*)HM10.rxAdvertisement.svcData+2,_slot); } else{ @@ -1425,10 +1613,10 @@ void HM10_TaskEvery100ms(){ // AddLog(LOG_LEVEL_DEBUG, PSTR("%sFound done HM10_TASK"),D_CMND_HM10); // AddLog(LOG_LEVEL_DEBUG, PSTR("%snext slot:%u, i: %u"),D_CMND_HM10, HM10_TASK_LIST[i+1][0],i); if(HM10_TASK_LIST[i+1][0] == TASK_HM10_NOTASK) { // check the next entry and if there is none - DEBUG_SENSOR_LOG(PSTR("%sno Tasks left"),D_CMND_HM10); - DEBUG_SENSOR_LOG(PSTR("%sHM10_TASK_DONE current slot %u"),D_CMND_HM10, i); + DEBUG_SENSOR_LOG(PSTR("%s: no Tasks left"),D_CMND_HM10); + DEBUG_SENSOR_LOG(PSTR("%s: HM10_TASK_DONE current slot %u"),D_CMND_HM10, i); for (uint8_t j = 0; j < HM10_MAX_TASK_NUMBER+1; j++) { // do a clean-up: - DEBUG_SENSOR_LOG(PSTR("%sHM10_TASK cleanup slot %u"),D_CMND_HM10, j); + DEBUG_SENSOR_LOG(PSTR("%s: HM10_TASK cleanup slot %u"),D_CMND_HM10, j); HM10_TASK_LIST[j][0] = TASK_HM10_NOTASK; // reset all task entries HM10_TASK_LIST[j][1] = 0; // reset all delays } @@ -1927,6 +2115,39 @@ void HM10Show(bool json) } } } + //Scale + if (MIBLEsensors[i].feature.scale){ + if(MIBLEsensors[i].eventType.scale || !HM10.mode.triggeredTele || HM10.option.allwaysAggregate + #ifdef USE_HOME_ASSISTANT + ||(hass_mode==2) + #endif //USE_HOME_ASSISTANT + ){ + HM10ShowContinuation(&commaflg); + ResponseAppend_P(PSTR("\"weight_removed\":%u"), MIBLEsensors[i].weight_removed); + HM10ShowContinuation(&commaflg); + ResponseAppend_P(PSTR("\"weight_stabilized\":%u"), MIBLEsensors[i].weight_stabilized); + HM10ShowContinuation(&commaflg); + ResponseAppend_P(PSTR("\"weight_unit\":\"%s\""), MIBLEsensors[i].weight_unit); + HM10ShowContinuation(&commaflg); + ResponseAppend_P(PSTR("\"" D_JSON_WEIGHT "\":%*_f"),Settings->flag2.weight_resolution, &MIBLEsensors[i].weight); + HM10ShowContinuation(&commaflg); + ResponseAppend_P(PSTR("\"datetime\":\"%02u/%02u/%04u %02u:%02u:%02u\"") + , MIBLEsensors[i].datetime.day + , MIBLEsensors[i].datetime.month + , MIBLEsensors[i].datetime.year + , MIBLEsensors[i].datetime.hour + , MIBLEsensors[i].datetime.minute + , MIBLEsensors[i].datetime.second + ); + } + } + if (MIBLEsensors[i].feature.impedance){ + HM10ShowContinuation(&commaflg); + ResponseAppend_P(PSTR("\"impedance\":%u"), MIBLEsensors[i].has_impedance ? MIBLEsensors[i].impedance : 0); + HM10ShowContinuation(&commaflg); + ResponseAppend_P(PSTR("\"impedance_stabilized\":%u"), MIBLEsensors[i].impedance_stabilized); + } + if (HM10.option.showRSSI) { HM10ShowContinuation(&commaflg); ResponseAppend_P(PSTR("\"RSSI\":%d"), MIBLEsensors[i].rssi); @@ -1992,7 +2213,18 @@ void HM10Show(bool json) WSContentSend_PD(HTTP_HM10_FLORA_DATA, kHM10DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].fertility); } } - if (MIBLEsensors[i].type>FLORA){ // everything "above" Flora + if (MIBLEsensors[i].type==MI_SCALE_V1 || MIBLEsensors[i].type==MI_SCALE_V2){ + + if (MIBLEsensors[i].feature.scale){ + WSContentSend_PD(HTTP_MISCALE_WEIGHT, kHM10DeviceType[MIBLEsensors[i].type-1], Settings->flag2.weight_resolution, &MIBLEsensors[i].weight, MIBLEsensors[i].weight_unit); + WSContentSend_PD(HTTP_MISCALE_WEIGHT_REMOVED, kHM10DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].weight_removed ? PSTR("yes") : PSTR("no")); + WSContentSend_PD(HTTP_MISCALE_STABILIZED, kHM10DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].weight_stabilized ? PSTR("yes") : PSTR("no")); + } + if (MIBLEsensors[i].feature.impedance){ + WSContentSend_PD(HTTP_MISCALE_IMPEDANCE, kHM10DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].has_impedance ? MIBLEsensors[i].impedance : 0); + } + } + else if (MIBLEsensors[i].type>FLORA){ // everything "above" Flora if(!isnan(MIBLEsensors[i].hum) && !isnan(MIBLEsensors[i].temp)){ WSContentSend_THD(kHM10DeviceType[MIBLEsensors[i].type-1], MIBLEsensors[i].temp, MIBLEsensors[i].hum); }