def gatherAverageRSSI(self, manufacturer, n_samples, passive=False): self.clear() self.start(passive=passive) rssi_scans = [] while len(rssi_scans) < n_samples: # wait 3 seconds before a timeout resp = self._waitResp(['scan', 'stat'], 3.0) # ^ if resp is None: break respType = resp['rsp'][0] if respType == 'stat': # if scan ended, restart it if resp['state'][0] == 'disc': self._mgmtCmd("scan") elif respType == 'scan': # device found addr = binascii.b2a_hex(resp['addr'][0]).decode('utf-8') addr = ':'.join([addr[i:i + 2] for i in range(0, 12, 2)]) if addr in self.scanned: dev = self.scanned[addr] else: dev = ScanEntry(addr, self.iface) self.scanned[addr] = dev isNewData = dev._update(resp) for (adtype, desc, value) in dev.getScanData(): if desc == "Manufacturer" and value == manufacturer: rssi_scans.append(dev.rssi) else: raise BTLEException(BTLEException.INTERNAL_ERROR, "Unexpected response: " + respType) mean_rssi = mean(rssi_scans) std_dev = stdev(rssi_scans) cutoff_rssi = [] lower_cutoff = mean_rssi - std_dev upper_cutoff = mean_rssi + std_dev for x in rssi_scans: if not (x < lower_cutoff or x > upper_cutoff): cutoff_rssi.append(x) self.stop() return mean(cutoff_rssi)
def _get_dev_name_from_scan_entry(self, dev: ScanEntry): dev_name = None # Attempt to load from Scandata for scan_data in dev.getScanData(): if scan_data[1] == 'Short Local Name': dev_name = scan_data[2] # Iterate through the dataTags if the above fails if dev_name is None: try: for idx, label in scan_data.dataTags: if label == 'Short Local Name': dev_name = dev.getValueText(idx) except AttributeError: pass # Load using handles if the above fails if dev_name is None: dev_name = dev.getValueText(8) return dev_name
def handleDiscovery(self, dev: ScanEntry, new_dev: bool, new_data: bool) -> None: if not dev.addr == self.mac.lower() or not new_dev or not new_data: return for (sdid, _, data) in dev.getScanData(): # Mi Body Composition Scale 2 (XMTZC05HM) / Xiaomi Scale 2 (XMTZC02HM) if not data.startswith("1b18") or sdid != 22: continue # 15b in little endian # 0-1: identifier? # 2: unit # 3: control byte # 4-5: year # 6: month # 7: day # 8: hour # 9: min # 10: sec # 11-12: impedance # 13-14: weight # unpack bytes to dictionary measured = dict(zip(DATA_KEYS, unpack("<xxBBHBBBBBHH", bytes.fromhex(data)))) # check if we got a proper measurement measurement_stabilized = measured["control"] & (1 << 5) impedance_available = measured["control"] & (1 << 1) # pick unit unit = UNITS.get(measured["unit_id"], None) # calc weight based on unit weight = measured["weight"] / 100 / 2 if measured["unit_id"] == 2 else measured["weight"] / 100 # check if we got a proper measurement if not all([measurement_stabilized, unit]): logging.debug(f"missing data! weight: {weight} | unit: {unit} | impedance: {measured['impedance']}") continue # create datetime measurement_datetime = datetime( measured["year"], measured["month"], measured["day"], measured["hour"], measured["min"], measured["sec"] ) # find the current user based on its weight if user := self.find_user(weight): bm = xbm.BodyMetrics( user[ATTRS.WEIGHT.value], user[ATTRS.HEIGHT.value], user[ATTRS.AGE.value], user[ATTRS.SEX.value], measured["impedance"], ) attributes = { ATTRS.USER.value: user[ATTRS.USER.value], ATTRS.AGE.value: user[ATTRS.AGE.value], ATTRS.SEX.value: user[ATTRS.SEX.value], ATTRS.HEIGHT.value: user[ATTRS.HEIGHT.value], ATTRS.WEIGHT.value: f"{weight:.2f}", ATTRS.UNIT.value: unit, ATTRS.BASAL_METABOLISM.value: f"{bm.get_bmr():.2f}", ATTRS.VISCERAL_FAT.value: f"{bm.getVisceralFat():.2f}", ATTRS.BMI.value: f"{bm.getBMI():.2f}", ATTRS.TIMESTAMP.value: measurement_datetime.isoformat(), } # if we got a valid impedance, we can add more metrics if impedance_available: attributes.update( { ATTRS.WATER.value: f"{bm.getWaterPercentage():.2f}", ATTRS.BONE_MASS.value: f"{bm.getBoneMass():.2f}", ATTRS.BODY_FAT.value: f"{bm.getFatPercentage():.2f}", ATTRS.LEAN_BODY_MASS.value: f"{bm.get_lbm_coefficient():.2f}", ATTRS.MUSCLE_MASS.value: f"{bm.getMuscleMass():.2f}", ATTRS.PROTEIN.value: f"{bm.getProteinPercentage():.2f}", } ) self.data.update( { "name": PLUGIN_NAME, "sensors": [ { "name": f"{self.alias} {user[ATTRS.USER.value]}", "value_template": "{{value_json." + ATTRS.WEIGHT.value + "}}", "entity_type": ATTRS.WEIGHT, "own_state_topic": True, }, ], "attributes": attributes, } )
def process(self, timeout=300.0): """ Method receives advertisements from nodes. Variable timeout has default value 300 seconds, it define time after which method will end. """ if self._helper is None: raise BTLEException(BTLEException.INTERNAL_ERROR, "Helper not started (did you call start()?)") start = time.time() while True: if timeout: remain = start + timeout - time.time() if remain <= 0.0: break else: remain = None resp = self._waitResp(['scan', 'stat'], remain) if resp is None: break respType = resp['rsp'][0] if respType == 'stat': logging.info("STAT message,") # if scan ended, restart it if resp['state'][0] == 'disc': logging.warning("Executing SCAN cmd!") self._mgmtCmd("scan") elif respType == 'scan': # device found addr = binascii.b2a_hex(resp['addr'][0]).decode('utf-8') addr = ':'.join([addr[i:i + 2] for i in range(0, 12, 2)]) addr = addr.upper() if not Device.select(lambda d: d.id.upper() == addr).first( ) and not settings.ALLOW_ANY: logging.warning( "Unknown device {} send message, skipping...".format( addr)) continue dev = ScanEntry(addr, self.iface) logging.info("SCAN message from {}".format(addr)) dev._update(resp) if not settings.ALLOW_ANY: name = '' for data in dev.getScanData(): for x in data: if type("Name") == type(x) and "Name" in x: name = data[-1] if not settings.NAME_VALIDATION: logging.warning( "Accepting device without name validation...") elif Device[addr].name != name and Device[ addr].name != None: logging.warning( "{} invalid name valid: {}, received: {}, skipping..." .format(addr, name, Device[addr].name)) continue if self.delegate is not None: self.delegate.handleDiscovery(dev, (dev.updateCount <= 1), True) else: raise BTLEException(BTLEException.INTERNAL_ERROR, "Unexpected response: " + respType)