Skip to content

Commit

Permalink
Merge pull request #74 from sverrham/master
Browse files Browse the repository at this point in the history
Add battery level support to wave pluss devices.
  • Loading branch information
sverrham authored Feb 5, 2022
2 parents 0d5abd7 + 2bb5833 commit e9175d9
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 8 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ sensor:
- platform: airthings_wave
scan_interval: 120
elevation: 998
voltage_100: 3.2
voltage_0: 2.2
```
### Optional Configuration Variables
Expand All @@ -58,20 +60,28 @@ 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
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.
Battery level only works for the Airthings wave pluss device.
## Known Issues
* Not yet able to specify the `monitored_conditions` configuration
Expand Down
71 changes: 69 additions & 2 deletions custom_components/airthings_wave/airthings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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'])

Expand All @@ -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]

Expand Down Expand Up @@ -127,6 +130,47 @@ 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):
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:])
res = {}
res['illuminance'] = val[2]
#res['measurement_periods'] = val[5]
res['battery'] = val[17] / 1000.0

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),
Expand All @@ -137,6 +181,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='<HHHHHHLL', scale=1.0),}

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


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

def find_devices(self, scans=50, timeout=0.1):
# Search for devices, scan for BLE devices scans times for timeout seconds
# Get manufacturer data and try to match match it to airthings ID.
# Get manufacturer data and try to match it to airthings ID.
scanner = btle.Scanner()
for _count in range(scans):
advertisements = scanner.scan(timeout)
Expand All @@ -178,6 +224,8 @@ def connect(self, mac, retries=10):
tries += 1
try:
self._dev = btle.Peripheral(mac.lower())
self.delgate = MyDelegate()
self._dev.withDelegate( self.delgate )
break
except Exception as e:
print(e)
Expand Down Expand Up @@ -238,15 +286,34 @@ def get_sensor_data(self):
if self._dev is not None:
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()
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('<H', 1), True)
char.write(command_decoders[str(characteristic.uuid)].cmd)
for i in range(3):
if self._dev.waitForNotifications(0.1):
_LOGGER.debug("Received notification, total data received len {}".format(len(self.delgate.data)))

sensor_data = command_decoders[str(characteristic.uuid)].decode_data(self.delgate.data)
_LOGGER.debug("{} Got cmddata {}".format(mac, sensor_data))

if sensor_data is not None:
if self.sensordata.get(mac) is None:
self.sensordata[mac] = sensor_data
else:
self.sensordata[mac].update(sensor_data)

except btle.BTLEDisconnectError:
_LOGGER.exception("Disconnected")
self._dev = None
Expand Down
2 changes: 1 addition & 1 deletion custom_components/airthings_wave/manifest.json
Original file line number Diff line number Diff line change
@@ -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": [],
Expand Down
31 changes: 30 additions & 1 deletion custom_components/airthings_wave/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TIMESTAMP,
DEVICE_CLASS_BATTERY,
ATTR_VOLTAGE,
DEVICE_CLASS_VOLTAGE,
EVENT_HOMEASSISTANT_STOP, ILLUMINANCE,
STATE_UNKNOWN)

Expand Down Expand Up @@ -84,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),
})


Expand Down Expand Up @@ -148,7 +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":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),
Expand All @@ -171,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)
Expand Down

0 comments on commit e9175d9

Please sign in to comment.