Skip to content

Commit 0e8a247

Browse files
committed
Support for battery and illuminance sensors for Airthings Wave+ devices.
1 parent b621dc4 commit 0e8a247

File tree

4 files changed

+82
-4
lines changed

4 files changed

+82
-4
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
## [1.0.0] - 2022-02-13
2+
### New
3+
* Support for battery and illuminance sensors for Airthings Wave+ devices pulled from upstream.
4+
5+
16
## [0.0.4] - 2022-02-04
27
### New
38
* Add support for long-term statistics.
49

510
### Fixes
611
* Updated logging format to show date and time.
712

13+
814
## [0.0.3] - 2021-09-19
915
### Fixes
1016
* Fix issue with addon not restarting when bluetooth adapter is not available on system reboot.

config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: Airthings
2-
version: 0.0.4
2+
version: 1.0.0
33
slug: airthings
44
description: Read sensor values from Airthings Wave environmental monitoring devices
55
url: https://github.com/mjmccans/hassio-addon-airthings

src/airthings-mqtt.ha.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,9 @@
3838
"voc": {"name": "VOC", "device_class": None, "unit_of_measurement": "ppb", "icon": "mdi:cloud", "state_class": "measurement"},
3939
"temperature": {"name": "Temperature", "device_class": "temperature", "unit_of_measurement": "°C", "icon": None, "state_class": "measurement"},
4040
"humidity": {"name": "Humidity", "device_class": "humidity", "unit_of_measurement": "%", "icon": None, "state_class": "measurement"},
41-
"rel_atm_pressure": {"name": "Pressure", "device_class": "pressure", "unit_of_measurement": "mbar", "icon": None, "state_class": "measurement"}
41+
"rel_atm_pressure": {"name": "Pressure", "device_class": "pressure", "unit_of_measurement": "mbar", "icon": None, "state_class": "measurement"},
42+
"illuminance": {"name": "Illuminance", "device_class": "illuminance", "unit_of_measurement": "lx", "icon": None, "state_class": "measurement"},
43+
"battery": {"name": "Battery", "device_class": "battery", "unit_of_measurement": "%", "icon": None, "state_class": "measurement"}
4244
}
4345

4446
class ATSensors:
@@ -288,8 +290,11 @@ def mqtt_publish(msgs):
288290
# Consistent mac formatting
289291
mac = mac.lower()
290292
if isinstance(val, str) == False:
293+
# Edit or format sensor data as needed
291294
if name == "temperature":
292295
val = round(val,1)
296+
elif name == "battery":
297+
val = max(0, min(100, round( (val-2.4)/(3.2-2.4)*100 ))) # Voltage is between 2.4 and 3.2
293298
else:
294299
val = round(val)
295300
_LOGGER.info("{} = {}".format("airthings/"+mac+"/"+name, val))

src/airthings.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
_LOGGER = logging.getLogger(__name__)
3939

4040
# Use full UUID since we do not use UUID from bluepy.btle
41+
CHAR_UUID_CCCD = btle.UUID('2902') # Client Characteristic Configuration Descriptor (CCCD)
4142
CHAR_UUID_MANUFACTURER_NAME = UUID('00002a29-0000-1000-8000-00805f9b34fb')
4243
CHAR_UUID_SERIAL_NUMBER_STRING = UUID('00002a25-0000-1000-8000-00805f9b34fb')
4344
CHAR_UUID_MODEL_NUMBER_STRING = UUID('00002a24-0000-1000-8000-00805f9b34fb')
@@ -51,6 +52,7 @@
5152
CHAR_UUID_WAVE_PLUS_DATA = UUID('b42e2a68-ade7-11e4-89d3-123b93f75cba')
5253
CHAR_UUID_WAVE_2_DATA = UUID('b42e4dcc-ade7-11e4-89d3-123b93f75cba')
5354
CHAR_UUID_WAVEMINI_DATA = UUID('b42e3b98-ade7-11e4-89d3-123b93f75cba')
55+
COMMAND_UUID = UUID('b42e2d06-ade7-11e4-89d3-123b93f75cba') # "Access Control Point" Characteristic
5456

5557
Characteristic = namedtuple('Characteristic', ['uuid', 'name', 'format'])
5658

@@ -74,7 +76,8 @@ def __str__(self):
7476

7577
sensors_characteristics_uuid = [CHAR_UUID_DATETIME, CHAR_UUID_TEMPERATURE, CHAR_UUID_HUMIDITY, CHAR_UUID_RADON_1DAYAVG,
7678
CHAR_UUID_RADON_LONG_TERM_AVG, CHAR_UUID_ILLUMINANCE_ACCELEROMETER,
77-
CHAR_UUID_WAVE_PLUS_DATA,CHAR_UUID_WAVE_2_DATA,CHAR_UUID_WAVEMINI_DATA]
79+
CHAR_UUID_WAVE_PLUS_DATA,CHAR_UUID_WAVE_2_DATA,CHAR_UUID_WAVEMINI_DATA,
80+
COMMAND_UUID]
7881

7982
sensors_characteristics_uuid_str = [str(x) for x in sensors_characteristics_uuid]
8083

@@ -153,6 +156,47 @@ def decode_data(self, raw_data):
153156
return data
154157

155158

