diff --git a/custom_components/airthings_wave/airthings.py b/custom_components/airthings_wave/airthings.py index 693ac9b..32f1985 100644 --- a/custom_components/airthings_wave/airthings.py +++ b/custom_components/airthings_wave/airthings.py @@ -1,3 +1,23 @@ +# Copyright (c) 2021 Martin Tremblay, Mark McCans +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + import struct import time from collections import namedtuple @@ -5,18 +25,22 @@ import logging from datetime import datetime -import bluepy.btle as btle +from bleak import BleakClient +from bleak import BleakScanner +import asyncio from uuid import UUID _LOGGER = logging.getLogger(__name__) -# Use full UUID since we do not use UUID from bluepy.btle -CHAR_UUID_CCCD = btle.UUID('2902') # Client Characteristic Configuration Descriptor (CCCD) +# Use full UUID since we do not use UUID from bluetooth library CHAR_UUID_MANUFACTURER_NAME = UUID('00002a29-0000-1000-8000-00805f9b34fb') CHAR_UUID_SERIAL_NUMBER_STRING = UUID('00002a25-0000-1000-8000-00805f9b34fb') CHAR_UUID_MODEL_NUMBER_STRING = UUID('00002a24-0000-1000-8000-00805f9b34fb') CHAR_UUID_DEVICE_NAME = UUID('00002a00-0000-1000-8000-00805f9b34fb') +CHAR_UUID_FIRMWARE_REV = UUID('00002a26-0000-1000-8000-00805f9b34fb') +CHAR_UUID_HARDWARE_REV = UUID('00002a27-0000-1000-8000-00805f9b34fb') + CHAR_UUID_DATETIME = UUID('00002a08-0000-1000-8000-00805f9b34fb') CHAR_UUID_TEMPERATURE = UUID('00002a6e-0000-1000-8000-00805f9b34fb') CHAR_UUID_HUMIDITY = UUID('00002a6f-0000-1000-8000-00805f9b34fb') @@ -34,18 +58,22 @@ device_info_characteristics = [manufacturer_characteristics, Characteristic(CHAR_UUID_SERIAL_NUMBER_STRING, 'serial_nr', "utf-8"), Characteristic(CHAR_UUID_MODEL_NUMBER_STRING, 'model_nr', "utf-8"), - Characteristic(CHAR_UUID_DEVICE_NAME, 'device_name', "utf-8")] + Characteristic(CHAR_UUID_DEVICE_NAME, 'device_name', "utf-8"), + Characteristic(CHAR_UUID_FIRMWARE_REV, 'firmware_rev', "utf-8"), + Characteristic(CHAR_UUID_HARDWARE_REV, 'hardware_rev', "utf-8")] class AirthingsDeviceInfo: - def __init__(self, manufacturer='', serial_nr='', model_nr='', device_name=''): + def __init__(self, manufacturer='', serial_nr='', model_nr='', device_name='', firmware_rev='', hardware_rev=''): self.manufacturer = manufacturer self.serial_nr = serial_nr self.model_nr = model_nr self.device_name = device_name + self.firmware_rev = firmware_rev + self.hardware_rev = hardware_rev def __str__(self): - return "Manufacturer: {} Model: {} Serial: {} Device:{}".format( - self.manufacturer, self.model_nr, self.serial_nr, self.device_name) + return "Manufacturer: {} Model: {} Serial: {} Device: {} Firmware: {} Hardware Rev.: {}".format( + self.manufacturer, self.model_nr, self.serial_nr, self.device_name, self.firmware_rev, self.hardware_rev) sensors_characteristics_uuid = [CHAR_UUID_DATETIME, CHAR_UUID_TEMPERATURE, CHAR_UUID_HUMIDITY, CHAR_UUID_RADON_1DAYAVG, @@ -157,20 +185,6 @@ def decode_data(self, raw_data): return res - -class MyDelegate(btle.DefaultDelegate): - def __init__(self): - btle.DefaultDelegate.__init__(self) - # ... initialise here - self.data = None - - def handleNotification(self, cHandle, data): - if self.data is None: - self.data = data - else: - self.data = self.data + data - - sensor_decoders = {str(CHAR_UUID_WAVE_PLUS_DATA):WavePlussDecode(name="Pluss", format_type='BBBBHHHHHHHH', scale=0), str(CHAR_UUID_DATETIME):WaveDecodeDate(name="date_time", format_type='HBBBBB', scale=0), str(CHAR_UUID_HUMIDITY):BaseDecode(name="humidity", format_type='H', scale=1.0/100.0), @@ -192,152 +206,158 @@ def __init__(self, scan_interval, mac=None): self.scan_interval = scan_interval self.last_scan = -1 self._dev = None - - def _parse_serial_number(self, manufacturer_data): - try: - (ID, SN, _) = struct.unpack(" self.scan_interval or self.last_scan == -1: self.last_scan = time.monotonic() for mac, characteristics in self.sensors.items(): - self.connect(mac) - if self._dev is not None: + await self.connect(mac) + if self._dev is not None and self._dev.is_connected: try: for characteristic in characteristics: sensor_data = None if str(characteristic.uuid) in sensor_decoders: - char = self._dev.getCharacteristics(uuid=characteristic.uuid)[0] - data = char.read() + data = await self._dev.read_gatt_char(characteristic.uuid) sensor_data = sensor_decoders[str(characteristic.uuid)].decode_data(data) _LOGGER.debug("{} Got sensordata {}".format(mac, sensor_data)) - + if str(characteristic.uuid) in command_decoders: - self.delgate.data = None # Clear the delegate so it is ready for new data. - char = self._dev.getCharacteristics(uuid=characteristic.uuid)[0] - # Do these steps to get notification to work, I do not know how it works, this link should explain it - # https://devzone.nordicsemi.com/guides/short-range-guides/b/bluetooth-low-energy/posts/ble-characteristics-a-beginners-tutorial - desc, = char.getDescriptors(forUUID=CHAR_UUID_CCCD) - desc.write(struct.pack(' 0: - devices = ad.get_info() + devices = await ad.get_info() for mac, dev in devices.items(): - _LOGGER.info("{}: {}".format(mac, dev)) + _LOGGER.info("Device: {}: {}".format(mac, dev)) - devices_sensors = ad.get_sensors() + devices_sensors = await ad.get_sensors() for mac, sensors in devices_sensors.items(): for sensor in sensors: - _LOGGER.info("{}: {}".format(mac, sensor)) + _LOGGER.info("Sensor: {}: {}".format(mac, sensor)) - sensordata = ad.get_sensor_data() + sensordata = await ad.get_sensor_data() for mac, data in sensordata.items(): for name, val in data.items(): - _LOGGER.info("{}: {}: {}".format(mac, name, val)) + _LOGGER.info("Sensor data: {}: {}: {}".format(mac, name, val)) + + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/custom_components/airthings_wave/manifest.json b/custom_components/airthings_wave/manifest.json index 6aee1c3..8a4bebf 100644 --- a/custom_components/airthings_wave/manifest.json +++ b/custom_components/airthings_wave/manifest.json @@ -1,12 +1,12 @@ { "domain": "airthings_wave", "name": "Airthings Wave", - "version": "3.1.0", + "version": "4.0.0", "documentation": "https://github.com/custom-components/sensor.airthings_wave/", "issue_tracker": "https://github.com/custom-components/sensor.airthings_wave/issues", "dependencies": [], "codeowners": ["@MartyTremblay","@sverrham"], "requirements": [ - "bluepy==1.3.0" + "bleak==0.14.3" ] } diff --git a/custom_components/airthings_wave/sensor.py b/custom_components/airthings_wave/sensor.py index a4c9427..c45d6d0 100644 --- a/custom_components/airthings_wave/sensor.py +++ b/custom_components/airthings_wave/sensor.py @@ -15,6 +15,7 @@ import logging from datetime import timedelta from math import exp +import asyncio from .airthings import AirthingsWaveDetect @@ -209,7 +210,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): airthingsdetect = AirthingsWaveDetect(scan_interval, mac) try: if mac is None: - num_devices_found = airthingsdetect.find_devices() + num_devices_found = asyncio.run(airthingsdetect.find_devices()) _LOGGER.info("Found {} airthings device(s)".format(num_devices_found)) if mac is None and num_devices_found == 0: @@ -217,19 +218,19 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return _LOGGER.debug("Getting info about device(s)") - devices_info = airthingsdetect.get_info() + devices_info = asyncio.run(airthingsdetect.get_info()) for mac, dev in devices_info.items(): _LOGGER.info("{}: {}".format(mac, dev)) _LOGGER.debug("Getting sensors") - devices_sensors = airthingsdetect.get_sensors() + devices_sensors = asyncio.run(airthingsdetect.get_sensors()) for mac, sensors in devices_sensors.items(): for sensor in sensors: _LOGGER.debug("{}: Found sensor UUID: {} Handle: {}".format(mac, sensor.uuid, sensor.handle)) _LOGGER.debug("Get initial sensor data to populate HA entities") ha_entities = [] - sensordata = airthingsdetect.get_sensor_data() + sensordata = asyncio.run(airthingsdetect.get_sensor_data()) for mac, data in sensordata.items(): for name, val in data.items(): _LOGGER.debug("{}: {}: {}".format(mac, name, val)) @@ -302,7 +303,7 @@ def update(self): """Fetch new state data for the sensor. This is the only method that should fetch new data for Home Assistant. """ - self.device.get_sensor_data() + asyncio.run(self.device.get_sensor_data()) value = self.device.sensordata[self._mac][self._sensor_name] self._state = self._sensor_specifics.transform(value) _LOGGER.debug("State {} {}".format(self._name, self._state))