diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ef7284b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/* +__pycache__/* diff --git a/README.md b/README.md index 848c07d..7685675 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ temperature, air pressure, humidity, TVOCs and CO2. * [Usage](#usage) * [Printing data to the terminal window](#printing-data-to-the-terminal-window) * [Piping data to a text-file](#piping-data-to-a-text-file) + * [Posting regularly to MQTT Server](#posting-regularly-to-mqtt-server) * [Sensor data description](#sensor-data-description) * [Contribution](#contribution) * [Release notes](#release-notes) @@ -36,8 +37,8 @@ The following tables shows a compact overview of dependencies for this project. | package | version | Comments | |-------------|-------------|-------------| -| python | 2.7 | Tested with python 2.7.13 -| python-pip | | pip for python2.7 +| python | 2.7 or 3.6 | Tested with python 2.7.13 and 3.6.5 +| python-pip | | pip for python 2.7 or python 3.6 | git | | To download this project | libglib2.0-dev | | For bluepy module @@ -104,11 +105,12 @@ or install git to be able to clone this repo. pi@raspberrypi:~$ sudo apt-get install git ``` -Additionally, the ```read_waveplus.py``` script depends on the ```tableprint``` module -to print nicely formated sensor data to the Raspberry Pi terminal at run-time. +Additionally, the ```read_waveplus.py``` script depends on the ```tableprint``` module to print nicely formated sensor data to the Raspberry Pi terminal at run-time and the ```paho.mqtt``` module for communication with an MQTT server. ``` -pi@raspberrypi:~$ sudo pip2 install tableprint==0.8.0 +pi@raspberrypi:~$ sudo pip install tableprint==0.8.0 +pi@raspberrypi:~$ sudo pip install paho-mqtt + ``` > **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. @@ -182,6 +184,40 @@ where you change ```SN``` with the 10-digit serial number, and change ```SAMPLE- Exit the script using ```Ctrl+C```. +## Posting regularly to MQTT Server + +If you need the data available on an home automation system, you can set up the script to post data read from the Airthings Wave plus to an MQTT server (like mosquitto). Invoking the read_waveplus.py with +``` +pi@raspberrypi:~/waveplus-reader $ sudo python3 read_waveplus_mqtt.py SN SERVER_ADDRESS +``` +will read values from Airthings Wave Plus and post all values on the given mqtt server, assuming there is no login required. +```SN``` should be the serial number of your device and ```SERVER_ADDRESS``` is the ip-address of the mqtt server you want your data posted to. + +The values wil be posted both as a json object containing all values and as individual individual values the following topics: +``` +waveplus/SN/MEASUREMENT_TYPE +``` +```MEASUREMENT_TYPE``` would be modified name of the measurements. Examples of topics would be +``` +waveplus/2930002359/Humidity +waveplus/2930002359/Radon_ST_avg +... +waveplus/2930002359/VOC_level +``` + +Practical setup would be to add a line to your cron table: +``` +pi@raspberrypi:~ $ sudo crontab -e +``` +Note the use of 'sudo': needed for the scripts access to the BTLE device. + +The following line will make a read and post every 5 minutes from your Raspberry Pi to the mqtt server located at ```192.168.0.16```: + +``` +*/5 * * * * sudo python3 /home/pi/waveplus-reader/read_waveplus_mqtt.py 2930002359 192.168.0.16 +``` + + # Sensor data description | sensor | units | Comments | diff --git a/Sensors.py b/Sensors.py new file mode 100644 index 0000000..d4f378f --- /dev/null +++ b/Sensors.py @@ -0,0 +1,51 @@ + +# =================================== +# Class Sensor and sensor definitions +# =================================== + +import sys + +class Sensors(): + + NUMBER_OF_SENSORS = 7 + SENSOR_IDX_HUMIDITY = 0 + SENSOR_IDX_RADON_SHORT_TERM_AVG = 1 + SENSOR_IDX_RADON_LONG_TERM_AVG = 2 + SENSOR_IDX_TEMPERATURE = 3 + SENSOR_IDX_REL_ATM_PRESSURE = 4 + SENSOR_IDX_CO2_LVL = 5 + SENSOR_IDX_VOC_LVL = 6 + + def __init__(self): + self.sensor_version = None + self.sensor_data = [None]*self.NUMBER_OF_SENSORS + self.sensor_units = ["%rH", "Bq/m3", "Bq/m3", "degC", "hPa", "ppm", "ppb"] + self.header = ['Humidity', 'Radon ST avg', 'Radon LT avg', 'Temperature', 'Pressure', 'CO2 level', 'VOC level'] + + + def set(self, rawData): + self.sensor_version = rawData[0] + if (self.sensor_version == 1): + self.sensor_data[self.SENSOR_IDX_HUMIDITY] = rawData[1]/2.0 + self.sensor_data[self.SENSOR_IDX_RADON_SHORT_TERM_AVG] = self.conv2radon(rawData[4]) + self.sensor_data[self.SENSOR_IDX_RADON_LONG_TERM_AVG] = self.conv2radon(rawData[5]) + self.sensor_data[self.SENSOR_IDX_TEMPERATURE] = rawData[6]/100.0 + self.sensor_data[self.SENSOR_IDX_REL_ATM_PRESSURE] = rawData[7]/50.0 + self.sensor_data[self.SENSOR_IDX_CO2_LVL] = rawData[8]*1.0 + self.sensor_data[self.SENSOR_IDX_VOC_LVL] = rawData[9]*1.0 + else: + print ("ERROR: Unknown sensor version.\n") + print ("GUIDE: Contact Airthings for support.\n") + sys.exit(1) + + def conv2radon(self, radon_raw): + radon = "N/A" # Either invalid measurement, or not available + if 0 <= radon_raw <= 16383: + radon = radon_raw + return radon + + def getValue(self, sensor_index): + return self.sensor_data[sensor_index] + + def getUnit(self, sensor_index): + return self.sensor_units[sensor_index] diff --git a/WavePlus.py b/WavePlus.py new file mode 100644 index 0000000..815528c --- /dev/null +++ b/WavePlus.py @@ -0,0 +1,82 @@ +# =============================== +# Class WavePlus +# =============================== + +import sys +import struct +from Sensors import Sensors +from bluepy.btle import UUID, Peripheral, Scanner, DefaultDelegate + +class WavePlus(): + + def __init__(self, SerialNumber): + self.periph = None + self.curr_val_char = None + self.MacAddr = None + self.SN = SerialNumber + self.uuid = UUID("b42e2a68-ade7-11e4-89d3-123b93f75cba") + + def connect(self): + # Auto-discover device on first connection + if (self.MacAddr is None): + scanner = Scanner().withDelegate(DefaultDelegate()) + searchCount = 0 + while self.MacAddr is None and searchCount < 50: + devices = scanner.scan(0.1) # 0.1 seconds scan period + searchCount += 1 + for dev in devices: + ManuData = dev.getValueText(255) + if ManuData != None: + SN = self.parseSerialNumber(ManuData) + if (SN == self.SN): + self.MacAddr = dev.addr # exits the while loop on next conditional check + break # exit for loop + + if (self.MacAddr is None): + print ("ERROR: Could not find device.") + print ("GUIDE: (1) Please verify the serial number.") + print (" (2) Ensure that the device is advertising.") + print (" (3) Retry connection.") + sys.exit(1) + + # Connect to device + if (self.periph is None): + self.periph = Peripheral(self.MacAddr) + if (self.curr_val_char is None): + self.curr_val_char = self.periph.getCharacteristics(uuid=self.uuid)[0] + + def read(self): + if (self.curr_val_char is None): + print ("ERROR: Devices are not connected.") + sys.exit(1) + rawdata = self.curr_val_char.read() + rawdata = struct.unpack('BBBBHHHHHHHH', rawdata) + sensors = Sensors() + sensors.set(rawdata) + return sensors + + def disconnect(self): + if self.periph is not None: + self.periph.disconnect() + self.periph = None + self.curr_val_char = None + + # ==================================== + # Utility functions for WavePlus class + # ==================================== + + def parseSerialNumber(self, ManuDataHexStr): + if (ManuDataHexStr == "None"): + SN = "Unknown" + else: + ManuData = bytearray.fromhex(ManuDataHexStr) + + if (((ManuData[1] << 8) | ManuData[0]) == 0x0334): + SN = ManuData[2] + SN |= (ManuData[3] << 8) + SN |= (ManuData[4] << 16) + SN |= (ManuData[5] << 24) + else: + SN = "Unknown" + return SN + diff --git a/read_waveplus.py b/read_waveplus.py index d7071bc..900644e 100644 --- a/read_waveplus.py +++ b/read_waveplus.py @@ -26,222 +26,96 @@ # Module import dependencies # =============================== -from bluepy.btle import UUID, Peripheral, Scanner, DefaultDelegate import sys import time -import struct import tableprint +from Sensors import Sensors +from WavePlus import WavePlus # =============================== # Script guards for correct usage # =============================== - +def print_usage(): + print ("USAGE: read_waveplus.py SN SAMPLE-PERIOD [pipe > yourfile.txt]") + print (" where SN is the 10-digit serial number found under the magnetic backplate of your Wave Plus.") + print (" where SAMPLE-PERIOD is the time in seconds between reading the current values.") + print (" where [pipe > yourfile.txt] is optional and specifies that you want to pipe your results to yourfile.txt.") + if len(sys.argv) < 3: - print "ERROR: Missing input argument SN or SAMPLE-PERIOD." - print "USAGE: read_waveplus.py SN SAMPLE-PERIOD [pipe > yourfile.txt]" - print " where SN is the 10-digit serial number found under the magnetic backplate of your Wave Plus." - print " where SAMPLE-PERIOD is the time in seconds between reading the current values." - print " where [pipe > yourfile.txt] is optional and specifies that you want to pipe your results to yourfile.txt." + print ("ERROR: Missing input argument SN or SAMPLE-PERIOD.") + print_usage() sys.exit(1) if sys.argv[1].isdigit() is not True or len(sys.argv[1]) != 10: - print "ERROR: Invalid SN format." - print "USAGE: read_waveplus.py SN SAMPLE-PERIOD [pipe > yourfile.txt]" - print " where SN is the 10-digit serial number found under the magnetic backplate of your Wave Plus." - print " where SAMPLE-PERIOD is the time in seconds between reading the current values." - print " where [pipe > yourfile.txt] is optional and specifies that you want to pipe your results to yourfile.txt." + print ("ERROR: Invalid SN format.") + print_usage() sys.exit(1) if sys.argv[2].isdigit() is not True or int(sys.argv[2])<0: - print "ERROR: Invalid SAMPLE-PERIOD. Must be a numerical value larger than zero." - print "USAGE: read_waveplus.py SN SAMPLE-PERIOD [pipe > yourfile.txt]" - print " where SN is the 10-digit serial number found under the magnetic backplate of your Wave Plus." - print " where SAMPLE-PERIOD is the time in seconds between reading the current values." - print " where [pipe > yourfile.txt] is optional and specifies that you want to pipe your results to yourfile.txt." + print ("ERROR: Invalid SAMPLE-PERIOD. Must be a numerical value larger than zero.") + print_usage() sys.exit(1) if len(sys.argv) > 3: Mode = sys.argv[3].lower() + if Mode == 'mqtt': + Broker = sys.argv[4] + else: + Broker = None else: Mode = 'terminal' # (default) print to terminal if Mode!='pipe' and Mode!='terminal': - print "ERROR: Invalid piping method." - print "USAGE: read_waveplus.py SN SAMPLE-PERIOD [pipe > yourfile.txt]" - print " where SN is the 10-digit serial number found under the magnetic backplate of your Wave Plus." - print " where SAMPLE-PERIOD is the time in seconds between reading the current values." - print " where [pipe > yourfile.txt] is optional and specifies that you want to pipe your results to yourfile.txt." + print ("ERROR: Invalid piping method.") + print_usage() sys.exit(1) SerialNumber = int(sys.argv[1]) SamplePeriod = int(sys.argv[2]) -# ==================================== -# Utility functions for WavePlus class -# ==================================== - -def parseSerialNumber(ManuDataHexStr): - if (ManuDataHexStr == "None"): - SN = "Unknown" - else: - ManuData = bytearray.fromhex(ManuDataHexStr) - - if (((ManuData[1] << 8) | ManuData[0]) == 0x0334): - SN = ManuData[2] - SN |= (ManuData[3] << 8) - SN |= (ManuData[4] << 16) - SN |= (ManuData[5] << 24) - else: - SN = "Unknown" - return SN - -# =============================== -# Class WavePlus -# =============================== - -class WavePlus(): - - - - def __init__(self, SerialNumber): - self.periph = None - self.curr_val_char = None - self.MacAddr = None - self.SN = SerialNumber - self.uuid = UUID("b42e2a68-ade7-11e4-89d3-123b93f75cba") - - def connect(self): - # Auto-discover device on first connection - if (self.MacAddr is None): - scanner = Scanner().withDelegate(DefaultDelegate()) - searchCount = 0 - while self.MacAddr is None and searchCount < 50: - devices = scanner.scan(0.1) # 0.1 seconds scan period - searchCount += 1 - for dev in devices: - ManuData = dev.getValueText(255) - SN = parseSerialNumber(ManuData) - if (SN == self.SN): - self.MacAddr = dev.addr # exits the while loop on next conditional check - break # exit for loop - - if (self.MacAddr is None): - print "ERROR: Could not find device." - print "GUIDE: (1) Please verify the serial number." - print " (2) Ensure that the device is advertising." - print " (3) Retry connection." - sys.exit(1) - - # Connect to device - if (self.periph is None): - self.periph = Peripheral(self.MacAddr) - if (self.curr_val_char is None): - self.curr_val_char = self.periph.getCharacteristics(uuid=self.uuid)[0] - - def read(self): - if (self.curr_val_char is None): - print "ERROR: Devices are not connected." - sys.exit(1) - rawdata = self.curr_val_char.read() - rawdata = struct.unpack('BBBBHHHHHHHH', rawdata) - sensors = Sensors() - sensors.set(rawdata) - return sensors - - def disconnect(self): - if self.periph is not None: - self.periph.disconnect() - self.periph = None - self.curr_val_char = None - -# =================================== -# Class Sensor and sensor definitions -# =================================== - -NUMBER_OF_SENSORS = 7 -SENSOR_IDX_HUMIDITY = 0 -SENSOR_IDX_RADON_SHORT_TERM_AVG = 1 -SENSOR_IDX_RADON_LONG_TERM_AVG = 2 -SENSOR_IDX_TEMPERATURE = 3 -SENSOR_IDX_REL_ATM_PRESSURE = 4 -SENSOR_IDX_CO2_LVL = 5 -SENSOR_IDX_VOC_LVL = 6 - -class Sensors(): - def __init__(self): - self.sensor_version = None - self.sensor_data = [None]*NUMBER_OF_SENSORS - self.sensor_units = ["%rH", "Bq/m3", "Bq/m3", "degC", "hPa", "ppm", "ppb"] - - def set(self, rawData): - self.sensor_version = rawData[0] - if (self.sensor_version == 1): - self.sensor_data[SENSOR_IDX_HUMIDITY] = rawData[1]/2.0 - self.sensor_data[SENSOR_IDX_RADON_SHORT_TERM_AVG] = self.conv2radon(rawData[4]) - self.sensor_data[SENSOR_IDX_RADON_LONG_TERM_AVG] = self.conv2radon(rawData[5]) - self.sensor_data[SENSOR_IDX_TEMPERATURE] = rawData[6]/100.0 - self.sensor_data[SENSOR_IDX_REL_ATM_PRESSURE] = rawData[7]/50.0 - self.sensor_data[SENSOR_IDX_CO2_LVL] = rawData[8]*1.0 - self.sensor_data[SENSOR_IDX_VOC_LVL] = rawData[9]*1.0 - else: - print "ERROR: Unknown sensor version.\n" - print "GUIDE: Contact Airthings for support.\n" - sys.exit(1) - - def conv2radon(self, radon_raw): - radon = "N/A" # Either invalid measurement, or not available - if 0 <= radon_raw <= 16383: - radon = radon_raw - return radon - - def getValue(self, sensor_index): - return self.sensor_data[sensor_index] - - def getUnit(self, sensor_index): - return self.sensor_units[sensor_index] try: #---- Initialize ----# waveplus = WavePlus(SerialNumber) if (Mode=='terminal'): - print "\nPress ctrl+C to exit program\n" - - print "Device serial number: %s" %(SerialNumber) + print ("\nPress ctrl+C to exit program\n") header = ['Humidity', 'Radon ST avg', 'Radon LT avg', 'Temperature', 'Pressure', 'CO2 level', 'VOC level'] + print ("Device serial number: %s" %(SerialNumber)) + if (Mode=='terminal'): - print tableprint.header(header, width=12) + print (tableprint.header(header, width=12)) elif (Mode=='pipe'): - print header - + print (header) + + while True: - + sensors = None waveplus.connect() # read values sensors = waveplus.read() - + # extract - humidity = str(sensors.getValue(SENSOR_IDX_HUMIDITY)) + " " + str(sensors.getUnit(SENSOR_IDX_HUMIDITY)) - radon_st_avg = str(sensors.getValue(SENSOR_IDX_RADON_SHORT_TERM_AVG)) + " " + str(sensors.getUnit(SENSOR_IDX_RADON_SHORT_TERM_AVG)) - radon_lt_avg = str(sensors.getValue(SENSOR_IDX_RADON_LONG_TERM_AVG)) + " " + str(sensors.getUnit(SENSOR_IDX_RADON_LONG_TERM_AVG)) - temperature = str(sensors.getValue(SENSOR_IDX_TEMPERATURE)) + " " + str(sensors.getUnit(SENSOR_IDX_TEMPERATURE)) - pressure = str(sensors.getValue(SENSOR_IDX_REL_ATM_PRESSURE)) + " " + str(sensors.getUnit(SENSOR_IDX_REL_ATM_PRESSURE)) - CO2_lvl = str(sensors.getValue(SENSOR_IDX_CO2_LVL)) + " " + str(sensors.getUnit(SENSOR_IDX_CO2_LVL)) - VOC_lvl = str(sensors.getValue(SENSOR_IDX_VOC_LVL)) + " " + str(sensors.getUnit(SENSOR_IDX_VOC_LVL)) + humidity = str(sensors.getValue(sensors.SENSOR_IDX_HUMIDITY)) + " " + str(sensors.getUnit(sensors.SENSOR_IDX_HUMIDITY)) + radon_st_avg = str(sensors.getValue(sensors.SENSOR_IDX_RADON_SHORT_TERM_AVG)) + " " + str(sensors.getUnit(sensors.SENSOR_IDX_RADON_SHORT_TERM_AVG)) + radon_lt_avg = str(sensors.getValue(sensors.SENSOR_IDX_RADON_LONG_TERM_AVG)) + " " + str(sensors.getUnit(sensors.SENSOR_IDX_RADON_LONG_TERM_AVG)) + temperature = str(sensors.getValue(sensors.SENSOR_IDX_TEMPERATURE)) + " " + str(sensors.getUnit(sensors.SENSOR_IDX_TEMPERATURE)) + pressure = str(sensors.getValue(sensors.SENSOR_IDX_REL_ATM_PRESSURE)) + " " + str(sensors.getUnit(sensors.SENSOR_IDX_REL_ATM_PRESSURE)) + CO2_lvl = str(sensors.getValue(sensors.SENSOR_IDX_CO2_LVL)) + " " + str(sensors.getUnit(sensors.SENSOR_IDX_CO2_LVL)) + VOC_lvl = str(sensors.getValue(sensors.SENSOR_IDX_VOC_LVL)) + " " + str(sensors.getUnit(sensors.SENSOR_IDX_VOC_LVL)) # Print data data = [humidity, radon_st_avg, radon_lt_avg, temperature, pressure, CO2_lvl, VOC_lvl] if (Mode=='terminal'): - print tableprint.row(data, width=12) + print (tableprint.row(data, width=12)) elif (Mode=='pipe'): - print data - + print (data) + waveplus.disconnect() - time.sleep(SamplePeriod) finally: diff --git a/read_waveplus_mqtt.py b/read_waveplus_mqtt.py new file mode 100644 index 0000000..64c3644 --- /dev/null +++ b/read_waveplus_mqtt.py @@ -0,0 +1,91 @@ +# MIT License +# +# Copyright (c) 2018 Airthings AS +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# https://airthings.com + +# =============================== +# Module import dependencies +# =============================== + +import sys +import time +import socket +import json +import paho.mqtt.client as mqtt +from Sensors import Sensors +from WavePlus import WavePlus + +# =============================== +# Script guards for correct usage +# =============================== +def print_usage(): + print ("USAGE: read_waveplus_mqtt.py SN SERVERADDRESS") + print (" where SN is the 10-digit serial number found under the magnetic backplate of your Wave Plus") + print (" and SERVERADDRESS specifies the IP-address of the MQTT server you want to post the results to.") + +if len(sys.argv) < 2: + print ("ERROR: Missing input argument SN or SERVERADDRESS.") + print_usage() + sys.exit(1) + +if sys.argv[1].isdigit() is not True or len(sys.argv[1]) != 10: + print ("ERROR: Invalid SN format.") + print_usage() + sys.exit(1) + +SerialNumber = int(sys.argv[1]) +Broker = sys.argv[2] + +try: + socket.inet_aton(Broker) + # legal +except socket.error: + # Not legal + print ("ERROR: Invalid IP address format:", Broker) + print_usage() + sys.exit(1) + +#---- Initialize ----# +waveplus = WavePlus(SerialNumber) +waveplus.connect() + +# read values +jsonPackage = dict() +sensors = waveplus.read() +client = mqtt.Client() +client.connect(Broker) +for i in range(sensors.NUMBER_OF_SENSORS): + topic = "waveplus/{0}/{1}".format(SerialNumber, sensors.header[i].replace(' ','_')) + info = client.publish(topic, sensors.getValue(i), retain=False) + info.wait_for_publish() + time.sleep(0.1) + jsonPackage[sensors.header[i].replace(' ','_')]={'value':sensors.getValue(i), 'notation':sensors.sensor_units[i]} + +# Post all values as json object +topic = "waveplus/{0}/".format(SerialNumber) +info = client.publish(topic, json.dumps(jsonPackage), retain=False) +info.wait_for_publish() +time.sleep(0.1) + +# End it all +client.disconnect() +waveplus.disconnect() diff --git a/waveplus.items b/waveplus.items new file mode 100644 index 0000000..371dff5 --- /dev/null +++ b/waveplus.items @@ -0,0 +1,14 @@ +// Wave Plus +// Replace with the serial no of your device +Group WavePlus +Number waveplus_livingroom_humidity "Luftfuktighet i stua" (WavePlus) { mqtt="<[mqtt:waveplus//Humidity:state:default]" } +Number waveplus_livingroom_co2level "CO2 i stua" (WavePlus) { mqtt="<[mqtt:waveplus//CO2_level:state:default]" } +Number waveplus_livingroom_pressure "Lufttrykk i stua" (WavePlus) { mqtt="<[mqtt:waveplus//Pressure:state:default]" } +Number waveplus_livingroom_radon_lta "Radon lang sikt gj.snitt" (WavePlus) { mqtt="<[mqtt:waveplus//Radon_LT_avg:state:default]" } +Number waveplus_livingroom_radon_sta "Radon kort sikt gj.snitt" (WavePlus) { mqtt="<[mqtt:waveplus//Radon_ST_avg:state:default]" } +Number waveplus_livingroom_temperature "Temperatur i stua" (WavePlus) { mqtt="<[mqtt:waveplus//Temperature:state:default]" } +Number waveplus_livingroom_voc_level "TVOC i stua" (WavePlus) { mqtt="<[mqtt:waveplus//VOC_level:state:default]" } +String waveplus_temperature "Temperatur [%.2f °C]" (WavePlus) { mqtt="<[mqtt:waveplus/:state:JSONPATH($.Temperature.value)]" } +String waveplus_radon "Radon [%.0f Bq/m3]" (WavePlus) { mqtt="<[mqtt:waveplus/:state:JSONPATH($.Radon_ST_avg.value)]" } +String waveplus_total_voc "Total VOC [%.0f ppbC]" (WavePlus) { mqtt="<[mqtt:waveplus/:state:JSONPATH($.VOC_level.value)]" } +