def _func_wrapper(*args, **kwargs): try: return func(*args, **kwargs) except BGAPIError as exception: raise BluetoothBackendException() from exception except NotConnectedError as exception: raise BluetoothBackendException() from exception
def wait_for_notification(self, handle: int, delegate, notification_timeout: float): """Listen for characteristics changes from a BLE address. @param: mac - MAC address in format XX:XX:XX:XX:XX:XX @param: handle - BLE characteristics handle in format 0xXX a value of 0x0100 is written to register for listening @param: delegate - gatttool receives the --listen argument and the delegate object's handleNotification is called for every returned row @param: notification_timeout """ if not self.is_connected(): raise BluetoothBackendException('Not connected to any device.') attempt = 0 delay = 10 _LOGGER.debug("Enter write_ble (%s)", current_thread()) while attempt <= self.retries: cmd = "gatttool --device={} --addr-type={} --char-write-req -a {} -n {} --adapter={} --listen".format( self._mac, self.address_type, self.byte_to_handle(handle), self.bytes_to_string(self._DATA_MODE_LISTEN), self.adapter) _LOGGER.debug("Running gatttool with a timeout of %d: %s", notification_timeout, cmd) with Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, preexec_fn=os.setsid) as process: try: result = process.communicate(timeout=notification_timeout)[0] _LOGGER.debug("Finished gatttool") except TimeoutExpired: # send signal to the process group, because listening always hangs os.killpg(process.pid, signal.SIGINT) result = process.communicate()[0] _LOGGER.debug("Listening stopped forcefully after timeout.") result = result.decode("utf-8").strip(' \n\t') if "Write Request failed" in result: raise BluetoothBackendException('Error writing handle to sensor: {}'.format(result)) _LOGGER.debug("Got %s from gatttool", result) # Parse the output to determine success if "successfully" in result: _LOGGER.debug("Exit write_ble with result (%s)", current_thread()) # extract useful data. for element in self.extract_notification_payload(result): delegate.handleNotification(handle, bytes([int(x, 16) for x in element.split()])) return True attempt += 1 _LOGGER.debug("Waiting for %s seconds before retrying", delay) if attempt < self.retries: time.sleep(delay) delay *= 2 raise BluetoothBackendException("Exit write_ble, no data ({})".format(current_thread()))
def write_handle(self, handle, value): # noqa: C901 # pylint: disable=arguments-differ """Read from a BLE address. @param: mac - MAC address in format XX:XX:XX:XX:XX:XX @param: handle - BLE characteristics handle in format 0xXX @param: value - value to write to the given handle """ if not self.is_connected(): raise BluetoothBackendException('Not connected to any device.') attempt = 0 delay = 10 _LOGGER.debug("Enter write_ble (%s)", current_thread()) while attempt <= self.retries: cmd = "gatttool --device={} --char-write-req -a {} -n {} --adapter={}".format( self._mac, self.byte_to_handle(handle), self.bytes_to_string(value), self.adapter) _LOGGER.debug("Running gatttool with a timeout of %d: %s", self.timeout, cmd) with Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, preexec_fn=os.setsid) as process: try: result = process.communicate(timeout=self.timeout)[0] _LOGGER.debug("Finished gatttool") except TimeoutExpired: # send signal to the process group os.killpg(process.pid, signal.SIGINT) result = process.communicate()[0] _LOGGER.debug("Killed hanging gatttool") result = result.decode("utf-8").strip(' \n\t') if "Write Request failed" in result: raise BluetoothBackendException( 'Error writing handle to sensor: {}'.format(result)) _LOGGER.debug("Got %s from gatttool", result) # Parse the output if "successfully" in result: _LOGGER.debug("Exit write_ble with result (%s)", current_thread()) return True attempt += 1 _LOGGER.debug("Waiting for %s seconds before retrying", delay) if attempt < self.retries: time.sleep(delay) delay *= 2 raise BluetoothBackendException("Exit write_ble, no data ({})".format( current_thread()))
def read_handle(self, handle: int) -> bytes: """Read from a BLE address. @param: mac - MAC address in format XX:XX:XX:XX:XX:XX @param: handle - BLE characteristics handle in format 0xXX @param: timeout - timeout in seconds """ if not self.is_connected(): raise BluetoothBackendException('Not connected to any device.') attempt = 0 delay = 10 _LOGGER.debug("Enter read_ble (%s)", current_thread()) while attempt <= self.retries: cmd = "gatttool --device={} --addr-type={} --char-read -a {} --adapter={}".format( self._mac, self.address_type, self.byte_to_handle(handle), self.adapter) _LOGGER.debug("Running gatttool with a timeout of %d: %s", self.timeout, cmd) with Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, preexec_fn=os.setsid) as process: try: result = process.communicate(timeout=self.timeout)[0] _LOGGER.debug("Finished gatttool") except TimeoutExpired: # send signal to the process group os.killpg(process.pid, signal.SIGINT) result = process.communicate()[0] _LOGGER.debug("Killed hanging gatttool") result = result.decode("utf-8").strip(' \n\t') _LOGGER.debug("Got \"%s\" from gatttool", result) # Parse the output if "read failed" in result: raise BluetoothBackendException( "Read error from gatttool: {}".format(result)) res = re.search("( [0-9a-fA-F][0-9a-fA-F])+", result) if res: _LOGGER.debug("Exit read_ble with result (%s)", current_thread()) return bytes([int(x, 16) for x in res.group(0).split()]) attempt += 1 _LOGGER.debug("Waiting for %s seconds before retrying", delay) if attempt < self.retries: time.sleep(delay) delay *= 2 raise BluetoothBackendException("Exit read_ble, no data ({})".format( current_thread()))
def fill_cache(self): """Fill the cache with new data from the sensor.""" _LOGGER.debug('Filling cache with new sensor data.') with self._bt_interface.connect(self._mac) as connection: # init cache self._cache = {} # battery battery = connection.read_handle(_HANDLE_READ_BATTERY) # pylint: disable=no-member if not battery: raise BluetoothBackendException( "Could not read data from sensor %s" % self._mac) self._cache[P_BATTERY] = ord(battery) # temperature temperature = connection.read_handle(_HANDLE_READ_TEMPERATURE) # pylint: disable=no-member if not temperature: raise BluetoothBackendException( "Could not read data from sensor %s" % self._mac) self._cache[P_TEMPERATURE] = round(unpack("<f", temperature)[0], 1) # moisture moisture = connection.read_handle(_HANDLE_READ_MOISTURE) # pylint: disable=no-member if not moisture: raise BluetoothBackendException( "Could not read data from sensor %s" % self._mac) self._cache[P_MOISTURE] = round(unpack("<f", moisture)[0], 1) # light light = connection.read_handle(_HANDLE_READ_LIGHT) # pylint: disable=no-member if not light: raise BluetoothBackendException( "Could not read data from sensor %s" % self._mac) self._cache[P_LIGHT] = round(unpack("<f", light)[0], 1) # conductivity conductivity = connection.read_handle(_HANDLE_READ_CONDUCTIVITY) # pylint: disable=no-member if not conductivity: raise BluetoothBackendException( "Could not read data from sensor %s" % self._mac) self._cache[P_CONDUCTIVITY] = unpack("<H", conductivity)[0] _LOGGER.debug( 'Cache content: %s', "; ".join([ "%s=%s" % (key, ('%(' + key + ')s') % self._cache) for key in self._cache ])) if self.cache_available(): self._last_read = datetime.now() else: # If a sensor doesn't work, wait 5 minutes before retrying self._last_read = datetime.now() - self._cache_timeout + \ timedelta(seconds=300)
def wait_for_notification(self, handle: int, delegate, notification_timeout: float): if self._peripheral is None: raise BluetoothBackendException('not connected to backend') self.write_handle(handle, self._DATA_MODE_LISTEN) self._peripheral.withDelegate(delegate) return self._peripheral.waitForNotifications(notification_timeout)
def parameter_value(self, parameter, read_cached=True): """Return a value of one of the monitored paramaters. This method will try to retrieve the data from cache and only request it by bluetooth if no cached value is stored or the cache is expired. This behaviour can be overwritten by the "read_cached" parameter. """ # Special handling for battery attribute if parameter == MI_BATTERY: return self.battery_level() # Use the lock to make sure the cache isn't updated multiple times with self.lock: if (read_cached is False) or \ (self._last_read is None) or \ (datetime.now() - self._cache_timeout > self._last_read): self.fill_cache() else: _LOGGER.debug("Using cache (%s < %s)", datetime.now() - self._last_read, self._cache_timeout) if self.cache_available(): return self._parse_data()[parameter] raise BluetoothBackendException( "Could not read data from Mi Temp sensor %s" % self._mac)
def name(self): """Return the name of the sensor.""" with self._bt_interface.connect(self._mac) as connection: name = connection.read_handle(_HANDLE_READ_NAME) # pylint: disable=no-member if not name: raise BluetoothBackendException("Could not read data from Mi Flora sensor %s" % self._mac) return ''.join(chr(n) for n in name)
def write_handle(self, handle: int, value: bytes): """Write a handle from the device. You must be connected to do this. """ if self._peripheral is None: raise BluetoothBackendException('not connected to backend') return self._peripheral.writeCharacteristic(handle, value, True)
def read_handle(self, handle: int) -> bytes: """Read a handle from the device. You must be connected to do this. """ if self._peripheral is None: raise BluetoothBackendException('not connected to backend') return self._peripheral.readCharacteristic(handle)
def firmware_version(self): """Return the firmware version.""" with self._bt_interface.connect(self._mac) as connection: firmwareRevision = connection.read_handle(_HANDLE_READ_VERSION) # pylint: disable=no-member if not firmwareRevision: raise BluetoothBackendException("Could not read data from sensor %s" % self._mac) return firmwareRevision.decode("utf-8").split('_')[1].split('-')[1];
def connect(self, mac): """Connect to a device.""" from bluepy.btle import Peripheral match_result = re.search(r'hci([\d]+)', self.adapter) if match_result is None: raise BluetoothBackendException( 'Invalid pattern "{}" for BLuetooth adpater. ' 'Expetected something like "hci0".'.format(self.adapter)) iface = int(match_result.group(1)) self._peripheral = Peripheral(mac, iface=iface)
def name(self): """ Return the name of the sensor. """ name = read_ble(self._mac, "0x03", retries=self.retries, timeout=self.ble_timeout, adapter=self._adapter) if not name: raise BluetoothBackendException( "Could not read NAME from Mi Temp sensor %s" % (self._mac)) return ''.join(chr(n) for n in name)
def _func_wrapper(*args, **kwargs): error_count = 0 last_error = None while error_count < RETRY_LIMIT: try: return func(*args, **kwargs) except BTLEException as exception: error_count += 1 last_error = exception time.sleep(RETRY_DELAY) _LOGGER.debug('Call to %s failed, try %d of %d', func, error_count, RETRY_LIMIT) raise BluetoothBackendException() from last_error
def fill_cache(self): """Fill the cache with new data from the sensor.""" _LOGGER.debug('Filling cache with new sensor data.') with self._bt_interface.connect(self._mac) as connection: # init cache self._cache = {} # battery battery = connection.read_handle(_HANDLE_READ_BATTERY) # pylint: disable=no-member if not battery: raise BluetoothBackendException("Could not read data from sensor %s" % self._mac) self._cache[P_BATTERY] = ord(battery) # air temperature air_temperature = connection.read_handle(_HANDLE_READ_AIR_TEMPERATURE) # pylint: disable=no-member if not air_temperature: raise BluetoothBackendException("Could not read data from sensor %s" % self._mac) self._cache[P_AIR_TEMPERATURE] = round(unpack("<f",air_temperature)[0], 1) # soil temperature raw_soil_temperature = connection.read_handle(_HANDLE_READ_SOIL_TEMPERATURE) # pylint: disable=no-member if not raw_soil_temperature: raise BluetoothBackendException("Could not read data from sensor %s" % self._mac) soil_temperature = 0.00000003044 * pow(unpack("<H",raw_soil_temperature)[0], 3.0) - 0.00008038 * pow(unpack("<H",raw_soil_temperature)[0], 2.0) + unpack("<H",raw_soil_temperature)[0] * 0.1149 - 30.49999999999999 if soil_temperature < -10.0: soil_temperature = -10.0 elif soil_temperature > 55.0: soil_temperature = 55.0 self._cache[P_SOIL_TEMPERATURE] = round(soil_temperature, 1) #self._cache[P_SOIL_TEMPERATURE] = round(unpack("<f",soil_temperature)[0], 1) # moisture moisture = connection.read_handle(_HANDLE_READ_MOISTURE) # pylint: disable=no-member if not moisture: raise BluetoothBackendException("Could not read data from sensor %s" % self._mac) self._cache[P_MOISTURE] = round(unpack("<f",moisture)[0]) # light light = connection.read_handle(_HANDLE_READ_LIGHT) # pylint: disable=no-member if not light: raise BluetoothBackendException("Could not read data from sensor %s" % self._mac) #light_in_lux = round(unpack("<f",light)[0] * 54) self._cache[P_LIGHT] = round(unpack("<f",light)[0], 2) # conductivity conductivity = connection.read_handle(_HANDLE_READ_CONDUCTIVITY) # pylint: disable=no-member if not conductivity: raise BluetoothBackendException("Could not read data from sensor %s" % self._mac) self._cache[P_CONDUCTIVITY] = unpack("<H",conductivity)[0] _LOGGER.debug('Cache content: %s', "; ".join(["%s=%s" % (key, ('%('+key+')s') % self._cache) for key in self._cache])) if self.cache_available(): self._last_read = datetime.now() else: # If a sensor doesn't work, wait 5 minutes before retrying self._last_read = datetime.now() - self._cache_timeout + \ timedelta(seconds=300)
def scan_for_devices(timeout: float, adapter='hci0') -> List[Tuple[str, str]]: """Scan for bluetooth low energy devices. Note this must be run as root!""" from bluepy.btle import Scanner match_result = re.search(r'hci([\d]+)', adapter) if match_result is None: raise BluetoothBackendException( 'Invalid pattern "{}" for BLuetooth adpater. ' 'Expetected something like "hci0".'.format(adapter)) iface = int(match_result.group(1)) scanner = Scanner(iface=iface) result = [] for device in scanner.scan(timeout): result.append((device.addr, device.getValueText(9))) return result
def update_sensor(self): """ Get data from device. This method reads the handle 0x003f that contains temperature, humidity and battery level. """ # bt_interface = BluetoothInterface(self._backend, "hci0") bt_interface = BluetoothInterface( backend=self._backend, adapter="hci0") try: with bt_interface.connect(self._mac) as connection: raw = connection.read_handle( 0x003F) # pylint: disable=no-member if not raw: raise BluetoothBackendException("Could not read 0x003f handle") raw_bytes = bytearray(raw) temp = int.from_bytes(raw_bytes[1:3], "little") / 10.0 if temp >= 32768: temp = temp - 65535 humidity = int(raw_bytes[4]) battery = int(raw_bytes[9]) self._temp = temp self._humidity = humidity self._battery = battery self._last_update = datetime.now() _LOGGER.debug("%s: Find temperature with value: %s", self._mac, self._temp) _LOGGER.debug("%s: Find humidity with value: %s", self._mac, self._humidity) _LOGGER.debug("%s: Find battery with value: %s", self._mac, self._battery) except BluetoothBackendException: return
def read_handle(self, handle: int) -> bytes: """Read a handle from the device.""" if not self.is_connected(): raise BluetoothBackendException('Not connected to device!') return self._device.char_read_handle(handle)
def _func_wrapper(*args, **kwargs): try: return func(*args, **kwargs) except IOError as exception: raise BluetoothBackendException() from exception
def read_write(self, _, __): # pylint: disable=no-self-use """Writing always fails.""" raise BluetoothBackendException('always raising')
def read_handle(self, _): """Reading always fails.""" raise BluetoothBackendException('always raising')
def write_handle(self, handle: int, value: bytes): """Write a handle to the device.""" if not self.is_connected(): raise BluetoothBackendException('Not connected to device!') self._device.char_write_handle(handle, value, True) return True
def connect(self, mac): """Raise exception when connecting.""" raise BluetoothBackendException('always raising exceptions')
def wait_for_notification(self, handle, delegate, notification_timeout): raise BluetoothBackendException('always raising')