Beispiel #1
0
 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
Beispiel #2
0
    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()))
Beispiel #3
0
    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()))
Beispiel #4
0
    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)
Beispiel #6
0
 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)
Beispiel #7
0
    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)
Beispiel #8
0
    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)
Beispiel #9
0
    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)
Beispiel #10
0
    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)
Beispiel #11
0
    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];
Beispiel #12
0
 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)
Beispiel #14
0
 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
Beispiel #15
0
    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)
Beispiel #16
0
    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
Beispiel #17
0
    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
Beispiel #18
0
 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)
Beispiel #19
0
 def _func_wrapper(*args, **kwargs):
     try:
         return func(*args, **kwargs)
     except IOError as exception:
         raise BluetoothBackendException() from exception
Beispiel #20
0
 def read_write(self, _, __):  # pylint: disable=no-self-use
     """Writing always fails."""
     raise BluetoothBackendException('always raising')
Beispiel #21
0
 def read_handle(self, _):
     """Reading always fails."""
     raise BluetoothBackendException('always raising')
Beispiel #22
0
 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
Beispiel #23
0
 def connect(self, mac):
     """Raise exception when connecting."""
     raise BluetoothBackendException('always raising exceptions')
Beispiel #24
0
 def wait_for_notification(self, handle, delegate, notification_timeout):
     raise BluetoothBackendException('always raising')