From cf9bc83bc0ff66a840c04829723baa9bb1e58a3c Mon Sep 17 00:00:00 2001 From: sverre Date: Thu, 27 Jan 2022 21:47:51 +0100 Subject: [PATCH 1/9] Updates README with input from user that it is not necessary to register with the official app first before getting data. Fixes #71 --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4d63a66..6f80a98 100644 --- a/README.md +++ b/README.md @@ -61,15 +61,14 @@ sensor: ## Limitations -It may be possible that the Wave must be connected to the official app at least -once before you can use this program, so you will probably not get around -registering an account with Airthings. +Users has reported that it is possible to get data without first registering with the official app, +so it should be possible to use the sensor with this integration without registering. The radon level history stored on the Wave itself cannot be accessed with this component. To get around this, it connects regularly to the radon detector. -Make sure you install the latest firmware on the device using the official app +It might be beneficial to install the latest firmware on the device using the official app first. ## Known Issues From b7a35b00513a28e613103789390002fb0c7bf23f Mon Sep 17 00:00:00 2001 From: sverre Date: Sun, 30 Jan 2022 19:26:07 +0100 Subject: [PATCH 2/9] Adds back end support for getting battery voltage by communicating with the command interface uuid. Tested and working with the airthings pluss only. #70 --- custom_components/airthings_wave/airthings.py | 70 ++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/custom_components/airthings_wave/airthings.py b/custom_components/airthings_wave/airthings.py index 98ca30f..b2588d4 100644 --- a/custom_components/airthings_wave/airthings.py +++ b/custom_components/airthings_wave/airthings.py @@ -12,6 +12,7 @@ _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) 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') @@ -25,6 +26,7 @@ CHAR_UUID_WAVE_PLUS_DATA = UUID('b42e2a68-ade7-11e4-89d3-123b93f75cba') CHAR_UUID_WAVE_2_DATA = UUID('b42e4dcc-ade7-11e4-89d3-123b93f75cba') CHAR_UUID_WAVEMINI_DATA = UUID('b42e3b98-ade7-11e4-89d3-123b93f75cba') +COMMAND_UUID = UUID('b42e2d06-ade7-11e4-89d3-123b93f75cba') # "Access Control Point" Characteristic Characteristic = namedtuple('Characteristic', ['uuid', 'name', 'format']) @@ -48,7 +50,8 @@ def __str__(self): sensors_characteristics_uuid = [CHAR_UUID_DATETIME, CHAR_UUID_TEMPERATURE, CHAR_UUID_HUMIDITY, CHAR_UUID_RADON_1DAYAVG, CHAR_UUID_RADON_LONG_TERM_AVG, CHAR_UUID_ILLUMINANCE_ACCELEROMETER, - CHAR_UUID_WAVE_PLUS_DATA,CHAR_UUID_WAVE_2_DATA,CHAR_UUID_WAVEMINI_DATA] + CHAR_UUID_WAVE_PLUS_DATA,CHAR_UUID_WAVE_2_DATA,CHAR_UUID_WAVEMINI_DATA, + COMMAND_UUID] sensors_characteristics_uuid_str = [str(x) for x in sensors_characteristics_uuid] @@ -127,6 +130,46 @@ def decode_data(self, raw_data): return data +class CommandDecode: + def __init__(self, name, format_type, cmd): + self.name = name + self.format_type = format_type + self.cmd = cmd + + def decode_data(self, raw_data): + cmd = raw_data[0:1] + if cmd != self.cmd: + _LOGGER.warning("Result for Wrong command received, expected {} got {}".format(self.cmd.hex(), cmd.hex())) + return {} + + val = struct.unpack( + self.format_type, + raw_data[2:]) + res = {} + res['ambientlight'] = val[2] + res['measurement_periods'] = val[5] + res['voltage'] = val[17] / 1000.0 + + V_MAX=3.2 + V_MIN=2.2 + res['battery']= max(0, min(100, round( (res['voltage']-V_MIN)/(V_MAX-V_MIN)*100))) + + 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), @@ -137,6 +180,8 @@ def decode_data(self, raw_data): str(CHAR_UUID_WAVE_2_DATA):Wave2Decode(name="Wave2", format_type='<4B8H', scale=1.0), str(CHAR_UUID_WAVEMINI_DATA):WaveMiniDecode(name="WaveMini", format_type=' Date: Sun, 30 Jan 2022 19:54:26 +0100 Subject: [PATCH 3/9] Commented out ambient light and measurement periods, don't want them for now... --- custom_components/airthings_wave/airthings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/custom_components/airthings_wave/airthings.py b/custom_components/airthings_wave/airthings.py index b2588d4..e4a69ca 100644 --- a/custom_components/airthings_wave/airthings.py +++ b/custom_components/airthings_wave/airthings.py @@ -146,8 +146,8 @@ def decode_data(self, raw_data): self.format_type, raw_data[2:]) res = {} - res['ambientlight'] = val[2] - res['measurement_periods'] = val[5] + #res['ambientlight'] = val[2] + #res['measurement_periods'] = val[5] res['voltage'] = val[17] / 1000.0 V_MAX=3.2 From c72440af60db85615d9b09f7c8884e02dd431c25 Mon Sep 17 00:00:00 2001 From: sverre Date: Sun, 30 Jan 2022 19:54:55 +0100 Subject: [PATCH 4/9] Adds battery and voltage sensors #70 --- custom_components/airthings_wave/sensor.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/custom_components/airthings_wave/sensor.py b/custom_components/airthings_wave/sensor.py index a35ec95..25dca4c 100644 --- a/custom_components/airthings_wave/sensor.py +++ b/custom_components/airthings_wave/sensor.py @@ -36,6 +36,8 @@ DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TIMESTAMP, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_VOLTAGE, EVENT_HOMEASSISTANT_STOP, ILLUMINANCE, STATE_UNKNOWN) @@ -149,6 +151,8 @@ def get_extra_attributes(self, data): DEVICE_SENSOR_SPECIFICS = { "date_time":Sensor('time', None, None, None), + "battery":Sensor(PERCENT, None, DEVICE_CLASS_BATTERY, 'mdi:battery'), + "voltage":Sensor('V', None, DEVICE_CLASS_VOLTAGE, None), "temperature":Sensor(TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, None), "humidity": Sensor(PERCENT, None, DEVICE_CLASS_HUMIDITY, None), "rel_atm_pressure": PressureSensor(ATM_METRIC_UNITS, None, DEVICE_CLASS_PRESSURE, None), From 4528f5818c3bb406b343cb886c13757c7f731dbe Mon Sep 17 00:00:00 2001 From: sverre Date: Sun, 30 Jan 2022 19:55:50 +0100 Subject: [PATCH 5/9] Updates version, two new sensors added voltage and battery (battery derived from voltage) #70 --- custom_components/airthings_wave/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/airthings_wave/manifest.json b/custom_components/airthings_wave/manifest.json index b4fd173..6aee1c3 100644 --- a/custom_components/airthings_wave/manifest.json +++ b/custom_components/airthings_wave/manifest.json @@ -1,7 +1,7 @@ { "domain": "airthings_wave", "name": "Airthings Wave", - "version": "3.0.5", + "version": "3.1.0", "documentation": "https://github.com/custom-components/sensor.airthings_wave/", "issue_tracker": "https://github.com/custom-components/sensor.airthings_wave/issues", "dependencies": [], From 6dbba9667b514f42646ccdde62a95cec304cfb91 Mon Sep 17 00:00:00 2001 From: sverre Date: Thu, 3 Feb 2022 22:19:50 +0100 Subject: [PATCH 6/9] Adds some fault handling to reject bad data. --- custom_components/airthings_wave/airthings.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/custom_components/airthings_wave/airthings.py b/custom_components/airthings_wave/airthings.py index e4a69ca..7694418 100644 --- a/custom_components/airthings_wave/airthings.py +++ b/custom_components/airthings_wave/airthings.py @@ -136,12 +136,17 @@ def __init__(self, name, format_type, cmd): self.format_type = format_type self.cmd = cmd - def decode_data(self, raw_data): + def decode_data(self, raw_data): + if raw_data is None: + return {} cmd = raw_data[0:1] if cmd != self.cmd: _LOGGER.warning("Result for Wrong command received, expected {} got {}".format(self.cmd.hex(), cmd.hex())) return {} - + + if len(raw_data[2:]) != struct.calcsize(self.format_type): + _LOGGER.debug("Wrong length data received ({}) verses expected ({})".format(len(cmd), struct.calcsize(self.format_type))) + return {} val = struct.unpack( self.format_type, raw_data[2:]) From 3cf03129050631eafdb0a48948b9636b9cfa7b85 Mon Sep 17 00:00:00 2001 From: sverre Date: Fri, 4 Feb 2022 21:44:20 +0100 Subject: [PATCH 7/9] Removes the battery calculations from the backend and only return the voltage from the device. Adds a battery sensor that converts voltage to battery percentage, linear calculation from min to max values that are user configurable. Main data from the sensor will be battery percentage but voltage will be returned as an attribute. Adds description to the readme. #70 --- README.md | 8 +++++ custom_components/airthings_wave/airthings.py | 6 +--- custom_components/airthings_wave/sensor.py | 31 +++++++++++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 6f80a98..387d3d6 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ sensor: - platform: airthings_wave scan_interval: 120 elevation: 998 + voltage_0: 2.2 ``` ### Optional Configuration Variables @@ -58,6 +59,11 @@ sensor: (float)(Optional) The current elevation in meters. Used to correct the pressure sensor to sea level conditions. +**voltage_100** + (float)(Optional) The voltage for 100% battery, calculated linearly between voltage_0 and voltage_100 (on supported device), default is 3.2 + +**voltage_0** + (float)(Optional) The voltage for 0% battery, calculated linearly between voltage_0 and voltage_100 (on supported device), default is 2.2 ## Limitations @@ -71,6 +77,8 @@ detector. It might be beneficial to install the latest firmware on the device using the official app first. +Battery level only works for the Airthings wave pluss device. + ## Known Issues * Not yet able to specify the `monitored_conditions` configuration diff --git a/custom_components/airthings_wave/airthings.py b/custom_components/airthings_wave/airthings.py index 7694418..461a032 100644 --- a/custom_components/airthings_wave/airthings.py +++ b/custom_components/airthings_wave/airthings.py @@ -153,11 +153,7 @@ def decode_data(self, raw_data): res = {} #res['ambientlight'] = val[2] #res['measurement_periods'] = val[5] - res['voltage'] = val[17] / 1000.0 - - V_MAX=3.2 - V_MIN=2.2 - res['battery']= max(0, min(100, round( (res['voltage']-V_MIN)/(V_MAX-V_MIN)*100))) + res['battery'] = val[17] / 1000.0 return res diff --git a/custom_components/airthings_wave/sensor.py b/custom_components/airthings_wave/sensor.py index 25dca4c..6785e06 100644 --- a/custom_components/airthings_wave/sensor.py +++ b/custom_components/airthings_wave/sensor.py @@ -37,6 +37,7 @@ DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TIMESTAMP, DEVICE_CLASS_BATTERY, + ATTR_VOLTAGE, DEVICE_CLASS_VOLTAGE, EVENT_HOMEASSISTANT_STOP, ILLUMINANCE, STATE_UNKNOWN) @@ -86,10 +87,15 @@ DOMAIN = 'airthings' +CONF_VOLTAGE_100 = "voltage_100" +CONF_VOLTAGE_0 = "voltage_0" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Optional(CONF_MAC, default=''): cv.string, vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, - vol.Optional(CONF_ELEVATION, default=0): vol.Any(vol.Coerce(float), None) + vol.Optional(CONF_ELEVATION, default=0): vol.Any(vol.Coerce(float), None), + vol.Optional(CONF_VOLTAGE_100, default=3.2): vol.Any(vol.Coerce(float), None), + vol.Optional(CONF_VOLTAGE_0, default=2.2): vol.Any(vol.Coerce(float), None), }) @@ -150,9 +156,24 @@ def get_extra_attributes(self, data): return {ATTR_RADON_LEVEL: radon_level} +class BatterySensor(Sensor): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.voltage = 0.0 + + def transform(self, value): + self.voltage = value + V_MAX=self.parameters[CONF_VOLTAGE_100] #3.2 + V_MIN=self.parameters[CONF_VOLTAGE_0] #2.4 + battery_level = max(0, min(100, round( (value-V_MIN)/(V_MAX-V_MIN)*100))) + return battery_level + + def get_extra_attributes(self, data): + return {ATTR_VOLTAGE: self.voltage} + + DEVICE_SENSOR_SPECIFICS = { "date_time":Sensor('time', None, None, None), - "battery":Sensor(PERCENT, None, DEVICE_CLASS_BATTERY, 'mdi:battery'), - "voltage":Sensor('V', None, DEVICE_CLASS_VOLTAGE, None), + "battery":BatterySensor(PERCENT, None, DEVICE_CLASS_BATTERY, 'mdi:battery'), "temperature":Sensor(TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, None), "humidity": Sensor(PERCENT, None, DEVICE_CLASS_HUMIDITY, None), "rel_atm_pressure": PressureSensor(ATM_METRIC_UNITS, None, DEVICE_CLASS_PRESSURE, None), @@ -175,6 +196,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None): DEVICE_SENSOR_SPECIFICS["rel_atm_pressure"].set_parameters( {'elevation': elevation}) + DEVICE_SENSOR_SPECIFICS["battery"].set_parameters( + {CONF_VOLTAGE_100: config.get(CONF_VOLTAGE_100), + CONF_VOLTAGE_0: config.get(CONF_VOLTAGE_0)}) + if not hass.config.units.is_metric: DEVICE_SENSOR_SPECIFICS["radon_1day_avg"].set_unit_scale(VOLUME_PICOCURIE, BQ_TO_PCI_MULTIPLIER) DEVICE_SENSOR_SPECIFICS["radon_longterm_avg"].set_unit_scale(VOLUME_PICOCURIE, BQ_TO_PCI_MULTIPLIER) From e58c0f785b7c52ff0ea170c748e4707be4202ca1 Mon Sep 17 00:00:00 2001 From: sverre Date: Sat, 5 Feb 2022 20:10:40 +0100 Subject: [PATCH 8/9] Adds the illuminance sensor data, do not know if this is correct, seems to respond to light changes in the room. --- custom_components/airthings_wave/airthings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/custom_components/airthings_wave/airthings.py b/custom_components/airthings_wave/airthings.py index 461a032..693ac9b 100644 --- a/custom_components/airthings_wave/airthings.py +++ b/custom_components/airthings_wave/airthings.py @@ -151,7 +151,7 @@ def decode_data(self, raw_data): self.format_type, raw_data[2:]) res = {} - #res['ambientlight'] = val[2] + res['illuminance'] = val[2] #res['measurement_periods'] = val[5] res['battery'] = val[17] / 1000.0 From 6f4a6be526ad1ab64f2bc6de17ba929248f0b9af Mon Sep 17 00:00:00 2001 From: sverre Date: Sat, 5 Feb 2022 20:11:08 +0100 Subject: [PATCH 9/9] Updates the readme, just cleanup so it looks nice. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 387d3d6..6711ca6 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ sensor: - platform: airthings_wave scan_interval: 120 elevation: 998 + voltage_100: 3.2 voltage_0: 2.2 ``` ### Optional Configuration Variables @@ -60,9 +61,11 @@ sensor: (float)(Optional) The current elevation in meters. Used to correct the pressure sensor to sea level conditions. **voltage_100** + (float)(Optional) The voltage for 100% battery, calculated linearly between voltage_0 and voltage_100 (on supported device), default is 3.2 **voltage_0** + (float)(Optional) The voltage for 0% battery, calculated linearly between voltage_0 and voltage_100 (on supported device), default is 2.2 ## Limitations