Skip to content

Commit e9175d9

Browse files
authored
Merge pull request #74 from sverrham/master
Add battery level support to wave pluss devices.
2 parents 0d5abd7 + 2bb5833 commit e9175d9

File tree

4 files changed

+114
-8
lines changed

4 files changed

+114
-8
lines changed

README.md

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ sensor:
4343
- platform: airthings_wave
4444
scan_interval: 120
4545
elevation: 998
46+
voltage_100: 3.2
47+
voltage_0: 2.2
4648
```
4749
### Optional Configuration Variables
4850
@@ -58,20 +60,28 @@ sensor:
5860
5961
(float)(Optional) The current elevation in meters. Used to correct the pressure sensor to sea level conditions.
6062
63+
**voltage_100**
64+
65+
(float)(Optional) The voltage for 100% battery, calculated linearly between voltage_0 and voltage_100 (on supported device), default is 3.2
66+
67+
**voltage_0**
68+
69+
(float)(Optional) The voltage for 0% battery, calculated linearly between voltage_0 and voltage_100 (on supported device), default is 2.2
6170
6271
## Limitations
6372
64-
It may be possible that the Wave must be connected to the official app at least
65-
once before you can use this program, so you will probably not get around
66-
registering an account with Airthings.
73+
Users has reported that it is possible to get data without first registering with the official app,
74+
so it should be possible to use the sensor with this integration without registering.
6775
6876
The radon level history stored on the Wave itself cannot be accessed
6977
with this component. To get around this, it connects regularly to the radon
7078
detector.
7179
72-
Make sure you install the latest firmware on the device using the official app
80+
It might be beneficial to install the latest firmware on the device using the official app
7381
first.
7482
83+
Battery level only works for the Airthings wave pluss device.
84+
7585
## Known Issues
7686
7787
* Not yet able to specify the `monitored_conditions` configuration

custom_components/airthings_wave/airthings.py

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

1414
# Use full UUID since we do not use UUID from bluepy.btle
15+
CHAR_UUID_CCCD = btle.UUID('2902') # Client Characteristic Configuration Descriptor (CCCD)
1516
CHAR_UUID_MANUFACTURER_NAME = UUID('00002a29-0000-1000-8000-00805f9b34fb')
1617
CHAR_UUID_SERIAL_NUMBER_STRING = UUID('00002a25-0000-1000-8000-00805f9b34fb')
1718
CHAR_UUID_MODEL_NUMBER_STRING = UUID('00002a24-0000-1000-8000-00805f9b34fb')
@@ -25,6 +26,7 @@
2526
CHAR_UUID_WAVE_PLUS_DATA = UUID('b42e2a68-ade7-11e4-89d3-123b93f75cba')
2627
CHAR_UUID_WAVE_2_DATA = UUID('b42e4dcc-ade7-11e4-89d3-123b93f75cba')
2728
CHAR_UUID_WAVEMINI_DATA = UUID('b42e3b98-ade7-11e4-89d3-123b93f75cba')
29+
COMMAND_UUID = UUID('b42e2d06-ade7-11e4-89d3-123b93f75cba') # "Access Control Point" Characteristic
2830

2931
Characteristic = namedtuple('Characteristic', ['uuid', 'name', 'format'])
3032

@@ -48,7 +50,8 @@ def __str__(self):
4850

4951
sensors_characteristics_uuid = [CHAR_UUID_DATETIME, CHAR_UUID_TEMPERATURE, CHAR_UUID_HUMIDITY, CHAR_UUID_RADON_1DAYAVG,
5052
CHAR_UUID_RADON_LONG_TERM_AVG, CHAR_UUID_ILLUMINANCE_ACCELEROMETER,
51-
CHAR_UUID_WAVE_PLUS_DATA,CHAR_UUID_WAVE_2_DATA,CHAR_UUID_WAVEMINI_DATA]
53+
CHAR_UUID_WAVE_PLUS_DATA,CHAR_UUID_WAVE_2_DATA,CHAR_UUID_WAVEMINI_DATA,
54+
COMMAND_UUID]
5255

5356
sensors_characteristics_uuid_str = [str(x) for x in sensors_characteristics_uuid]
5457

@@ -127,6 +130,47 @@ def decode_data(self, raw_data):
127130
return data
128131

129132

133+
class CommandDecode:
134+
def __init__(self, name, format_type, cmd):
135+
self.name = name
136+
self.format_type = format_type
137+
self.cmd = cmd
138+
139+
def decode_data(self, raw_data):
140+
if raw_data is None:
141+
return {}
142+
cmd = raw_data[0:1]
143+
if cmd != self.cmd:
144+
_LOGGER.warning("Result for Wrong command received, expected {} got {}".format(self.cmd.hex(), cmd.hex()))
145+
return {}
146+
147+
if len(raw_data[2:]) != struct.calcsize(self.format_type):
148+
_LOGGER.debug("Wrong length data received ({}) verses expected ({})".format(len(cmd), struct.calcsize(self.format_type)))
149+
return {}
150+
val = struct.unpack(
151+
self.format_type,
152+
raw_data[2:])
153+
res = {}
154+
res['illuminance'] = val[2]
155+
#res['measurement_periods'] = val[5]
156+
res['battery'] = val[17] / 1000.0
157+
158+
return res
159+
160+
161+
class MyDelegate(btle.DefaultDelegate):
162+
def __init__(self):
163+
btle.DefaultDelegate.__init__(self)
164+
# ... initialise here
165+
self.data = None
166+
167+
def handleNotification(self, cHandle, data):
168+
if self.data is None:
169+
self.data = data
170+
else:
171+
self.data = self.data + data
172+
173+
130174
sensor_decoders = {str(CHAR_UUID_WAVE_PLUS_DATA):WavePlussDecode(name="Pluss", format_type='BBBBHHHHHHHH', scale=0),
131175
str(CHAR_UUID_DATETIME):WaveDecodeDate(name="date_time", format_type='HBBBBB', scale=0),
132176
str(CHAR_UUID_HUMIDITY):BaseDecode(name="humidity", format_type='H', scale=1.0/100.0),
@@ -137,6 +181,8 @@ def decode_data(self, raw_data):
137181
str(CHAR_UUID_WAVE_2_DATA):Wave2Decode(name="Wave2", format_type='<4B8H', scale=1.0),
138182
str(CHAR_UUID_WAVEMINI_DATA):WaveMiniDecode(name="WaveMini", format_type='<HHHHHHLL', scale=1.0),}
139183

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

141187
class AirthingsWaveDetect:
142188
def __init__(self, scan_interval, mac=None):
@@ -158,7 +204,7 @@ def _parse_serial_number(self, manufacturer_data):
158204

159205
def find_devices(self, scans=50, timeout=0.1):
160206
# Search for devices, scan for BLE devices scans times for timeout seconds
161-
# Get manufacturer data and try to match match it to airthings ID.
207+
# Get manufacturer data and try to match it to airthings ID.
162208
scanner = btle.Scanner()
163209
for _count in range(scans):
164210
advertisements = scanner.scan(timeout)
@@ -178,6 +224,8 @@ def connect(self, mac, retries=10):
178224
tries += 1
179225
try:
180226
self._dev = btle.Peripheral(mac.lower())
227+
self.delgate = MyDelegate()
228+
self._dev.withDelegate( self.delgate )
181229
break
182230
except Exception as e:
183231
print(e)
@@ -238,15 +286,34 @@ def get_sensor_data(self):
238286
if self._dev is not None:
239287
try:
240288
for characteristic in characteristics:
289+
sensor_data = None
241290
if str(characteristic.uuid) in sensor_decoders:
242291
char = self._dev.getCharacteristics(uuid=characteristic.uuid)[0]
243292
data = char.read()
244293
sensor_data = sensor_decoders[str(characteristic.uuid)].decode_data(data)
245294
_LOGGER.debug("{} Got sensordata {}".format(mac, sensor_data))
295+
296+
if str(characteristic.uuid) in command_decoders:
297+
self.delgate.data = None # Clear the delegate so it is ready for new data.
298+
char = self._dev.getCharacteristics(uuid=characteristic.uuid)[0]
299+
# Do these steps to get notification to work, I do not know how it works, this link should explain it
300+
# https://devzone.nordicsemi.com/guides/short-range-guides/b/bluetooth-low-energy/posts/ble-characteristics-a-beginners-tutorial
301+
desc, = char.getDescriptors(forUUID=CHAR_UUID_CCCD)
302+
desc.write(struct.pack('<H', 1), True)
303+
char.write(command_decoders[str(characteristic.uuid)].cmd)
304+
for i in range(3):
305+
if self._dev.waitForNotifications(0.1):
306+
_LOGGER.debug("Received notification, total data received len {}".format(len(self.delgate.data)))
307+
308+
sensor_data = command_decoders[str(characteristic.uuid)].decode_data(self.delgate.data)
309+
_LOGGER.debug("{} Got cmddata {}".format(mac, sensor_data))
310+
311+
if sensor_data is not None:
246312
if self.sensordata.get(mac) is None:
247313
self.sensordata[mac] = sensor_data
248314
else:
249315
self.sensordata[mac].update(sensor_data)
316+
250317
except btle.BTLEDisconnectError:
251318
_LOGGER.exception("Disconnected")
252319
self._dev = None

custom_components/airthings_wave/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"domain": "airthings_wave",
33
"name": "Airthings Wave",
4-
"version": "3.0.5",
4+
"version": "3.1.0",
55
"documentation": "https://github.com/custom-components/sensor.airthings_wave/",
66
"issue_tracker": "https://github.com/custom-components/sensor.airthings_wave/issues",
77
"dependencies": [],

custom_components/airthings_wave/sensor.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@
3636
DEVICE_CLASS_TEMPERATURE,
3737
DEVICE_CLASS_PRESSURE,
3838
DEVICE_CLASS_TIMESTAMP,
39+
DEVICE_CLASS_BATTERY,
40+
ATTR_VOLTAGE,
41+
DEVICE_CLASS_VOLTAGE,
3942
EVENT_HOMEASSISTANT_STOP, ILLUMINANCE,
4043
STATE_UNKNOWN)
4144

@@ -84,10 +87,15 @@
8487

8588
DOMAIN = 'airthings'
8689

90+
CONF_VOLTAGE_100 = "voltage_100"
91+
CONF_VOLTAGE_0 = "voltage_0"
92+
8793
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
8894
vol.Optional(CONF_MAC, default=''): cv.string,
8995
vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period,
90-
vol.Optional(CONF_ELEVATION, default=0): vol.Any(vol.Coerce(float), None)
96+
vol.Optional(CONF_ELEVATION, default=0): vol.Any(vol.Coerce(float), None),
97+
vol.Optional(CONF_VOLTAGE_100, default=3.2): vol.Any(vol.Coerce(float), None),
98+
vol.Optional(CONF_VOLTAGE_0, default=2.2): vol.Any(vol.Coerce(float), None),
9199
})
92100

93101

@@ -148,7 +156,24 @@ def get_extra_attributes(self, data):
148156
return {ATTR_RADON_LEVEL: radon_level}
149157

150158

159+
class BatterySensor(Sensor):
160+
def __init__(self, *args, **kwargs):
161+
super().__init__(*args, **kwargs)
162+
self.voltage = 0.0
163+
164+
def transform(self, value):
165+
self.voltage = value
166+
V_MAX=self.parameters[CONF_VOLTAGE_100] #3.2
167+
V_MIN=self.parameters[CONF_VOLTAGE_0] #2.4
168+
battery_level = max(0, min(100, round( (value-V_MIN)/(V_MAX-V_MIN)*100)))
169+
return battery_level
170+
171+
def get_extra_attributes(self, data):
172+
return {ATTR_VOLTAGE: self.voltage}
173+
174+
151175
DEVICE_SENSOR_SPECIFICS = { "date_time":Sensor('time', None, None, None),
176+
"battery":BatterySensor(PERCENT, None, DEVICE_CLASS_BATTERY, 'mdi:battery'),
152177
"temperature":Sensor(TEMP_CELSIUS, None, DEVICE_CLASS_TEMPERATURE, None),
153178
"humidity": Sensor(PERCENT, None, DEVICE_CLASS_HUMIDITY, None),
154179
"rel_atm_pressure": PressureSensor(ATM_METRIC_UNITS, None, DEVICE_CLASS_PRESSURE, None),
@@ -171,6 +196,10 @@ def setup_platform(hass, config, add_entities, discovery_info=None):
171196
DEVICE_SENSOR_SPECIFICS["rel_atm_pressure"].set_parameters(
172197
{'elevation': elevation})
173198

199+
DEVICE_SENSOR_SPECIFICS["battery"].set_parameters(
200+
{CONF_VOLTAGE_100: config.get(CONF_VOLTAGE_100),
201+
CONF_VOLTAGE_0: config.get(CONF_VOLTAGE_0)})
202+
174203
if not hass.config.units.is_metric:
175204
DEVICE_SENSOR_SPECIFICS["radon_1day_avg"].set_unit_scale(VOLUME_PICOCURIE, BQ_TO_PCI_MULTIPLIER)
176205
DEVICE_SENSOR_SPECIFICS["radon_longterm_avg"].set_unit_scale(VOLUME_PICOCURIE, BQ_TO_PCI_MULTIPLIER)

0 commit comments

Comments
 (0)