159+
class CommandDecode:
160+
def __init__(self, name, format_type, cmd):
161+
self.name = name
162+
self.format_type = format_type
163+
self.cmd = cmd
164+
165+
def decode_data(self, raw_data):
166+
if raw_data is None:
167+
return {}
168+
cmd = raw_data[0:1]
169+
if cmd != self.cmd:
170+
_LOGGER.warning("Result for Wrong command received, expected {} got {}".format(self.cmd.hex(), cmd.hex()))
171+
return {}
172+
173+
if len(raw_data[2:]) != struct.calcsize(self.format_type):
174+
_LOGGER.debug("Wrong length data received ({}) verses expected ({})".format(len(cmd), struct.calcsize(self.format_type)))
175+
return {}
176+
val = struct.unpack(
177+
self.format_type,
178+
raw_data[2:])
179+
res = {}
180+
res['illuminance'] = val[2]
181+
#res['measurement_periods'] = val[5]
182+
res['battery'] = val[17] / 1000.0
183+
184+
return res
185+
186+
187+
class MyDelegate(btle.DefaultDelegate):
188+
def __init__(self):
189+
btle.DefaultDelegate.__init__(self)
190+
# ... initialise here
191+
self.data = None
192+
193+
def handleNotification(self, cHandle, data):
194+
if self.data is None:
195+
self.data = data
196+
else:
197+
self.data = self.data + data
198+
199+
156200
sensor_decoders = {str(CHAR_UUID_WAVE_PLUS_DATA):WavePlussDecode(name="Pluss", format_type='BBBBHHHHHHHH', scale=0),
157201
str(CHAR_UUID_DATETIME):WaveDecodeDate(name="date_time", format_type='HBBBBB', scale=0),
158202
str(CHAR_UUID_HUMIDITY):BaseDecode(name="humidity", format_type='H', scale=1.0/100.0),
@@ -163,6 +207,8 @@ def decode_data(self, raw_data):
163207
str(CHAR_UUID_WAVE_2_DATA):Wave2Decode(name="Wave2", format_type='<4B8H', scale=1.0),
164208
str(CHAR_UUID_WAVEMINI_DATA):WaveMiniDecode(name="WaveMini", format_type='<HHHHHHLL', scale=1.0),}
165209

210+
command_decoders = {str(COMMAND_UUID):CommandDecode(name="Battery", format_type='<L12B6H', cmd=struct.pack('<B', 0x6d))}
211+
166212

167213
class AirthingsWaveDetect:
168214
def __init__(self, scan_interval, mac=None):
@@ -184,7 +230,7 @@ def _parse_serial_number(self, manufacturer_data):
184230

185231
def find_devices(self, scans=50, timeout=0.1):
186232
# Search for devices, scan for BLE devices scans times for timeout seconds
187-
# Get manufacturer data and try to match match it to airthings ID.
233+
# Get manufacturer data and try to match it to airthings ID.
188234
scanner = btle.Scanner()
189235
for _count in range(scans):
190236
advertisements = scanner.scan(timeout)
@@ -204,6 +250,8 @@ def connect(self, mac, retries=10):
204250
tries += 1
205251
try:
206252
self._dev = btle.Peripheral(mac.lower())
253+
self.delgate = MyDelegate()
254+
self._dev.withDelegate( self.delgate )
207255
break
208256
except Exception as e:
209257
# print(e)
@@ -264,15 +312,34 @@ def get_sensor_data(self):
264312
if self._dev is not None:
265313
try:
266314
for characteristic in characteristics:
315+
sensor_data = None
267316
if str(characteristic.uuid) in sensor_decoders:
268317
char = self._dev.getCharacteristics(uuid=characteristic.uuid)[0]
269318
data = char.read()
270319
sensor_data = sensor_decoders[str(characteristic.uuid)].decode_data(data)
271320
_LOGGER.debug("{} Got sensordata {}".format(mac, sensor_data))
321+
322+
if str(characteristic.uuid) in command_decoders:
323+
self.delgate.data = None # Clear the delegate so it is ready for new data.
324+
char = self._dev.getCharacteristics(uuid=characteristic.uuid)[0]
325+
# Do these steps to get notification to work, I do not know how it works, this link should explain it
326+
# https://devzone.nordicsemi.com/guides/short-range-guides/b/bluetooth-low-energy/posts/ble-characteristics-a-beginners-tutorial
327+
desc, = char.getDescriptors(forUUID=CHAR_UUID_CCCD)
328+
desc.write(struct.pack('<H', 1), True)
329+
char.write(command_decoders[str(characteristic.uuid)].cmd)
330+
for i in range(3):
331+
if self._dev.waitForNotifications(0.1):
332+
_LOGGER.debug("Received notification, total data received len {}".format(len(self.delgate.data)))
333+
334+
sensor_data = command_decoders[str(characteristic.uuid)].decode_data(self.delgate.data)
335+
_LOGGER.debug("{} Got cmddata {}".format(mac, sensor_data))
336+
337+
if sensor_data is not None:
272338
if self.sensordata.get(mac) is None:
273339
self.sensordata[mac] = sensor_data
274340
else:
275341
self.sensordata[mac].update(sensor_data)
342+
276343
except btle.BTLEDisconnectError:
277344
_LOGGER.exception("Disconnected")
278345
self._dev = None

0 commit comments

Comments
 (0)