├── LICENSE ├── README.md ├── read_waveplus.py └── renovate.json /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Airthings 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airthings Wave Plus Sensor Reader 2 | 3 | This is a project to provide users an interface (```read_waveplus.py```) to read current sensor values from the 4 | [Airthings Wave Plus](https://airthings.com/wave-plus/) devices using a Raspberry Pi 3 5 | Model B over Bluetooth Low Energy (BLE). 6 | 7 | Airthings Wave Plus is a smart IAQ monitor with Radon detection, including sensors for 8 | temperature, air pressure, humidity, TVOCs and CO2. 9 | 10 | **Table of contents** 11 | 12 | - [Airthings Wave Plus Sensor Reader](#airthings-wave-plus-sensor-reader) 13 | - [Requirements](#requirements) 14 | - [Setup Raspberry Pi](#setup-raspberry-pi) 15 | - [Turn on the BLE interface](#turn-on-the-ble-interface) 16 | - [Installing linux and python packages](#installing-linux-and-python-packages) 17 | - [Downloading script](#downloading-script) 18 | - [Usage](#usage) 19 | - [Printing data to the terminal window](#printing-data-to-the-terminal-window) 20 | - [Piping data to a text-file](#piping-data-to-a-text-file) 21 | - [Sensor data description](#sensor-data-description) 22 | - [Contribution](#contribution) 23 | - [Release notes](#release-notes) 24 | 25 | # Requirements 26 | 27 | The following tables shows a compact overview of dependencies for this project. 28 | 29 | **List of OS dependencies** 30 | 31 | | OS | Device/model/version | Comments | 32 | |-------------|-------------|-------------| 33 | | Raspbian | Raspberry Pi 3 Model B | Used in this project. 34 | | Linux | x86 Debian | Should work according to [bluepy](https://github.com/IanHarvey/bluepy) 35 | 36 | **List of linux/raspberry dependencies** 37 | 38 | | package | version | Comments | 39 | |-------------|-------------|-------------| 40 | | python | 2.7 | Tested with python 2.7.13 41 | | python-pip | | pip for python2.7 42 | | git | | To download this project 43 | | libglib2.0-dev | | For bluepy module 44 | 45 | **List of Python dependencies** 46 | 47 | | module | version | Comments | 48 | |-------------|-------------|-------------| 49 | | bluepy | 1.2.0 | Newer versions have not been tested. 50 | | tableprint | 0.8.0 | Newer versions have not been tested. 51 | 52 | ## Setup Raspberry Pi 53 | 54 | The first step is to setup the Raspberry Pi with Raspbian. An installation guide for 55 | Raspbian can be found on the [Raspberry Pi website](https://www.raspberrypi.org/downloads/raspbian/). 56 | In short: download the Raspbian image and write it to a micro SD card. 57 | 58 | To continue, you need access to the Raspberry Pi using either a monitor and keyboard, or 59 | by connecting through WiFi or ethernet from another computer. The latter option does not 60 | require an external screen or keyboard and is called “headless” setup. To access a headless 61 | setup, you must first activate SSH on the Pi. This can be done by creating a file named ssh 62 | in the boot partition of the SD card. Connect to the Pi using SSH from a command line 63 | interface (terminal): 64 | 65 | ``` 66 | $ ssh pi@raspberrypi.local 67 | ``` 68 | 69 | The default password for the “pi” user is “raspberry”. 70 | 71 | ## Turn on the BLE interface 72 | 73 | In the terminal window on your Raspberry Pi: 74 | 75 | ``` 76 | pi@raspberrypi:~$ bluetoothctl 77 | [bluetooth]# power on 78 | [bluetooth]# show 79 | ``` 80 | 81 | After issuing the command ```show```, a list of bluetooth settings will be printed 82 | to the Raspberry Pi terminal window. Look for ```Powered: yes```. 83 | 84 | ## Installing linux and python packages 85 | 86 | > **Note:** The ```read_waveplus.py``` script is only compatible with Python2.7. 87 | 88 | The next step is to install the bluepy Python library for talking to the BLE stack. 89 | For the current released version for Python 2.7: 90 | 91 | ``` 92 | pi@raspberrypi:~$ sudo apt-get install python-pip libglib2.0-dev 93 | pi@raspberrypi:~$ sudo pip2 install bluepy==1.2.0 94 | ``` 95 | 96 | Make sure your Raspberry Pi has git installed 97 | 98 | ``` 99 | pi@raspberrypi:~$ git --version 100 | ``` 101 | 102 | or install git to be able to clone this repo. 103 | 104 | ``` 105 | pi@raspberrypi:~$ sudo apt-get install git 106 | ``` 107 | 108 | Additionally, the ```read_waveplus.py``` script depends on the ```tableprint``` module 109 | to print nicely formated sensor data to the Raspberry Pi terminal at run-time. 110 | 111 | ``` 112 | pi@raspberrypi:~$ sudo pip2 install tableprint==0.8.0 113 | ``` 114 | 115 | > **Note:** The ```read_waveplus.py``` script has been tested with bluepy==1.2.0 and tableprint==0.8.0. You may download the latest versions at your own risk. 116 | 117 | ## Downloading script 118 | 119 | Downloading using git: 120 | 121 | ``` 122 | pi@raspberrypi:~$ sudo git clone https://github.com/Airthings/waveplus-reader.git 123 | ``` 124 | 125 | Downloading using wget: 126 | 127 | ``` 128 | pi@raspberrypi:~$ wget https://raw.githubusercontent.com/Airthings/waveplus-reader/master/read_waveplus.py 129 | ``` 130 | 131 | # Usage 132 | 133 | To read the sensor data from the Airthings Wave Plus using the ```read_waveplus.py``` script, 134 | you need the 10-digit serial number of the device. This can be found under the magnetic backplate 135 | of your Airthings Wave Plus. 136 | 137 | If your device is paired and connected to e.g. a phone, you may need to turn off bluetooth on 138 | your phone while using this script. 139 | 140 | ```cd``` into the directory where the ```read_waveplus.py``` script is located if you cloned the repo. 141 | 142 | The general format for calling the ```read_waveplus.py``` script is as follows: 143 | 144 | ``` 145 | read_waveplus.py SN SAMPLE-PERIOD [pipe > yourfilename.txt] 146 | ``` 147 | 148 | where the input arguments are: 149 | 150 | | input argument | example | Comments | 151 | |-------------|-------------|-------------| 152 | | SN | 0123456789 | 10-digit number. Can be found under the magnetic backplate of your Airthings Wave Plus. 153 | | SAMPLE-PERIOD | 60 | Read sensor values every 60 seconds. Must be larger than zero. 154 | | pipe | pipe > yourfilename.txt | Optional. Since tableprint is incompatible with piping, we use a third optional input argument "pipe". 155 | 156 | > **Note on choosing a sample period:** 157 | Except for the radon measurements, the Wave Plus updates its current sensor values once every 5 minutes. 158 | Radon measurements are updated once every hour. 159 | 160 | ## Printing data to the terminal window 161 | 162 | By default, the ```read_waveplus.py``` script will print the current sensor values to the Rasberry Pi terminal. 163 | Run the Python script in the following way: 164 | 165 | ``` 166 | pi@raspberrypi:~/waveplus-reader $ sudo python2 read_waveplus.py SN SAMPLE-PERIOD 167 | ``` 168 | 169 | where you change ```SN``` with the 10-digit serial number, and change ```SAMPLE-PERIOD``` to a numerical value of your choice. 170 | 171 | After a short delay, the script will print the current sensor values to the 172 | Raspberry Pi terminal window. Exit the script using ```Ctrl+C```. 173 | 174 | ## Piping data to a text-file 175 | 176 | If you want to pipe the results to a text-file, you can run the script in the following way: 177 | 178 | ``` 179 | pi@raspberrypi:~/waveplus-reader $ sudo python2 read_waveplus.py SN SAMPLE-PERIOD pipe > yourfilename.txt 180 | ``` 181 | 182 | where you change ```SN``` with the 10-digit serial number, and change ```SAMPLE-PERIOD``` to a numerical value of your choice. 183 | 184 | Exit the script using ```Ctrl+C```. 185 | 186 | # Sensor data description 187 | 188 | | sensor | units | Comments | 189 | |-------------|-------------|-------------| 190 | | Humidity | %rH | 191 | | Temperature | °C | 192 | | Radon short term average | Bq/m3 | First measurement available 1 hour after inserting batteries 193 | | Radon long term average | Bq/m3 | First measurement available 1 hour after inserting batteries 194 | | Relative atmospheric pressure | hPa | 195 | | CO2 level | ppm | 196 | | TVOC level | ppb | Total volatile organic compounds level 197 | 198 | # Contribution 199 | 200 | Let us know how it went! If you want contribute, you can do so by posting issues or suggest enhancement 201 | [here](https://github.com/Airthings/waveplus-reader/issues), or you can open a pull request for review 202 | [here](https://github.com/Airthings/waveplus-reader/pulls). 203 | 204 | # Release notes 205 | 206 | Release dated 04-Dec-2020 207 | 208 | * [bug] Fixed missing little-endian specifier. 209 | 210 | Release dated 14-Jan-2019 211 | 212 | * [bug] Fixed issue ([#4][i4]) 213 | 214 | Release dated 14-Dec-2018 215 | 216 | * Added SAMPLE-PERIOD as an input argument. 217 | 218 | Initial release 12-Dec-2018 219 | 220 | [i4]: https://github.com/Airthings/waveplus-reader/issues/4 -------------------------------------------------------------------------------- /read_waveplus.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2018 Airthings AS 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | # 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | # 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | # 23 | # https://airthings.com 24 | 25 | # =============================== 26 | # Module import dependencies 27 | # =============================== 28 | 29 | from bluepy.btle import UUID, Peripheral, Scanner, DefaultDelegate 30 | import sys 31 | import time 32 | import struct 33 | import tableprint 34 | 35 | # =============================== 36 | # Script guards for correct usage 37 | # =============================== 38 | 39 | if len(sys.argv) < 3: 40 | print "ERROR: Missing input argument SN or SAMPLE-PERIOD." 41 | print "USAGE: read_waveplus.py SN SAMPLE-PERIOD [pipe > yourfile.txt]" 42 | print " where SN is the 10-digit serial number found under the magnetic backplate of your Wave Plus." 43 | print " where SAMPLE-PERIOD is the time in seconds between reading the current values." 44 | print " where [pipe > yourfile.txt] is optional and specifies that you want to pipe your results to yourfile.txt." 45 | sys.exit(1) 46 | 47 | if sys.argv[1].isdigit() is not True or len(sys.argv[1]) != 10: 48 | print "ERROR: Invalid SN format." 49 | print "USAGE: read_waveplus.py SN SAMPLE-PERIOD [pipe > yourfile.txt]" 50 | print " where SN is the 10-digit serial number found under the magnetic backplate of your Wave Plus." 51 | print " where SAMPLE-PERIOD is the time in seconds between reading the current values." 52 | print " where [pipe > yourfile.txt] is optional and specifies that you want to pipe your results to yourfile.txt." 53 | sys.exit(1) 54 | 55 | if sys.argv[2].isdigit() is not True or int(sys.argv[2])<0: 56 | print "ERROR: Invalid SAMPLE-PERIOD. Must be a numerical value larger than zero." 57 | print "USAGE: read_waveplus.py SN SAMPLE-PERIOD [pipe > yourfile.txt]" 58 | print " where SN is the 10-digit serial number found under the magnetic backplate of your Wave Plus." 59 | print " where SAMPLE-PERIOD is the time in seconds between reading the current values." 60 | print " where [pipe > yourfile.txt] is optional and specifies that you want to pipe your results to yourfile.txt." 61 | sys.exit(1) 62 | 63 | if len(sys.argv) > 3: 64 | Mode = sys.argv[3].lower() 65 | else: 66 | Mode = 'terminal' # (default) print to terminal 67 | 68 | if Mode!='pipe' and Mode!='terminal': 69 | print "ERROR: Invalid piping method." 70 | print "USAGE: read_waveplus.py SN SAMPLE-PERIOD [pipe > yourfile.txt]" 71 | print " where SN is the 10-digit serial number found under the magnetic backplate of your Wave Plus." 72 | print " where SAMPLE-PERIOD is the time in seconds between reading the current values." 73 | print " where [pipe > yourfile.txt] is optional and specifies that you want to pipe your results to yourfile.txt." 74 | sys.exit(1) 75 | 76 | SerialNumber = int(sys.argv[1]) 77 | SamplePeriod = int(sys.argv[2]) 78 | 79 | # ==================================== 80 | # Utility functions for WavePlus class 81 | # ==================================== 82 | 83 | def parseSerialNumber(ManuDataHexStr): 84 | if (ManuDataHexStr == None or ManuDataHexStr == "None"): 85 | SN = "Unknown" 86 | else: 87 | ManuData = bytearray.fromhex(ManuDataHexStr) 88 | 89 | if (((ManuData[1] << 8) | ManuData[0]) == 0x0334): 90 | SN = ManuData[2] 91 | SN |= (ManuData[3] << 8) 92 | SN |= (ManuData[4] << 16) 93 | SN |= (ManuData[5] << 24) 94 | else: 95 | SN = "Unknown" 96 | return SN 97 | 98 | # =============================== 99 | # Class WavePlus 100 | # =============================== 101 | 102 | class WavePlus(): 103 | 104 | 105 | 106 | def __init__(self, SerialNumber): 107 | self.periph = None 108 | self.curr_val_char = None 109 | self.MacAddr = None 110 | self.SN = SerialNumber 111 | self.uuid = UUID("b42e2a68-ade7-11e4-89d3-123b93f75cba") 112 | 113 | def connect(self): 114 | # Auto-discover device on first connection 115 | if (self.MacAddr is None): 116 | scanner = Scanner().withDelegate(DefaultDelegate()) 117 | searchCount = 0 118 | while self.MacAddr is None and searchCount < 50: 119 | devices = scanner.scan(0.1) # 0.1 seconds scan period 120 | searchCount += 1 121 | for dev in devices: 122 | ManuData = dev.getValueText(255) 123 | SN = parseSerialNumber(ManuData) 124 | if (SN == self.SN): 125 | self.MacAddr = dev.addr # exits the while loop on next conditional check 126 | break # exit for loop 127 | 128 | if (self.MacAddr is None): 129 | print "ERROR: Could not find device." 130 | print "GUIDE: (1) Please verify the serial number." 131 | print " (2) Ensure that the device is advertising." 132 | print " (3) Retry connection." 133 | sys.exit(1) 134 | 135 | # Connect to device 136 | if (self.periph is None): 137 | self.periph = Peripheral(self.MacAddr) 138 | if (self.curr_val_char is None): 139 | self.curr_val_char = self.periph.getCharacteristics(uuid=self.uuid)[0] 140 | 141 | def read(self): 142 | if (self.curr_val_char is None): 143 | print "ERROR: Devices are not connected." 144 | sys.exit(1) 145 | rawdata = self.curr_val_char.read() 146 | rawdata = struct.unpack('