def handleDiscovery(self, device: ScanEntry, isNew: bool, isData: bool): if not isNew: return device_name = device.getValueText(ScanEntry.COMPLETE_LOCAL_NAME) if not isinstance(device_name, str) or not device_name.startswith("Snooz"): return advertisement = device.getValue(ScanEntry.MANUFACTURER) # unexpected data if len(advertisement) < 3: return if advertisement[:3] == b"\xFF\xFF\x08": _LOGGER.warning( f"Discovered {device_name} ({device.addr}) but it is not in pairing mode." ) return token = b64encode(advertisement[3:]).decode("UTF-8") snooz_device = DiscoveredSnooz(device_name, token, device) self.devices.append(snooz_device)
def _update(self, resp): ScanEntry._update(self, resp) if self.addrType is "random": self.vendor = "None (Random MAC address)" else: self.vendor = lookup_vendor(self.addr) if self.scanData: self.data = self._get_data(self.getScanData())
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 buildFromScanEntry(scanEntry: ScanEntry) -> Optional["OralBAdvertise"]: vendorSpecificData = scanEntry.getValueText(ScanEntry.MANUFACTURER) parser = OralBAdvertise(vendorSpecificData) if parser.isValid: return parser else: return None
def _find_or_create(self, addr): if addr in self.scanned: dev = self.scanned[addr] else: dev = ScanEntry(addr, self.iface) self.scanned[addr] = dev return dev
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 process(self, timeout=10.0): 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': # 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) if self.delegate is not None: self.delegate.handleDiscovery(dev, (dev.updateCount <= 1), isNewData) else: raise BTLEException(BTLEException.INTERNAL_ERROR, "Unexpected response: " + respType)
def __init__(self, addr, iface): ScanEntry.__init__(self, addr, iface) self.vendor = None self.data = []
from unittest import TestCase from time import time from bluepy.btle import ScanEntry from scripts.scan import AdvertisingPacket from scripts.beacon_container import BeaconContainer scan_entry = ScanEntry('7e:96:b0:dc:fc:95', 0) scan_entry2 = ScanEntry('7e:96:b0:dc:fc:96', 0) packet = AdvertisingPacket(scan_entry) packet2 = AdvertisingPacket(scan_entry2) class TestBeaconContainer(TestCase): def test_clean(self): container = BeaconContainer() packet.updated_at = 0 packet2.updated_at = time() container.insert(packet.addr, packet) container.insert(packet2.addr, packet2) container.clean() self.assertIsNone(container.get('7e:96:b0:dc:fc:95')) self.assertIsNotNone(container.get('7e:96:b0:dc:fc:96')) def test_iter(self): container = BeaconContainer() container.insert(packet.addr, packet) container.insert(packet2.addr, packet2) # print container for item in container: self.assertIsNotNone(item)
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 handleDiscovery(self, dev: ScanEntry, isNewDev: bool, isNewData: bool): name = dev.getValue(ScanEntry.COMPLETE_LOCAL_NAME) or "" toy_type = TOY_BY_PREFIX.get(name[:3]) if toy_type: self.queue.put_nowait(_ScanItem(dev.addr, toy_type, name))
def is_turn_touch(device: btle.ScanEntry) -> bool: """Determine whether a ScanEntry (device) is a Turn Touch.""" short_name = device.getValueText(BLE_SHORT_DEVICE_NAME) name = device.getValueText(BLE_COMPLETE_DEVICE_NAME) return name == TT_DEVICE_NAME or short_name == TT_SHORT_DEVICE_NAME
def relay_ble_advertisement(self, scanEntry: ScanEntry): """ Send the BLE packet to the connected service running on iOS """ ### Message format # 1 byte type - 4 bytes message length - message ### name = scanEntry.getValueText( ScanEntry.COMPLETE_LOCAL_NAME) if not name: name = scanEntry.getValueText( ScanEntry.SHORT_LOCAL_NAME) # services_16 = scanEntry.getValueText(ScanEntry.INCOMPLETE_16B_SERVICES) # services_32 = scanEntry.getValueText(ScanEntry.INCOMPLETE_32B_SERVICES) # services_128 = scanEntry.getValueText(ScanEntry.INCOMPLETE_128B_SERVICES) # print("Services 16B: \n\t{}\nServices 32B: \n\t{}\nServices 128B: \n\t{}".format(services_16, services_32 , services_128 )) DBG("Raw Data: {}".format(scanEntry.rawData), logLevel=LogLevel.DEBUG) raw_data_hex = "" if scanEntry.rawData: raw_data_hex = scanEntry.rawData.hex() packet_content = { "manufacturerDataHex": scanEntry.getValueText(ScanEntry.MANUFACTURER), "macAddress": scanEntry.addr, "rssi": scanEntry.rssi, "name": name, "flags": scanEntry.getValueText( ScanEntry.FLAGS), "addressType": scanEntry.addrType, "connectable": scanEntry.connectable, "rawData": raw_data_hex, #"scanData": {tag: scanEntry.getValueText(k) for k, tag in scanEntry.dataTags.items()} } # Check for additional content in this advertisement # Get service UUIDs serviceUUIDs = list() if scanEntry.getValueText(ScanEntry.INCOMPLETE_16B_SERVICES): serviceUUIDs.append(scanEntry.getValueText(ScanEntry.INCOMPLETE_16B_SERVICES)) if scanEntry.getValueText(ScanEntry.INCOMPLETE_32B_SERVICES): serviceUUIDs.append(scanEntry.getValueText(ScanEntry.INCOMPLETE_32B_SERVICES)) if scanEntry.getValueText(ScanEntry.INCOMPLETE_128B_SERVICES): serviceUUIDs.append(scanEntry.getValueText(ScanEntry.INCOMPLETE_128B_SERVICES)) if scanEntry.getValueText(ScanEntry.COMPLETE_16B_SERVICES): serviceUUIDs.append(scanEntry.getValueText(ScanEntry.COMPLETE_16B_SERVICES)) if scanEntry.getValueText(ScanEntry.COMPLETE_32B_SERVICES): serviceUUIDs.append(scanEntry.getValueText(ScanEntry.COMPLETE_32B_SERVICES)) if scanEntry.getValueText(ScanEntry.COMPLETE_128B_SERVICES): serviceUUIDs.append(scanEntry.getValueText(ScanEntry.COMPLETE_128B_SERVICES)) if len(serviceUUIDs) > 0: packet_content["serviceUUIDs"] = serviceUUIDs # Get service data if scanEntry.getValueText(ScanEntry.SERVICE_DATA_16B): packet_content["serviceData16Bit"] = scanEntry.getValueText(ScanEntry.SERVICE_DATA_16B) if scanEntry.getValueText(ScanEntry.SERVICE_DATA_32B): packet_content["serviceData32Bit"] = scanEntry.getValueText(ScanEntry.SERVICE_DATA_32B) if scanEntry.getValueText(ScanEntry.SERVICE_DATA_128B): packet_content["serviceData128Bit"] = scanEntry.getValueText(ScanEntry.SERVICE_DATA_128B) DBG("Encoding json: ", packet_content) json_packet = json.dumps(packet_content).encode() # Message type for advertisements is 0 self.send_packet_over_socket(packet_type=0, packet_data=json_packet)
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)