Finally got some time it port my libpcap based stuff into the driver. I figure I'd post it here in case you had any interest in incorporating it into the main driver. I could see it having its advantages over the socket based approach.
I'm able to run this on a server already running a webserver or port 80, the myacurite functionality still works, and (my favorite part) all functionality is encompassed within the driver and nothing is required outside of it (ie no ngrep, or tcpdump redirects. My formal OOP skills are a bit rusty, and I am sure the pcap stuff could be incorporated into the architecture of the base driver better than I have. My intent was to allow for both methods of operation, defaulting to the socket approach unless a flag was specified. FYI, I didn't resolve the standalone portion with its current form (initially I passed in the whole options dict for the various parameters, now it reads those out of stn_dict), just an FYI if you try to run it as is - it will probably fail without some code massaging, On Sunday, October 16, 2016 at 12:40:14 AM UTC-5, Jerome Helbert wrote: > > ok, I see what radar is doing... Most of the "magic" is going to happen in > a perl script that he is piping the stream into. His tcpdump and stdbuf all > pretty much give the same stuff as the ngrep/sed stuff I have been trying > to use (spread across multiple lines still) and then he has a perl script > that glues them all together. > > I already wasn't super thrilled about having to run the helper script > outside the driver to sniff the data, and I really want to avoid a third > step... > > I am brushing up on my iptables... hoping that some combination of a TEE > and a DNAT or REDIRECT can sniff off the relevant stream and change the > dest ip/port. That then feeds into the interceptor driver as normal. If I > cant get that to work, I am thinking about bringing in the libpcap stuff I > had added to my hackulink driver and eliminating the helper script all > together. > > If I got the libpcap working, is that something we'd be interesting in > bringing into the driver itself? > > On Saturday, October 15, 2016 at 3:04:22 PM UTC-5, mwall wrote: >> >> On Saturday, October 15, 2016 at 12:21:13 PM UTC-4, Jerome Helbert wrote: >> >> >>> Two things here... >>> >>> 1. I need some way to get ngrep or tcpdump to piece together the >>> http stream properly... I tried to use sed, but since sed operates on a >>> line by line basis - it is not very easy (if possible) for it to remove >>> newlines... >>> >>> have you tried radar's approach with stdbuf and strings? once the >> fragments are reassembled, you should be able to send the data along using >> curl. >> >> >>> >>> 1. I seem to have found a bug with the version reporting... since it >>> reported a different version than what was in it the bridge went into >>> reprogramming mode and would *not* leave until it updated firmware >>> (undid the DNS Hijack, etc and let it re-update to 224 even though it >>> was >>> already at that version) >>> >>> there is an (untested) fix for this at commit b27acc1 (interceptor >> driver 0.13a) >> >> [Interceptor] >> ... >> firmware_version = 224 >> >> m >> >> >> >> > -- You received this message because you are subscribed to the Google Groups "weewx-user" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. For more options, visit https://groups.google.com/d/optout.
#!/usr/bin/env python # Copyright 2016 Matthew Wall, all rights reserved """ This driver runs a simple web server designed to receive data directly from an internet weather reporting device such as the Acurite internet bridge, the LaCrosse GW1000U internet bridge, the Oregon Scientific LW300 (LW301/LW302) internet bridge, or the FineOffset HP1000 console or WH2600 internet bridge. Thanks to rich of modern toil and george nincehelser for acurite parsing http://moderntoil.com/?p=794 http://nincehelser.com/ipwx/ Thanks to Pat at obrienlabs.net for the fine offset parsing http://obrienlabs.net/redirecting-weather-station-data-from-observerip/ Thanks to sergei and waebi for the LW301/LW302 samples http://www.silent-gardens.com/blog/shark-hunt-lw301/ Thanks to Sam Roza for packet captures from the LW301 Thanks to skydvrz, mycal, kennkong for publishing their lacrosse work http://www.wxforum.net/index.php?topic=14299.0 https://github.com/lowerpower/LaCrosse https://github.com/kennkong/Weather-ERF-Gateway-1000U About the stations Acurite Bridge The Acurite bridge communicates with Acurite 5-in-1, 3-in-1, temperature, and temperature/humidity sensors. It receives signals from any number of sensors, even though Acurite's web interface is limited to 3 devices (or 10 as of the July 2016 firmware update). By default, the bridge transmits data to www.acu-link.com. Acurite requires registration of the bridge's MAC address in order to use acu-link.com. However, the bridge will function even if it is not registered, as long as it receives the proper response. The bridge sends data as soon as it receives an observation from the sensors. Chaney did a firmware update to the bridge in July 2016. This update made the bridge emit data using the weather underground protocol instead of the Chaney protocol. Observer Manufactured by Fine Offset as the WH2600, HP1000, and HP1003. WH2600: bridge (wifi), cluster, THP HP1000: console (wifi), cluster, THP HP1003: console (no wifi), cluster, THP Sold by Ambient as the 'Observer' including WS1001, WS1200IP, and WS1400IP. WS0800: bridge, THP, TH WS1400: bridge (wifi), cluster, THP WS1200: bridge (wifi), console, cluster, THP WS1001: console (wifi), cluster, THP Ambient also sells 'AirBridge' and 'WeatherBridge' variants, but these use a meteostick and meteohub/plug instead of the Fine Offset bridge. Sold by Froggit as the HP1000 Profi Funk Wetterstation. Sold by Aercus as the WeatherSleuth and WeatherRanger. It looks like this hardware simply sends data in weather underground format. The bridge sends data every 5 minutes. Oregon Scientific LW301/LW302 The "Anywhere Weather Kit" comes in two packages, the LW301 with a full set of sensors, and the LW302 with only inside and outside temperature/humidity sensors. Both kits include the LW300 "Internet connected hub" which is connected to the sensor base station via USB (for power only?) and to the network via wired ethernet. LW301: bridge (ethernet), base, rain, wind, TH LW302: bridge (ethernet), base, TH The base communicates with many different OS sensors, not just those included in the Anywhere Weather Kit. For example, the THGR810 temperature/humidity sensors (up to 10 channels!) and the sensors included with the WMR86 stations are recognized by the LW300 base receivers. By default, the bridge communicates with www.osanywhereweather.com LaCrosse GW1000U The LaCrosse gateway communicates via radio with the C84612 display, which in turn communicates with the rain, wind, and TH sensors. The gateway has a wired ethernet connection. The gateway communicates with weatherdirect.com. LaCrosse alerts is a fee- based system for receiving alerts from the gateway via lacrossealertsmobile.com If you have any intention of using LaCrosse's alerts service, you should register your station with LaCrosse before using this driver. The bridge attempts to upload to /request.breq The easiest way to use this driver is to use the Gateway Advance Setup (GAS) utility from LaCrosse to configure the gateway to send to the computer with this driver. """ # FIXME: automatically detect the traffic type # FIXME: handle traffic from multiple types of devices # FIXME: default acurite mapping confuses multiple tower sensors from __future__ import with_statement import BaseHTTPServer import SocketServer import Queue import binascii import calendar import syslog import threading import time import urlparse import pcap import weewx.drivers DRIVER_NAME = 'Interceptor' DRIVER_VERSION = '0.13a2' DEFAULT_PORT = 80 DEFAULT_ADDR = '' DEFAULT_IFACE = 'eth0' DEFAULT_DEVICE_TYPE = 'acurite-bridge' def loader(config_dict, _): return InterceptorDriver(**config_dict[DRIVER_NAME]) def confeditor_loader(): return InterceptorConfigurationEditor() def logmsg(level, msg): syslog.syslog(level, 'interceptor: %s: %s' % (threading.currentThread().getName(), 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) def _obfuscate_passwords(msg): idx = msg.find('PASSWORD') if idx >= 0: import re msg = re.sub(r'PASSWORD=[^&]+', r'PASSWORD=XXXX', msg) return msg def _cgi_to_dict(s): return dict([y.strip() for y in x.split('=')] for x in s.split('&')) class Consumer(object): queue = Queue.Queue() def __init__(self, server_address, handler, parser, iface, sniffer=False): self.parser = parser self.sniff_not_intercept = sniffer if sniffer: self._sniffer = Consumer.Sniffer(server_address, iface, handler) else: self._server = Consumer.Server(server_address, handler) def run_server(self): if self.sniff_not_intercept: while True: self._sniffer.packet_sniffer.dispatch(1, self._sniffer.decode_ip_packet) else: self._server.serve_forever() def shutdown(self): if self.sniff_not_intercept: self._sniffer.packet_sniffer.close() else: self._server.shutdown() self._server.server_close() self._server = None def get_queue(self): return Consumer.queue class Server(SocketServer.TCPServer): daemon_threads = True allow_reuse_address = True def __init__(self, server_address, handler): SocketServer.TCPServer.__init__(self, server_address, handler) class Sniffer(): packet_sniffer = pcap.pcapObject() reassembled_string = '' get_sniff_active = False def __init__(self, server_address, iface, handler): (addr, port) = server_address self.handler = handler try: self.packet_sniffer.open_live(iface, 1600, 0, 100) self.packet_sniffer.setfilter("src host %s && dst port %i and greater 61" % (addr, port), 0, 0) except: syslog.syslog(syslog.LOG_ERR, "Interceptor: Unable to monitor packets coming from host %s port %s on device %s" % (addr, port, iface)) raise print "Interceptor: Monitoring packets coming from host %s port %s on device %s" % (addr, port, iface) def decode_ip_packet(self, pktlen, data, timestamp): if data: #print "pktlen: %d, timestamp: %d, data: %s" % (pktlen, timestamp, data) if data[12:14]=='\x08\x00': header_len = ord(data[14]) & 0x0f _data = data[4*header_len + 34:] #print " %s" % _data if 'GET' in _data: self.reassembled_string = _data self.get_sniff_active = True elif 'HTTP' in _data and self.get_sniff_active: self.get_sniff_active = False #print self.reassembled_string data = urlparse.urlparse(self.reassembled_string).query Consumer.queue.put(data) elif self.get_sniff_active: self.reassembled_string += _data class Handler(BaseHTTPServer.BaseHTTPRequestHandler): def get_response(self): # default reply is a simple 'OK' string return 'OK' def reply(self): # standard reply is HTTP code of 200 and the response string response = bytes(self.get_response()) self.send_response(200) self.send_header("Content-Length", str(len(response))) self.end_headers() self.wfile.write(response) def do_POST(self): # get the payload from an HTTP POST length = int(self.headers["Content-Length"]) data = str(self.rfile.read(length)) logdbg('POST: %s' % _obfuscate_passwords(data)) Consumer.queue.put(data) self.reply() def do_PUT(self): pass def do_GET(self): # get the query string from an HTTP GET data = urlparse.urlparse(self.path).query logdbg('GET: %s' % _obfuscate_passwords(data)) Consumer.queue.put(data) self.reply() # do not spew messages on every connection def log_message(self, _format, *_args): pass class Parser(object): @staticmethod def parse_identifiers(s): return dict() def parse(self, s): return dict() @staticmethod def map_to_fields(pkt, sensor_map): # the sensor map is a dictionary of database field names as keys, # each with an associated observation identifier. if sensor_map is None: return pkt packet = {'dateTime': pkt['dateTime'], 'usUnits': pkt['usUnits']} for n in sensor_map: label = Consumer.Parser._find_match(sensor_map[n], pkt.keys()) if label: packet[n] = pkt.get(label) return packet @staticmethod def _find_match(pattern, keylist): # pattern can be a simple label, or an identifier pattern. # keylist is an array of observations, each of which is either # a simple label, or an identifier tuple. match = None pparts = pattern.split('.') if len(pparts) == 3: for k in keylist: kparts = k.split('.') if (len(kparts) == 3 and Consumer.Parser._part_match(pparts[0], kparts[0]) and Consumer.Parser._part_match(pparts[1], kparts[1]) and Consumer.Parser._part_match(pparts[2], kparts[2])): match = k elif pparts[0] == k: match = k else: for k in keylist: if pattern == k: match = k return match @staticmethod def _part_match(pattern, value): # see whether the value matches the pattern. if pattern == value: return True if pattern == '*' and value: return True return False @staticmethod def _delta_rain(rain, last_rain): if last_rain is None: loginf("skipping rain measurement of %s: no last rain" % rain) return None if rain < last_rain: loginf("rain counter wraparound detected: new=%s last=%s" % (rain, last_rain)) return None return rain - last_rain @staticmethod def decode_float(x): return None if x is None else float(x) @staticmethod def decode_int(x): return None if x is None else int(x) @staticmethod def decode_wu_datetime(s): if s == 'now': return int(time.time() + 0.5) s = s.replace("%20", " ") ts = time.strptime(s, "%Y-%m-%d %H:%M:%S") return calendar.timegm(ts) # sample output from a bridge with 3 t/h sensors and 1 5-in-1 # # Chaney format (pre-July2016): # id=X&mt=pressure&C1=452D&C2=0D7F&C3=010D&C4=0330&C5=8472&C6=1858&C7=09C4&A=07&B=1B&C=06&D=09&PR=91CA&TR=8270 # id=X&sensor=02004&mt=5N1x31&windspeed=A001660000&winddir=8&rainfall=A0000000&battery=normal&rssi=3 # id=X&sensor=02004&mt=5N1x38&windspeed=A001890000&humidity=A0280&temperature=A014722222&battery=normal&rssi=3 # id=X&sensor=06022&mt=tower&humidity=A0270&temperature=A020100000&battery=normal&rssi=3 # id=X&sensor=05961&mt=tower&humidity=A0300&temperature=A017400000&battery=normal&rssi=3 # id=X&sensor=14074&mt=tower&humidity=A0300&temperature=A021500000&battery=normal&rssi=4 # # WU format (as of July 2016): # GET /weatherstation/updateweatherstation?dateutc=now&action=updateraw&realtime=1&id=X&mt=5N1x31&sensor=00003301&windspeedmph=5&winddir=113&rainin=0.00&dailyrainin=0.00&humidity=45&tempf=95.6&dewptf=76.0&baromin=30.11&battery=normal&rssi=2 # # new format samples from nincehelser (July 2016): # dateutc=now&action=updateraw&realtime=1&id=24C86Exxxxxx&mt=tower&sensor=00002719&humidity=15&tempf=83.8&baromin=29.92&battery=normal&rssi=3 # dateutc=now&action=updateraw&realtime=1&id=24C86Exxxxxx&mt=5N1x31&sensor=00001398&windspeedmph=9&winddir=180&rainin=0.00&dailyrainin=0.03&baromin=29.92&battery=normal&rssi=1 # dateutc=now&action=updateraw&realtime=1&id=24C86Exxxxxx&mt=5N1x38&sensor=00001398&windspeedmph=9&humidity=76&tempf=84.0&baromin=29.92&battery=normal&rssi=1 # # new format samples from radar on the weewx-user forum 21aug2016 # (docbee posted about ptempf, probe, and check on wxforum 24aug2016) # # 5n1 # &id=MAC&mt=5N1x31&sensor=0000xxxx # &windspeedmph=1&winddir=45&rainin=0.00&dailyrainin=0.00 # &baromin=28.77&battery=normal&rssi=2 # # &id=MAC&mt=5N1x38&sensor=0000xxxx # &windspeedmph=1&humidity=53&tempf=73.8 # &baromin=28.77&battery=normal&rssi=2 # # tower # &id=MAC&mt=tower&sensor=0000xxxx # &humidity=54&tempf=66.0 # &baromin=28.77&battery=normal&rssi=2 # # room-monitor with one water decetor # &id=MAC&mt=ProIn&sensor=0000xxxx # &indoorhumidity=61&indoortempf=65.8 # &probe=1&check=0&water=0 # &baromin=28.77&battery=normal&rssi=2 # # outside temp and humidity with Liquid and or Soil Temp # &id=MAC&mt=ProOut&sensor=0000xxxx # &humidity=63&tempf=65.2 # &probe=2&check=0&ptempf=64.9 # &baromin=28.77&battery=normal&rssi=3 # # rain gauge # &id=MAC&mt=rain899&sensor=000xxxxx # &rainin=0.00&dailyrainin=0.00 # &baromin=28.77&battery=normal&rssi=2 # # ProIn sensor no indicators # &id=MAC&mt=ProIn&sensor=0000xxxx # &indoorhumidity=61&indoortempf=67.1 # &baromin=28.69&battery=normal&rssi=2 # # ProIn sensor with one Water Detector # &id=MAC&mt=ProIn&sensor=0000xxxx # &indoorhumidity=60&indoortempf=67.1 # &probe=1&check=0&water=0 # &baromin=28.68&battery=normal&rssi=2 # # ProIn sensor with Liquide and or Soil Temp # &id=MAC&mt=ProIn&sensor=0000xxxx # &indoorhumidity=58&indoortempf=69.0 # &probe=2&check=0&ptempf=66.9 # &baromin=28.68&battery=normal&rssi=3 # # ProIn with water detector when water is detected # &id=MAC&mt=ProIn&sensor=0000xxxx # &indoorhumidity=59&indoortempf=67.2 # &probe=1&check=0&water=1 # &baromin=28.65&battery=normal&rssi=2 # # ProIn sensor with Spot Check Temperature and Humidity Sensor model# 06012RM # &id=MAC&mt=ProIn&sensor=0000xxxx # &indoorhumidity=63&indoortempf=66.9 # &probe=3&check=0&ptempf=74.3&phumidity=50 # &baromin=28.90&battery=normal&rssi=2 # the room monitor with water detector # Model: 00276WD-bundle # the outdoor monitor with liquid & soil temperature sensor # Model: 00275LS-bundle class AcuriteBridge(Consumer): # these are the known firmware versions as of 15oct2016: # # 126 is the version for the chaney format (pre july 2016) # 224 is the version for the wu format (circa july 2016) # # if the firmware version does not match that of the bridge, the bridge # will attempt to download the latest firmware from chaney. _firmware_version = 126 def __init__(self, server_address, **stn_dict): if 'sniff' in stn_dict: if 'iface' in stn_dict: iface = stn_dict['iface'] print "sniffing" super(AcuriteBridge, self).__init__( server_address, AcuriteBridge.Handler, AcuriteBridge.Parser(), iface, sniffer=True) else: super(AcuriteBridge, self).__init__( server_address, AcuriteBridge.Handler, AcuriteBridge.Parser()) if 'firmware_version' in stn_dict: AcuriteBridge._firmware_version = stn_dict['firmware_version'] class Handler(Consumer.Handler): def get_response(self): return '{ "success": 1, "checkversion": "%s" }' % AcuriteBridge._firmware_version class Parser(Consumer.Parser): # map database fields to observation identifiers DEFAULT_SENSOR_MAP = { 'pressure': 'pressure..*', 'inTemp': 'temperature..*', 'outTemp': 'temperature.*.*', 'outHumidity': 'humidity.*.*', 'windSpeed': 'windspeed.*.*', 'windDir': 'winddir.*.*', 'rain': 'rainfall.*.*', 'txBatteryStatus': 'battery.*.*', 'rxCheckPercent': 'rssi.*.*'} # this is *not* the same as the acurite console mapping! IDX_TO_DEG = {5: 0.0, 7: 22.5, 3: 45.0, 1: 67.5, 9: 90.0, 11: 112.5, 15: 135.0, 13: 157.5, 12: 180.0, 14: 202.5, 10: 225.0, 8: 247.5, 0: 270.0, 2: 292.5, 6: 315.0, 4: 337.5} # map wu names to observation names LABEL_MAP = { 'humidity': 'humidity', 'tempf': 'temperature', 'indoorhumidity': 'humidity', 'indoortempf': 'temperature', 'ptempf': 'probe_temperature', 'baromin': 'barometer', 'windspeedmph': 'windspeed', 'winddir': 'winddir', 'rainin': 'rainfall' } IGNORED_LABELS = ['dailyrainin', 'realtime', 'rtfreq', 'action', 'ID', 'PASSWORD', 'dateutc', 'updateraw', 'sensor', 'mt', 'id', 'probe', 'check', 'water'] @staticmethod def parse_identifiers(s): data = _cgi_to_dict(s) return {'sensor_type': data.get('mt'), 'sensor_id': data.get('sensor'), 'bridge_id': data.get('id')} def __init__(self): self._last_rain = None # be ready for either the chaney format or the wu format def parse(self, s): if s.find('action') >= 0: return self.parse_wu(s) return self.parse_chaney(s) # parse packets that are in the weather underground format def parse_wu(self, s): pkt = dict() try: data = _cgi_to_dict(s) # FIXME: add option to use computer time instead of station pkt['dateTime'] = self.decode_wu_datetime( data.pop('dateutc', int(time.time() + 0.5))) pkt['usUnits'] = weewx.US for n in data: if n == 'id': pkt['bridge_id'] = data[n] elif n == 'sensor': pkt['sensor_id'] = data[n] elif n == 'mt': pkt['sensor_type'] = data[n] elif n == 'battery': pkt['battery'] = 0 if data[n] == 'normal' else 1 elif n == 'rssi': pkt['rssi'] = float(data[n]) / 4.0 elif n in self.LABEL_MAP: pkt[self.LABEL_MAP[n]] = self.decode_float(data[n]) elif n in self.IGNORED_LABELS: logdbg("ignored parameter %s=%s" % (n, data[n])) else: loginf("unrecognized parameter %s=%s" % (n, data[n])) except ValueError, e: logerr("parse failed for %s: %s" % (s, e)) return self.add_identifiers(pkt) # parse packets that are in the chaney format def parse_chaney(self, s): pkt = dict() parts = s.split('&') for x in parts: if not x: continue (n, v) = x.split('=') n = n.strip() v = v.strip() try: if n == 'id': pkt['bridge_id'] = v elif n == 'sensor': pkt['sensor_id'] = v elif n == 'mt': pkt['sensor_type'] = v elif n == 'battery': pkt['battery'] = 0 if v == 'normal' else 1 elif n == 'rssi': pkt['rssi'] = float(v) / 4.0 elif n == 'humidity': pkt['humidity'] = float(v[2:5]) / 10.0 # % elif n == 'temperature': pkt['temperature'] = float(v[1:5]) / 10.0 # C elif n == 'windspeed': pkt['windspeed'] = float(v[2:5]) / 10.0 # m/s elif n == 'winddir': pkt['winddir'] = AcuriteBridge.Parser.IDX_TO_DEG.get(int(v, 16)) elif n == 'rainfall': pkt['rainfall'] = float(v[2:8]) / 1000.0 # mm (delta) elif n in ['C1', 'C2', 'C3', 'C4', 'C5', 'C6', 'C7', 'A', 'B', 'C', 'D', 'PR', 'TR']: pkt[n] = int(v, 16) else: loginf("unknown element '%s' with value '%s'" % (n, v)) except (ValueError, IndexError), e: logerr("decode failed for %s '%s': %s" % (n, v, e)) # if this is a pressure packet, calculate the pressure if 'sensor_type' in pkt and pkt['sensor_type'] == 'pressure': pkt['pressure'], pkt['temperature'] = AcuriteBridge.Parser.decode_pressure(pkt) # apply timestamp and units pkt['dateTime'] = int(time.time() + 0.5) pkt['usUnits'] = weewx.METRICWX return self.add_identifiers(pkt) @staticmethod def add_identifiers(pkt): # tag each observation with identifiers: # observation.<sensor_id>.<bridge_id> packet = {'dateTime': pkt['dateTime'], 'usUnits': pkt['usUnits']} _id = '%s.%s' % ( pkt.get('sensor_id', ''), pkt.get('bridge_id', '')) for n in pkt: packet["%s.%s" % (n, _id)] = pkt[n] return packet @staticmethod def map_to_fields(pkt, sensor_map): if sensor_map is None: sensor_map = AcuriteBridge.Parser.DEFAULT_SENSOR_MAP return Consumer.Parser.map_to_fields(pkt, sensor_map) @staticmethod def decode_pressure(pkt): # pressure in mbar, temperature in degree C if (0x100 <= pkt['C1'] <= 0xffff and 0x0 <= pkt['C2'] <= 0x1fff and 0x0 <= pkt['C3'] <= 0x400 and 0x0 <= pkt['C4'] <= 0x1000 and 0x1000 <= pkt['C5'] <= 0xffff and 0x0 <= pkt['C6'] <= 0x4000 and 0x960 <= pkt['C7'] <= 0xa28 and 0x01 <= pkt['A'] <= 0x3f and 0x01 <= pkt['B'] <= 0x3f and 0x01 <= pkt['C'] <= 0x0f and 0x01 <= pkt['D'] <= 0x0f): return AcuriteBridge.Parser.decode_HP03S( pkt['C1'], pkt['C2'], pkt['C3'], pkt['C4'], pkt['C5'], pkt['C6'], pkt['C7'], pkt['A'], pkt['B'], pkt['C'], pkt['D'], pkt['PR'], pkt['TR']) logerr("one or more bogus constants in pressure packet: %s" % pkt) return None, None @staticmethod def decode_HP03S(c1, c2, c3, c4, c5, c6, c7, a, b, c, d, d1, d2): if d2 >= c5: dut = d2 - c5 - ((d2-c5)/128) * ((d2-c5)/128) * a / (2<<(c-1)) else: dut = d2 - c5 - ((d2-c5)/128) * ((d2-c5)/128) * b / (2<<(c-1)) off = 4 * (c2 + (c4 - 1024) * dut / 16384) sens = c1 + c3 * dut / 1024 x = sens * (d1 - 7168) / 16384 - off p = 0.1 * (x * 10 / 32 + c7) t = 0.1 * (250 + dut * c6 / 65536 - dut / (2<<(d-1))) return p, t # sample output from an observer # # ID=XXXX&PASSWORD=PASSWORD&tempf=43.3&humidity=98&dewptf=42.8&windchil # lf=43.3&winddir=129&windspeedmph=0.00&windgustmph=0.00&rainin=0.00&da # ilyrainin=0.04&weeklyrainin=0.04&monthlyrainin=0.91&yearlyrainin=0.91 # &solarradiation=0.00&UV=0&indoortempf=76.5&indoorhumidity=49&baromin= # 29.05&lowbatt=0&dateutc=2016-1-4%2021:2:35&softwaretype=Weather%20log # ger%20V2.1.9&action=updateraw&realtime=1&rtfreq=5 # # ID=XXXX&PASSWORD=PASSWORD&intemp=22.8&outtemp=1.4&dewpoint=1.1&windch # ill=1.4&inhumi=36&outhumi=98&windspeed=0.0&windgust=0.0&winddir=193&a # bsbaro=1009.5&relbaro=1033.4&rainrate=0.0&dailyrain=0.0&weeklyrain=10 # .5&monthlyrain=10.5&yearlyrain=10.5&light=1724.9&UV=38&dateutc=2016-4 # -19%204:42:35&softwaretype=HP1001%20V2.2.2&action=updateraw&realtime= # 1&rtfreq=5 # # ID=XXXX&PASSWORD=PASSWORD&intemp=23.2&outtemp=10.1&dewpoint=2.0&windc # hill=10.1&inhumi=32&outhumi=57&windspeed=0.0&windgust=0.0&winddir=212 # &absbaro=1010.1&relbaro=1034.0&rainrate=0.0&dailyrain=0.0&weeklyrain= # 10.5&monthlyrain=10.5&yearlyrain=10.5&light=31892.0&UV=919&dateutc=20 # 16-4-19%207:54:4&softwaretype=HP1001%20V2.2.2&action=updateraw&realti # me=1&rtfreq=5 # # GET /weatherstation/updateweatherstation.asp?ID=XXXXXXXXXXXXX&PASSWOR # D=PASSWORD&outtemp=6.3&outhumi=80&dewpoint=3.1&windchill=6.3&winddir= # 197&windspeed=0.0&windgust=0.0&rainrate=0.0&dailyrain=0.0&weeklyrain= # 0.0&monthlyrain=0.0&yearlyrain=0.0&light=0.00&UV=1&intemp=19.8&inhumi # =46&absbaro=1018.30&relbaro=1018.30&lowbatt=0&dateutc=2016-4-30%2021: # 5:1&softwaretype=Weather%20logger%20V2.1.9&action=updateraw&realtime= # 1&rtfreq=5 HTTP/1.0 # # GET /weatherstation/updateweatherstation.php?ID=XXXXXXXXXXXXX&PASSWOR # D=PASSWORD&tempf=-9999&humidity=-9999&dewptf=-9999&windchillf=-9999&w # inddir=-9999&windspeedmph=-9999&windgustmph=-9999&rainin=0.00&dailyra # inin=0.00&weeklyrainin=0.00&monthlyrainin=0.00&yearlyrainin=0.00&sola # rradiation=-9999&UV=-9999&indoortempf=66.2&indoorhumidity=47&baromin= # 29.94&lowbatt=0&dateutc=2016-5-10%202:34:15&softwaretype=Weather%20lo # gger%20V3.0.7&action=updateraw&realtime=1&rtfreq=5 class Observer(Consumer): def __init__(self, server_address, **stn_dict): super(Observer, self).__init__( server_address, Consumer.Handler, Observer.Parser()) class Parser(Consumer.Parser): # map database fields to observation names DEFAULT_SENSOR_MAP = { 'pressure': 'pressure', 'barometer': 'barometer', 'outHumidity': 'out_humidity', 'inHumidity': 'in_humidity', 'outTemp': 'out_temperature', 'inTemp': 'in_temperature', 'windSpeed': 'wind_speed', 'windGust': 'wind_gust', 'radiation': 'radiation', 'dewpoint': 'dewpoint', 'windchill': 'windchill', 'rain': 'rain', 'rainRate': 'rain_rate', 'UV': 'uv', 'txBatteryStatus': 'battery'} # map labels to observation names LABEL_MAP = { # for firmware Weather logger V2.1.9 'humidity': 'out_humidity', 'indoorhumidity': 'in_humidity', 'tempf': 'out_temperature', 'indoortempf': 'in_temperature', 'baromin': 'barometer', 'windspeedmph': 'wind_speed', 'windgustmph': 'wind_gust', 'solarradiation': 'radiation', 'dewptf': 'dewpoint', 'windchillf': 'windchill', 'yearlyrainin': 'rain_total', # for firmware HP1001 2.2.2 'outhumi': 'out_humidity', 'inhumi': 'in_humidity', 'outtemp': 'out_temperature', 'intemp': 'in_temperature', 'absbaro': 'pressure', 'windspeed': 'wind_speed', 'windgust': 'wind_gust', 'light': 'radiation', 'dewpoint': 'dewpoint', 'windchill': 'windchill', 'rainrate': 'rain_rate', 'yearlyrain': 'rain_total', # for all firmware 'winddir': 'wind_dir', 'UV': 'uv', 'lowbatt': 'battery', } IGNORED_LABELS = ['relbaro', 'dailyrain', 'weeklyrain', 'monthlyrain', 'rainin', 'dailyrainin', 'weeklyrainin', 'monthlyrainin', 'realtime', 'rtfreq', 'action', 'ID', 'PASSWORD', 'dateutc', 'softwaretype'] def __init__(self): self._last_rain = None def parse(self, s): pkt = dict() try: data = _cgi_to_dict(s) # FIXME: add option to use computer time instead of station pkt['dateTime'] = self.decode_wu_datetime( data.pop('dateutc', int(time.time() + 0.5))) pkt['usUnits'] = weewx.US if 'tempf' in data else weewx.METRIC for n in data: if n in self.LABEL_MAP: pkt[self.LABEL_MAP[n]] = self.decode_float(data[n]) elif n in self.IGNORED_LABELS: logdbg("ignored parameter %s=%s" % (n, data[n])) else: loginf("unrecognized parameter %s=%s" % (n, data[n])) # get the rain this period from yearly total if 'rain_total' in pkt: newtot = pkt['rain_total'] if pkt['usUnits'] == weewx.METRIC: newtot /= 10.0 # METRIC wants cm, not mm pkt['rain'] = self._delta_rain(newtot, self._last_rain) self._last_rain = newtot except ValueError, e: logerr("parse failed for %s: %s" % (s, e)) return pkt @staticmethod def map_to_fields(pkt, sensor_map): if sensor_map is None: sensor_map = Observer.Parser.DEFAULT_SENSOR_MAP return Consumer.Parser.map_to_fields(pkt, sensor_map) @staticmethod def decode_float(x): # these stations send a value of -9999 to indicate no value, so # convert that to a proper None. x = Consumer.Parser.decode_float(x) return None if x == -9999 else x # sample output from a LW301 # # mac=XX&id=8e&rid=af&pwr=0&or=0&uvh=0&uv=125&ch=1&p=1 # mac=XX&id=90&rid=9d&pwr=0&gw=0&av=0&wd=315&wg=1.9&ws=1.1&ch=1&p=1 # mac=XX&id=84&rid=20&pwr=0&htr=0&cz=3&oh=90&ttr=0&ot=18.9&ch=1&p=1 # mac=XX&id=82&rid=1d&pwr=0&rro=0&rr=0.00&rfa=5.114&ch=1&p=1 # mac=XX&id=c2&pv=0&lb=0&ac=0®=1803&lost=0000&baro=806&ptr=0&wfor=3&p=1 # mac=XX&id=90&rid=9d&pwr=0&gw=0&av=0&wd=247&wg=1.9&ws=1.1&ch=1&p=1 # mac=XX&id=8e&rid=63&pwr=0&or=0&uvh=0&uv=365&ch=1&p=1 # # observed values for lost: # 0000: ? # 0803: wind, t/h, rain # 1803: wind, t/h, rain, uv # # observed values for wfor: # 0=partly_cloudy, 1=sunny, 2=cloudy, 3=rainy, 4=snowy # # all packets # mac - mac address of the bridge # id - sensor type identifier? # # base station packets (0xc2) # pv - ? samples: 0 # lb - ? samples: 0 # ac - ? samples: 0 # reg - registered sensors? samples: 1803, 1009 # lost - lost contact? samples: 0000 # baro - barometer mbar # ptr - ? samples: 0 # wfor - weather forecast? # # all non-base packets # rid - sensor identifier # pwr - battery status? # ch - channel # # uv sensor (0x8e) # or - ? samples: 0 # uvh - ? samples: 0 # uv - index? what is range? samples: 125, 365 # # wind sensor (0x90) # gw - ? samples: 0 # av - ? samples: 0 # wd - wind direction in compass degrees # wg - wind gust m/s # ws - wind speed m/s # # temperature/humidity sensor (0x84) # htr - ? samples: 0, 1 # cz - ? samples: 3, 2 # oh - humidity % # ttr - ? samples: 0, 1 # ot - temperature C # # rain sensor (0x82) # rro - ? samples: 0 # rr - rain rate? mm/hr # rfa - rain fall accumulated? mm class LW30x(Consumer): def __init__(self, server_address, **stn_dict): super(LW30x, self).__init__( server_address, Consumer.Handler, LW30x.Parser()) class Parser(Consumer.Parser): def __init__(self): self._last_rain = None FLOATS = ['baro', 'ot', 'oh', 'ws', 'wg', 'wd', 'rr', 'rfa', 'uv'] # map database fields to sensor tuples DEFAULT_SENSOR_MAP = { 'barometer': 'baro..*', # FIXME: should this be pressure? 'outTemp': 'ot.*.*', 'outHumidity': 'oh.*.*', 'windSpeed': 'ws.*.*', 'windGust': 'wg.*.*', 'windDir': 'wd.*.*', 'rainRate': 'rr.*.*', 'rain': 'rain.*.*', 'UV': 'uv.*.*'} @staticmethod def parse_identifiers(s): data = _cgi_to_dict(s) return {'sensor_type': data.get('id'), 'channel': data.get('ch'), 'sensor_id': data.get('rid'), 'bridge_id': data.get('mac')} def parse(self, s): pkt = dict() try: data = _cgi_to_dict(s) for n in data: if n in LW30x.Parser.FLOATS: pkt[n] = self.decode_float(data[n]) else: pkt[n] = data[n] except ValueError, e: logerr("parse failed for %s: %s" % (s, e)) # convert accumulated rain to rain delta if 'rfa' in pkt: pkt['rain'] = self._delta_rain(pkt['rfa'], self._last_rain) self._last_rain = pkt['rfa'] # tag each observation with identifiers: # observation.<channel><sensor_id>.<bridge_id> packet = {'dateTime': int(time.time() + 0.5), 'usUnits': weewx.METRICWX} _id = '%s%s.%s' % (pkt.get('ch', ''), pkt.get('rid', ''), pkt.get('mac', '')) for n in pkt: packet["%s.%s" % (n, _id)] = pkt[n] return packet @staticmethod def map_to_fields(pkt, sensor_map): if sensor_map is None: sensor_map = LW30x.Parser.DEFAULT_SENSOR_MAP return Consumer.Parser.map_to_fields(pkt, sensor_map) """ The output from a GW1000U is more complicated that a simply http GET/POST. What follows is the dissection using conventions from mycal. Each request has a header HTTP_IDENTIFY that specifys the request type, gateway identification, and key. For example: HTTP_IDENTIFY: 8009E3A7:00:45A49CAF5B9ED7E2:70 ^^^^^^^^ ^^ ^^^^^^^^^^^^^^^^ ^^ A B C D E A - always 80 (2 characters) B - MAC address less vendor ID (6 characters) C - packet code (2 characters) D - registration code (16 characters) E - packet code (2 characters) Some packets have data, many do not. The packet code C:E is used to identify incoming packet types. Some replies have data, many do not. Each reply includes a HTTP_FLAGS header in the form 00:00. Packet types 00:01 ? 00:10 gateway power up 00:20 gateway unregistered 00:30 gateway finished registration 00:70 gateway ping 01:00 weather station ping 41 46 30 67 39 01:01 weather station data 01:14 weather station registration verification 7f:10 weather station registration 00:14 ? Data packets This is the decoding of the data, based on mycal description: start nyb nybble encoding description 00H 0 2 byte Record type, always 01 01H 2 4 ??? Unknown 03H 6 3 byte status? 04L 9 10 BDC Date/Time of Max Inside Temp 09L 13 10 BCD Date/Time of Min Inside Temp 0eL 1d 3 BCD Max Inside Temp 10H 20 2 ??? Unknown 11H 22 3 BCD Min Inside Temp 12L 25 2 ??? Unknown 13L 27 3 BCD Current Inside Temp 15H 2a 3 ??? Unknown 16L 2d 10 BCD Date/Time of Max Outside Temp 1bL 37 10 BCD Date/Time of Min Outside Temp 20L 41 3 BCD Max Outside Temp 22H 44 2 ??? Unknown 23H 46 3 BCD Min Outside Temp 24L 49 2 ??? Unknown 25L 4b 3 BCD Current Outside Temp 27H 4e 3 ??? Unknown 28L 51 10 BCD Unknown Date/Time 1 2dL 5b 10 BCD Unknown Date/Time 2 32L 65 10 ??? Unknown 37L 6f 3 BCD Copy of outside temp? 39H 72 2 ??? Status byte-per skydvr 0xA0 error 3aH 74 10 BCD Date/Time of Max Inside Humidity 3fH 7e 10 BCD Date/Time of Min Inside Humidity 44H 88 2 binary Max Inside Humidity 45H 8a 2 binary Min Inside Humidity 46H 8c 2 binary Current Inside Humidity 47H 8e 10 BCD Date/Time of Max Outside Humidity 4cH 98 10 BCD Date/Time of Min Outside Humidity 51H a2 2 binary Max Outside Humidity 52H a4 2 binary Min Outside Humidity 53H a6 2 binary Current Outside Humidity 54H a8 18 ??? Unknown all 0s 5dH ba 4 ??? Unknown 5fH be 20 ??? Unknown all 0s 69H d2 2 ??? Unknown 6aH d4 10 BCD Unknown Date/Time 3 6fH de 12 ??? Unknown 75H ea 10 BCD Date/Time last 1-hour rain window ended 7aH f4 13 ??? Unknown 80L 101 10 BCD Date/Time of Last Rain Reset 85L 10b 23 ??? Unknown - skydvr says rainfall array 91H 122 4 binary Current Ave Wind Speed 93H 126 4 ??? Unknown 93H 126 4 ??? Unknown 95H 12a 6 nybbles Wind direction history -- One nybble per time period 98H 130 10 BCD Time of Max Wind Gust 9dH 13a 4 binary Max Wind Gust since reset in 100th of km/h 9fH 13e 2 ??? Unknown a0H 140 4 binary Max Wind Gust this Cycle in 100th of km/h a2H 144 4 ??? Unknown - skydvr says wind status a4H 148 6 nybbles Copy of wind direction history? a7H 14e 1 ??? Unknown a7L 14f 4 BCD Current barometer in inches Hg a9L 153 6 ??? Unknown - skydvr says 0xAA might be pressure delta acL 159 4 BCD Min Barometer aeL 15d 6 ??? Unknown b1L 163 4 BCD Max Barometer b3L 167 5 ??? Unknown b6H 16c 10 BCD Unknown Date/Time 5 bbH 176 10 BCD Unknown Date/Time 6 c0H 180 6 ??? Unknown c3H 186 2 binary Checksum1 c4H 188 2 binary Checksum2 May be one 16-bit checksum """ class GW1000U(Consumer): # values for history interval: # 0x00 - 1 minute # 0x01 - 5 minutes # 0x02 - 10 minutes # 0x03 - 15 minutes (default) # 0x04 - 20 minutes # 0x05 - 30 minutes # 0x06 - 1 hour # 0x07 - 2 hours HISTORY_INTERVALS = { 0: '1m', 1: '5m', 2: '10m', 3: '15m', 4: '20m', 5: '30m', 6: '1h', 7: '2h'} station_serial = '0' * 16 ping_interval = 60 # how often gateway should ping the server, in seconds sensor_interval = 300 # seconds between data packets (5m is default) history_interval = 3 lcd_brightness = 4 server_name = 'box.weatherdirect.com' def __init__(self, server_address, **stn_dict): super(GW1000U, self).__init__( server_address, GW1000U.Handler, GW1000U.Parser()) GW1000U.station_serial = stn_dict.get('serial', '0' * 16) if len(GW1000U.station_serial) != 16: raise weewx.ViolatedPrecondition("serial number must be 16 characters") loginf('using serial number %s' % GW1000U.station_serial) GW1000U.sensor_interval = stn_dict.get('sensor_interval', 300) loginf('using sensor interval %ss' % GW1000U.sensor_interval) GW1000U.history_interval = stn_dict.get('history_interval', 3) if GW1000U.history_interval not in GW1000U.HISTORY_INTERVALS: raise weewx.ViolatedPrecondition("history interval must be 0-7") loginf('using history interval %s (%s)' % (GW1000U.history_interval, GW1000U.HISTORY_INTERVALS.get(GW1000U.history_interval))) @staticmethod def encode_ts(ts): # encode a 12-character time stamp into 6 bytes tstr = time.strftime("%H%M%S%d%m%y", time.localtime(ts)) s = '' for x in range(0, 6): s += chr(GW1000U.encode_bcd(tstr[x*2: x*2+2])) return s @staticmethod def decode_serial(data): return binascii.hexlify(data) @staticmethod def encode_serial(sn): # encode a 16-character serial number into 8 bytes return binascii.unhexlify(sn) @staticmethod def encode_bcd(x): x = int(x) msb = x / 10 lsb = x % 10 if msb > 10: msb = 10 return ((msb << 4) | (lsb & 0xf)) class Handler(Consumer.Handler): last_history_address = 0 def handle(self): Consumer.Handler.handle(self) flags = '00:00' response = '' parts = self.headers.get('HTTP_IDENTIFY', '').split(':') if len(parts) == 4: (mac, id1, key, id2) = parts pkt_type = ("%s:%s" % (id1, id2)).upper() length = int(self.headers.get('Content-Length', 0)) data = self.rfile.read(length) if length else '' logdbg("recv: %s:%s %s %s %s" % (id1, id2, mac, key, self._fmt_bytes(data))) if pkt_type == '00:10': # power up for unregistered gateway flags = '10:00' loginf("power up from gateway with mac %s" % mac) elif pkt_type == '00:20': # push button registration flags = '20:00' # sometimes replies with 20:01 response = self._create_gateway_reg_response() loginf("registration from gateway with mac %s" % mac) elif pkt_type == '00:30': # received after response to 00:70 packet flags = '30:00' elif pkt_type == '00:70': # gateway ping flags = '70:00' response = self._create_gateway_ping_response() elif pkt_type == '7F:10': # station registration. station sends its serial number # as the first 8 digits of the packet. if it is the # default serial number, there should be 13 bytes. ignore # requests from anything other than the known serial. if data and len(data) >= 8: sn = GW1000U.decode_serial(data[0:8]) if sn == GW1000U.station_serial: flags = '14:00' response = self._create_station_reg_response() else: loginf("ignore registration from serial %s" % sn) else: loginf('cannot extract serial from packet: %s' % self._fmt_bytes(data)) elif pkt_type == '00:14': # reply after 7f:10 packet. station sends 14 bytes. flags = '1C:00' elif pkt_type == '01:14': # station sends 14 bytes of data. data is new serial in # same format as 7f:10 with one extra byte on the end. flags = '1C:00' elif pkt_type == '01:00': # weather station ping. station sends 5 bytes. flags = '14:01' response = self._create_station_ping_response() elif pkt_type == '01:01': # data packet - current or history if len(data) == 197: Consumer.queue.put({'mac': mac, 'data': binascii.b2a_hex(data)}) else: loginf('ignore data packet: unexpected length %s' % len(data)) else: loginf("unknown packet type %s" % pkt_type) elif 'HTTP_IDENTIFY' not in self.headers: logdbg('no HTTP_IDENTIFY in headers') else: logdbg("unknown format for HTTP_IDENTIFY: '%s'" % self.headers.get('HTTP_IDENTIFY', '')) logdbg("send: %s %s" % (flags, self._fmt_bytes(response))) self.send_response(200) self.send_header('HTTP_FLAGS', flags) self.send_header('Server', 'Microsoft-II/6.0') self.send_header('X-Powered-By', 'ASP.NET') self.send_header('X-ApsNet-Version', '2.0.50727') self.send_header('Cache-Control', 'private') self.send_header('Content-Length', len(response)) self.send_header('Content-Type', 'application/octet-stream') self.end_headers() self.wfile.write(response) @staticmethod def _create_gateway_reg_response(): server = GW1000U.server_name return ''.join( [chr(0) * 8, # used to generate a new key server.ljust(0x98, chr(0)), ("%s%s%s" % (server, chr(0), server)).ljust(0x56, chr(0)), chr(0) * 5, chr(0xff)]) @staticmethod def _create_gateway_ping_response(): # 18-byte reply. last two bytes are the ping interval in seconds. interval = GW1000U.ping_interval hi = interval / 256 lo = interval % 256 return ''.join([chr(0xff) * 4, chr(0) * 12, chr(hi), chr(lo)]) # FIXME: the reg_response and ping_response look awfully similar. # can they be replaced with a single response? @staticmethod def _create_station_reg_response(): # reply to station registration request with 38 bytes of data. # this reply can set the serial number of the weather station if # the station has the default serial number of 0102030405060708. # once changed, the serial number cannot be modified, so it might # be advisable to register with lacrosse first so that if you ever # want to go back to the lacross service you could. sn = GW1000U.station_serial payload = ''.join( [chr(1), GW1000U.encode_serial(sn), # 8 bytes chr(0) + chr(0x30) + chr(0) + chr(0xf) + chr(0) + chr(0) + chr(0) + chr(0xf) + chr(0) + chr(0) + chr(0) + chr(0x77) + chr(0), chr(0xe) + chr(0xff), # skydriver calls this epoch GW1000U.encode_ts(int(time.time())), # 6 bytes chr(0x53), chr(0x7), # unknown chr(GW1000U.lcd_brightness), # LCD brightness chr(0) + chr(0), # beep weather station chr(0), # unknown chr(0x7)]) # unknown - 0x7 is from lacrosse alerts cs = GW1000U.Handler.checksum8(payload) return payload + chr(cs) @staticmethod def _create_station_ping_response(): # reply with 38 bytes of data sn = GW1000U.station_serial hi = GW1000U.Handler.last_history_address / 256 lo = GW1000U.Handler.last_history_address % 256 interval = GW1000U.sensor_interval / 60 payload = ''.join( [chr(1), GW1000U.encode_serial(sn), # 8 bytes chr(0) + chr(0x32) + chr(0) + chr(0xb) + chr(0) + chr(0) + chr(0) + chr(0xf) + chr(0) + chr(0) + chr(0), chr(interval - 1), # byte 0x14 (0x3) chr(0), chr(hi) + chr(lo), # last_history_address 2 bytes (0x3e 0xde) GW1000U.encode_ts(int(time.time())), # 6 bytes chr(0x53), chr(GW1000U.history_interval), # byte 0x1f (0x7) chr(GW1000U.lcd_brightness), # byte 0x20 (0x4) chr(0) + chr(0), chr(0)]) cs = GW1000U.Handler.checksum16(payload) + 7 return payload + chr(cs >> 8) + chr(cs & 0xff) @staticmethod def checksum8(x): n = 0 for c in x: n += int(c, 16) return n & 0xff @staticmethod def checksum16(x): n = 0 for c in x: n += ord(c) return n & 0xffff @staticmethod def _fmt_bytes(data): return ' '.join(['%02x' % ord(x) for x in data]) class Parser(Consumer.Parser): # map database fields to sensor identifier tuples DEFAULT_SENSOR_MAP = { 'barometer': 'barometer..*', 'inTemp': 'in_temperature..*', 'outTemp': 'out_temperature..*', 'inHumidity': 'in_humidity..*', 'outHumidity': 'out_humidity..*', 'windSpeed': 'wind_speed..*', 'windGust': 'wind_gust..*', 'windDir': 'wind_dir..*', 'rain': 'rain..*', 'rxCheckPercent': 'rf_signal_strength..*'} def __init__(self): self._last_rain = None @staticmethod def parse_identifiers(payload): return {'bridge_id': payload.get('mac')} def parse(self, payload): mac = payload.get('mac') s = payload.get('data', '') # this expects a string of hex characters. the data packet length # is 197, so the hex string should be 394 characters. pkt = dict() if len(s) != 394: return pkt pkt['record_type'] = int(s[0:2], 16) # always 01 pkt['rf_signal_strength'] = int(s[2:4], 16) # % pkt['status'] = s[4:6] # 0x10, 0x20, 0x30 pkt['forecast'] = s[6:8] # 0x11, 0x12, 0x20, 0x21 pkt['in_temperature'] = self.to_temperature(s, 39) # C pkt['out_temperature'] = self.to_temperature(s, 75) # C ok = int(s[114], 16) == 0 # 0=ok, 0xa=err pkt['windchill'] = self.to_temperature(s, 111) if ok else None # C pkt['in_humidity'] = self.to_hum(s, 140) # % pkt['out_humidity'] = self.to_hum(s, 166) # % pkt['rain_count'] = self.to_rainfall(s, 267) / 10.0 # cm pkt['rain'] = self._delta_rain(pkt['rain_count'], self._last_rain) self._last_rain = pkt['rain_count'] ok = int(s[297], 16) == 0 # 0=ok, 5=err if ok: pkt['wind_speed'] = self.to_windspeed(s, 290) # kph pkt['wind_dir'] = self.to_winddir(s, 298) # degrees pkt['wind_gust'] = self.to_windspeed(s, 320) # kph else: pkt['wind_speed'] = None pkt['wind_dir'] = None pkt['wind_gust'] = None pkt['barometer'] = self.to_pressure(s, 339) # mbar # now tag each value with identifiers packet = {'dateTime': int(time.time() + 0.5), 'usUnits': weewx.METRIC} for n in pkt: packet["%s..%s" % (n, mac)] = pkt[n] return packet @staticmethod def map_to_fields(pkt, sensor_map): if sensor_map is None: sensor_map = GW1000U.Parser.DEFAULT_SENSOR_MAP return Consumer.Parser.map_to_fields(pkt, sensor_map) @staticmethod def to_temperature(x, idx): # returns temperature in degree C s = x[idx:idx+3] if s.lower() == 'aaa' or s.lower() == 'aa3': return None return GW1000U.Parser.bcd2int(s) / 10.0 - 40.0 @staticmethod def to_hum(x, idx): # returns humidity in percent return GW1000U.Parser.bcd2int(x[idx:idx+2]) @staticmethod def to_windspeed(x, idx): # returns windspeed in km per hour return GW1000U.Parser.bin2int(x[idx:idx+4]) / 100.0 @staticmethod def to_winddir(x, idx): # returns compass degrees in [0,360] return int(x[idx:idx+1], 16) * 22.5 @staticmethod def to_pressure(x, idx): # returns barometric pressure in mbar return GW1000U.Parser.bcd2int(x[idx:idx+5]) / 10.0 @staticmethod def to_rainfall(x, idx, n=7): # each tip is 0.01", returns rain total in mm v = GW1000U.Parser.bcd2int(x[idx:idx+n]) if n == 6: v /= 100.0 else: v /= 1000.0 return v @staticmethod def bcd2int(x): v = 0 for y in x: v = v * 10 + int(y) return v @staticmethod def bin2int(x): v = 0 for y in x: v = (v << 4) + int(y, 16) return v class InterceptorConfigurationEditor(weewx.drivers.AbstractConfEditor): @property def default_stanza(self): return """ [Interceptor] # This section is for the network traffic interceptor driver. # Specify the hardware device to capture. Options include: # acurite-bridge - acurite internet bridge # observer - fine offset WH2600/HP1000/HP1003, aka 'observer' # lw30x - oregon scientific LW301/LW302 # lacrosse-bridge - lacrosse GW1000U/C84612 internet bridge device_type = acurite-bridge # The driver to use: driver = user.interceptor """ def prompt_for_settings(self): print "Specify the type of device whose data will be captured" device_type = self._prompt('device_type', 'acurite-bridge', ['acurite-bridge', 'observer', 'lw30x', 'lacrosse-bridge']) return {'device_type': device_type} class InterceptorDriver(weewx.drivers.AbstractDevice): DEVICE_TYPES = { 'acurite-bridge': AcuriteBridge, 'observer': Observer, 'observerip': Observer, 'lw30x': LW30x, 'lacrosse-bridge': GW1000U} def __init__(self, **stn_dict): loginf('driver version is %s' % DRIVER_VERSION) addr = stn_dict.get('address', DEFAULT_ADDR) port = int(stn_dict.get('port', DEFAULT_PORT)) loginf('driver will listen on %s:%s' % (addr, port)) self._obs_map = stn_dict.get('sensor_map', None) loginf('sensor map: %s' % self._obs_map) self._device_type = stn_dict.get('device_type', 'acurite-bridge') if not self._device_type in self.DEVICE_TYPES: raise Exception("unsupported device type '%s'" % self._device_type) self._device = self.DEVICE_TYPES.get(self._device_type)( (addr, port), **stn_dict) self._server_thread = threading.Thread(target=self._device.run_server) self._server_thread.setDaemon(True) self._server_thread.setName('ServerThread') self._server_thread.start() def closePort(self): loginf('shutting down server thread') self._device.shutdown() self._server_thread.join(20.0) if self._server_thread.isAlive(): logerr('unable to shut down server thread') def hardware_name(self): return self._device_type def genLoopPackets(self): while True: try: data = self._device.get_queue().get(True, 10) logdbg('raw data: %s' % data) pkt = self._device.parser.parse(data) logdbg('raw packet: %s' % pkt) pkt = self._device.parser.map_to_fields(pkt, self._obs_map) logdbg('mapped packet: %s' % pkt) if pkt and 'dateTime' in pkt and 'usUnits' in pkt: yield pkt else: logdbg("skipping bogus packet %s ('%s')" % (pkt, data)) except Queue.Empty: logdbg('empty queue') # define a main entry point for determining sensor identifiers. # invoke this as follows from the weewx root dir: # # PYTHONPATH=bin python bin/user/interceptor.py if __name__ == '__main__': import optparse usage = """%prog [options] [--debug] [--help]""" syslog.openlog('interceptor', syslog.LOG_PID | syslog.LOG_CONS) syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_INFO)) parser = optparse.OptionParser(usage=usage) parser.add_option('--version', dest='version', action='store_true', help='display driver version') parser.add_option('--debug', dest='debug', action='store_true', default=False, help='display diagnostic information while running') parser.add_option('--port', dest='port', metavar='PORT', type=int, default=DEFAULT_PORT, help='port on which to listen') parser.add_option('--address', dest='addr', metavar='ADDRESS', default=DEFAULT_ADDR, help='address on which to bind') parser.add_option('--interface', dest='iface', metavar='INTERFACE', default=DEFAULT_IFACE, help='interface on which to sniff') parser.add_option('--sniff', dest='sniff', action='store_true', default=False, help='interface on which to sniff') parser.add_option('--device', dest='device_type', metavar='DEVICE_TYPE', default=DEFAULT_DEVICE_TYPE, help='type of device for which to listen') (options, args) = parser.parse_args() debug = False if options.debug: syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG)) debug = True if not options.device_type in InterceptorDriver.DEVICE_TYPES: raise Exception("unsupported device type '%s'. options include %s" % (options.device_type, ', '.join(InterceptorDriver.DEVICE_TYPES.keys()))) device = InterceptorDriver.DEVICE_TYPES.get(options.device_type)( (options.addr, int(options.port)), options) server_thread = threading.Thread(target=device.run_server) server_thread.setDaemon(True) server_thread.setName('ServerThread') server_thread.start() while True: try: _data = device.get_queue().get(True, 10) print 'identifiers:', device.parser.parse_identifiers(_data) if debug: print 'raw data: %s' % _data _pkt = device.parser.parse(_data) print 'raw packet: %s' % _pkt _pkt = device.parser.map_to_fields(_pkt, None) print 'mapped packet: %s' % _pkt except Queue.Empty: logdbg("empty queue")
