diff --git a/README.md b/README.md index c56596c..265f80f 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,13 @@ Ventus W820 bluetooth driver and weewx connector The Ventus W820 is a bluetooth enabled weather station. This repository provides a library to read the values of the sensors and a driver for the [weewx](http://www.weewx.com/) weather station software -The library requires [bluepy](https://github.com/IanHarvey/bluepy) for the BLE communication +The library requires [bluepy](https://github.com/IanHarvey/bluepy) for the BLE communication. Pick the python3 version (i.e. pip3 ...). Project website: [http://daduke.org/coding/ventus.html](http://daduke.org/coding/ventus.html) How to use it: - * install [bluepy](https://github.com/IanHarvey/bluepy) - * `ventus.py` is the low level library that talks to the weather station, copy it to a standard python library path (e.g. `/usr/local/lib/python2.7/site-packages/` + * install python3 version of [bluepy](https://github.com/IanHarvey/bluepy) + * `ventus.py` is the low level library that talks to the weather station, copy it to a standard python library path (e.g. `/usr/local/lib/python3.9/site-packages/` * `ventusw820.py` is the weewx driver, it goes into the weewx driver directory * define your Ventus station in the weewx config file: ``` diff --git a/ventus.py b/ventus.py index 8046976..6fde29d 100755 --- a/ventus.py +++ b/ventus.py @@ -1,5 +1,19 @@ import bluepy.btle as btle import array +import sys +import syslog + +def logmsg(level, msg): + syslog.syslog(level, 'ventus.py: %s' % msg) + +def logdbg(msg): + logmsg(syslog.LOG_DEBUG, msg) + +def loginf(msg): + logmsg(syslog.LOG_INFO, msg) + +def logerr(msg): + logmsg(syslog.LOG_ERR, msg) # read measurements from Ventus W820 weather station via bluetooth # (c) 2015 Christian Herzog @@ -29,71 +43,78 @@ def __init__(self, hndl): def handleNotification(self, cHandle, data): #data = data.encode("hex") data = array.array('B', data) + # JW adding some output + #loginf('handleNotification started') + #logdbg('data[0] is %s' % data[0]) + #logdbg('data: %s' % data.tolist()) + if data[0] == 1: - sensorData['degF'] = 0 - if data[5] < 127: #temps come in 2's complement - sensorData['indoorTemperature'] = ((data[5] * 256) + data[6])/10.0 - else: - sensorData['indoorTemperature'] = -(((255-data[5]) * 256) + (255-data[6]))/10.0 - sensorData['indoorHumidity'] = data[7] - if data[12] < 127: #temps come in 2's complement - sensorData['outdoorTemperature'] = ((data[12] * 256) + data[13])/10.0 - else: - sensorData['outdoorTemperature'] = -(((255-data[12]) * 256) + (255-data[13]))/10.0 - sensorData['outdoorHumidity'] = data[14] - if (data[1] & 2) != 0: # degF - sensorData['indoorTemperature'] = (sensorData['indoorTemperature'] - 32) / 1.8 - sensorData['outdoorTemperature'] = (sensorData['outdoorTemperature'] - 32) / 1.8 - sensorData['degF'] = 1 - if (data[1] & 8) != 0: - sensorData['lowBat'] = 1 - else: - sensorData['lowBat'] = 0 - + sensorData['degF'] = 0 + if data[5] < 127: #temps come in 2's complement + sensorData['indoorTemperature'] = ((data[5] * 256) + data[6])/10.0 + else: + sensorData['indoorTemperature'] = -(((255-data[5]) * 256) + (255-data[6]))/10.0 + sensorData['indoorHumidity'] = data[7] + if data[12] < 127: #temps come in 2's complement + sensorData['outdoorTemperature'] = ((data[12] * 256) + data[13])/10.0 + else: + sensorData['outdoorTemperature'] = -(((255-data[12]) * 256) + (255-data[13]))/10.0 + sensorData['outdoorHumidity'] = data[14] + if (data[1] & 2) != 0: # degF + sensorData['indoorTemperature'] = (sensorData['indoorTemperature'] - 32) / 1.8 + sensorData['outdoorTemperature'] = (sensorData['outdoorTemperature'] - 32) / 1.8 + sensorData['degF'] = 1 + if (data[1] & 8) != 0: + sensorData['lowBat'] = 1 + else: + sensorData['lowBat'] = 0 + elif data[0] == 2: - pressureUnit = ((data[1]) & 6) >> 1 - if pressureUnit > 2: - pressureUnit = 2 - sensorData['airPressure'] = ((data[3] * 256) + data[4]) - if pressureUnit == 0: #Pascalish - sensorData['airPressure'] /= 10.0 - elif pressureUnit == 1: #mm Hg, but wrong - sensorData['airPressure'] /= 10.0 - elif pressureUnit == 2: #inches Hg - sensorData['airPressure'] *= 0.338639 + pressureUnit = ((data[1]) & 6) >> 1 + if pressureUnit > 2: + pressureUnit = 2 + sensorData['airPressure'] = ((data[3] * 256) + data[4]) + if pressureUnit == 0: #Pascalish + sensorData['airPressure'] /= 10.0 + elif pressureUnit == 1: #mm Hg, but wrong + sensorData['airPressure'] /= 10.0 + elif pressureUnit == 2: #inches Hg + sensorData['airPressure'] *= 0.338639 elif data[0] == 3: - rainUnit = data[1] & 16 #0: mm, 16: inches - if rainUnit == 16: - rainFactor = 0.01 * 25.4 - else: - rainFactor = 0.1 - windUnit = (data[1] & 14) >> 1 - if windUnit > 4: #0: km/h, 1: mph, 2: m/s, 3: knots, 4: bft - windUnit = 4 + rainUnit = data[1] & 16 #0: mm, 16: inches + if rainUnit == 16: + rainFactor = 0.01 * 25.4 + else: + rainFactor = 0.1 + windUnit = (data[1] & 14) >> 1 + if windUnit > 4: #0: km/h, 1: mph, 2: m/s, 3: knots, 4: bft + windUnit = 4 sensorData['UV'] = data[18] - sensorData['rainDaily'] = ( (data[3] * 256) + data[4] ) * rainFactor - sensorData['rainWeekly'] = ( (data[5] * 256) + data[6] ) * rainFactor - sensorData['rainMonthly'] = ( ((data[7] * 65536) + (data[8] * 256)) + data[9] ) * rainFactor - sensorData['rainTotal'] = ( ((data[15] * 65536) + (data[16] * 256)) + data[17] ) * rainFactor - sensorData['windSpeed'] = (data[11] * 256) + data[12] - sensorData['windChill'] = ((data[13] * 256) + data[14])/10.0 - sensorData['windDirection'] = data[10] - if windUnit == 0: - sensorData['windSpeed'] *= 0.1 - if windUnit == 1: - sensorData['windSpeed'] = sensorData['windSpeed'] * 0.1 * 1.60934 - if sensorData['degF'] == 0: # bug in W820: wind speeds not in km/h depend on temperature unit!!! - sensorData['windSpeed'] *= 0.01313868613138686131 - elif windUnit == 2: - sensorData['windSpeed'] = sensorData['windSpeed'] * 0.1 * 3.6 - if sensorData['degF'] == 0: # bug in W820: wind speeds not in km/h depend on temperature unit!!! - sensorData['windSpeed'] *= 0.01157742402315484804 - elif windUnit == 3: - sensorData['windSpeed'] = sensorData['windSpeed'] * 0.1 * 1.85199539525386 - if sensorData['degF'] == 0: # bug in W820: wind speeds not in km/h depend on temperature unit!!! - sensorData['windSpeed'] *= 0.03782505910165484633 - + sensorData['rainDaily'] = ( (data[3] * 256) + data[4] ) * rainFactor + sensorData['rainWeekly'] = ( (data[5] * 256) + data[6] ) * rainFactor + sensorData['rainMonthly'] = ( ((data[7] * 65536) + (data[8] * 256)) + data[9] ) * rainFactor + sensorData['rainTotal'] = ( ((data[15] * 65536) + (data[16] * 256)) + data[17] ) * rainFactor + sensorData['windSpeed'] = (data[11] * 256) + data[12] + sensorData['windChill'] = ((data[13] * 256) + data[14])/10.0 + sensorData['windDirection'] = data[10] + if windUnit == 0: + #JW: i think this should not be changed (i.e. *=1.0), according to my observation/with my ventusw820 + # but keeping it as is in the PR as it is on python3 only + sensorData['windSpeed'] *= 0.1 + if windUnit == 1: + sensorData['windSpeed'] = sensorData['windSpeed'] * 0.1 * 1.60934 + if sensorData['degF'] == 0: # bug in W820: wind speeds not in km/h depend on temperature unit!!! + sensorData['windSpeed'] *= 0.01313868613138686131 + elif windUnit == 2: + sensorData['windSpeed'] = sensorData['windSpeed'] * 0.1 * 3.6 + if sensorData['degF'] == 0: # bug in W820: wind speeds not in km/h depend on temperature unit!!! + sensorData['windSpeed'] *= 0.01157742402315484804 + elif windUnit == 3: + sensorData['windSpeed'] = sensorData['windSpeed'] * 0.1 * 1.85199539525386 + if sensorData['degF'] == 0: # bug in W820: wind speeds not in km/h depend on temperature unit!!! + sensorData['windSpeed'] *= 0.03782505910165484633 + def connect(mac): w820 = None try: @@ -101,7 +122,7 @@ def connect(mac): return w820 except: e = sys.exc_info()[0] - print e + print(e) finally: return w820 @@ -114,23 +135,23 @@ def setTime(w820, time): def read(w820): w820.setDelegate(w820Delegate('')) try: - w820.writeCharacteristic(0x000e, '\x01\x00', True) # Turn on notificiations - w820.writeCharacteristic(0x000b, '\xb0\x01\x00\x00', True) # Request data - w820.waitForNotifications(1) # wait for response - w820.writeCharacteristic(0x000b, '\x40\x01\x00\x00', True) # send ACK for next packet - w820.waitForNotifications(1) # wait for response - w820.writeCharacteristic(0x000b, '\x40\x01\x00\x00', True) # send ACK for next packet - w820.waitForNotifications(1) # wait for response + w820.writeCharacteristic(0x000e, b'\x01\x00', True) # Turn on notificiations + w820.writeCharacteristic(0x000b, b'\xb0\x01\x00\x00', True) # Request data + w820.waitForNotifications(1) # wait for response + w820.writeCharacteristic(0x000b, b'\x40\x01\x00\x00', True) # send ACK for next packet + w820.waitForNotifications(1) # wait for response + w820.writeCharacteristic(0x000b, b'\x40\x01\x00\x00', True) # send ACK for next packet + w820.waitForNotifications(1) # wait for response except: e = sys.exc_info()[0] - print e + print(e) finally: - return sensorData + return sensorData def disconnect(w820): try: w820.disconnect except: e = sys.exc_info()[0] - print e + print(e) diff --git a/ventusw820.py b/ventusw820.py index 7cf17ce..263e55d 100644 --- a/ventusw820.py +++ b/ventusw820.py @@ -30,7 +30,7 @@ def confeditor_loader(): def logmsg(level, msg): - syslog.syslog(level, 'w820: %s' % msg) + syslog.syslog(level, 'ventusw820.py: %s' % msg) def logdbg(msg): logmsg(syslog.LOG_DEBUG, msg) @@ -71,31 +71,32 @@ def genLoopPackets(self): while True: data = self.station.get_readings() if Station.validate_data(data): - packet = Station.data_to_packet(data, int(time.time() + 0.5), last_rain=self._last_rain) - self._last_rain = packet['rainTotal'] + packet = Station.data_to_packet(data, int(time.time() + 0.5), last_rain=self._last_rain) + self._last_rain = packet['rainTotal'] yield packet time.sleep(self.polling_interval) class Station(object): def __init__(self, mac): self.mac = mac - self.handle = None - self.sensorData = {} + self.handle = None + self.sensorData = {} def get_readings(self): - sensorData = {} + sensorData = {} sensorData['indoorTemperature'] = None if self.handle is None: try: - logdbg("open bluetooth connection to MAC %s" % self.mac) + #JW logdbg("open bluetooth connection to MAC %s" % self.mac) self.handle = ventus.connect(self.mac) sensorData = ventus.read(self.handle) + #loginf(sensorData) except: - e = sys.exc_info()[0] + e = sys.exc_info()[0] loginf("could not open BLE connection: %s" % e) - finally: + finally: if self.handle is not None: - logdbg("close bluetooth connection to %s" % self.mac) + #JW logdbg("close bluetooth connection to %s" % self.mac) ventus.disconnect(self.handle) self.handle = None return sensorData @@ -106,64 +107,69 @@ def validate_data(sensorData): #TODO if sensorData and sensorData['indoorTemperature'] is not None: return 1 - else: - return 0 + else: + return 0 @staticmethod def data_to_packet(data, ts, last_rain=None): - """Convert raw data to format and units required by weewx. - - WS820 weewx (metric) - temperature degree C degree C - humidity percent percent - uv index unitless unitless - pressure mbar/hPa mbar - wind speed km/h km/h - wind dir 0..15 (N->E) degree - rain mm cm - """ - - packet = dict() - packet['usUnits'] = weewx.METRIC - packet['dateTime'] = ts - packet['inTemp'] = data['indoorTemperature'] - packet['inHumidity'] = data['indoorHumidity'] - packet['outTemp'] = data['outdoorTemperature'] - packet['outHumidity'] = data['outdoorHumidity'] - packet['barometer'] = data['airPressure'] - packet['outTempBatteryStatus'] = data['lowBat'] - - ws, wd, wso, wsv = (data['windSpeed'], data['windDirection'], 0, 0) - if wso == 0 and wsv == 0: - packet['windSpeed'] = ws - packet['windDir'] = wd*22.5 if packet['windSpeed'] else None - else: - loginf('invalid wind reading: speed=%s dir=%s overflow=%s invalid=%s' % (ws, wd, wso, wsv)) - packet['windSpeed'] = None - packet['windDir'] = None - - packet['windGust'] = None - packet['windGustDir'] = None - - packet['rainTotal'] = data['rainTotal'] - if packet['rainTotal'] is not None: - packet['rainTotal'] /= 10.0 # weewx wants cm - packet['rain'] = weewx.wxformulas.calculate_rain(packet['rainTotal'], last_rain) - - #station provides some derived variables -# packet['rainRate'] = data['rh'] -# if packet['rainRate'] is not None: -# packet['rainRate'] /= 10 # weewx wants cm/hr -# packet['dewpoint'] = data['dp'] -# packet['windchill'] = data['wc'] - - return packet + """Convert raw data to format and units required by weewx. + + WS820 weewx (metric) + temperature degree C degree C + humidity percent percent + uv index unitless unitless + pressure mbar/hPa mbar + wind speed km/h km/h + wind dir 0..15 (N->E) degree + rain mm cm + """ + + #JW: logdbg("data received are: %s" % data) + packet = dict() + packet['usUnits'] = weewx.METRIC + packet['dateTime'] = ts + packet['inTemp'] = data['indoorTemperature'] + packet['inHumidity'] = data['indoorHumidity'] + packet['outTemp'] = data['outdoorTemperature'] + packet['outHumidity'] = data['outdoorHumidity'] + packet['barometer'] = data['airPressure'] + packet['outTempBatteryStatus'] = data['lowBat'] + + ws, wd, wso, wsv = (data['windSpeed'], data['windDirection'], 0, 0) + if wso == 0 and wsv == 0: + packet['windSpeed'] = ws + packet['windDir'] = wd*22.5 if packet['windSpeed'] else None + else: + loginf('invalid wind reading: speed=%s dir=%s overflow=%s invalid=%s' % (ws, wd, wso, wsv)) + packet['windSpeed'] = None + packet['windDir'] = None + + packet['windGust'] = None + packet['windGustDir'] = None + + packet['rainTotal'] = data['rainTotal'] + if packet['rainTotal'] is not None: + packet['rainTotal'] /= 10.0 # weewx wants cm + packet['rain'] = weewx.wxformulas.calculate_rain(packet['rainTotal'], last_rain) + + #JW: one could add UV as well + #packet['UV'] = data['UV'] + + #station provides some derived variables +# packet['rainRate'] = data['rh'] +# if packet['rainRate'] is not None: +# packet['rainRate'] /= 10 # weewx wants cm/hr +# packet['dewpoint'] = data['dp'] +# packet['windchill'] = data['wc'] + + return packet class W820ConfEditor(weewx.drivers.AbstractConfEditor): @property def default_stanza(self): - return """ + return +""" [W820] # This section is for the Ventus W820 weather station. @@ -175,7 +181,7 @@ def default_stanza(self): """ def prompt_for_settings(self): - print "Specify the MAC address of your W820 bluetooth interface" + print("Specify the MAC address of your W820 bluetooth interface") mac = self._prompt('mac', 'ff:ff:ff:ff:ff:ff') return {'mac': mac} @@ -201,9 +207,9 @@ def prompt_for_settings(self): (options, args) = parser.parse_args() if options.version: - print "Ventus W820 driver version %s" % DRIVER_VERSION + print("Ventus W820 driver version %s" % DRIVER_VERSION) exit(0) with Station(options.mac) as s: while True: - print time.time(), s.get_readings() + print(time.time(), s.get_readings())