Finally got a chance to check out the driver (latest commit 2ad77b7
<https://github.com/matthewwall/weewx-interceptor/commit/2ad77b771db76b46858e04750a9f902f113ecaa8>),
had to make some changes to get it to run at all.
1. I see you had to change from filter to pcap_filter
1. The weewx.conf template still references filter
2. The standalone version of the program (__main__) does as well
2. class SniffServer had references to itself in its init when
referencing SniffServer.SNAPLEN, SniffServer.PROMISCUOUS,
SniffServer.TIMEOUT_MSEnter code here...
1. Changed these to self.SNAPLEN, self.PROMISCUOUS, self.TIMEOUT_MS
3. The two logdbg statements in decode_ip_packet are causing problems...
some of the values arent strings.
1. I had no solution... just commented them out for now.
With those changes it seems to be running great now
On Tuesday, November 8, 2016 at 7:07:54 AM UTC-6, Jerome Helbert wrote:
>
> also should mention that python-libpcap is dependent on the libpcap C
> libraries.
>
> On Sunday, November 6, 2016 at 10:11:15 AM UTC-6, Jerome Helbert wrote:
>>
>> I looked over the commit, looks good. I will try to get a chance to pull
>> it down and try it out later today.
>>
>> When I was first writing this stuff into the hackulink driver (and every
>> time I would do a system reinstall) I found there a range of python pcap
>> modules, and they had APIs that all varied. I would recommend adding the
>> specific python module that I called out in my other thread to make
>> installs easier.
>>
>> On Sunday, November 6, 2016 at 8:54:14 AM UTC-6, mwall wrote:
>>>
>>> On Saturday, November 5, 2016 at 10:24:08 PM UTC-4, Jerome Helbert wrote:
>>>>
>>>> I posted a modified version of the interceptor a few weeks ago, I used
>>>> the libpcap libraries to sniff the data stream directly (no need to run
>>>> tcpdump or ngrep or any of that external to the driver.
>>>>
>>>>>
>>>>>>>>>>
>>> jerome,
>>>
>>> i merged your pcap changes into the interceptor driver at commit 59f09a6
>>> (driver version 0.15). do you mind giving it a try?
>>>
>>> you will need a configuration something like this:
>>>
>>> [Interceptor]
>>> driver = user.interceptor
>>> device_type = acurite-bridge
>>> mode = sniff
>>> iface = eth0
>>> filter = src host X && dst port Y and greater 61
>>>
>>> plus a sensor map if you have anything beyond just a single 5n1 sensor
>>> cluster.
>>>
>>> the implementation should be general enough to work with the os lw30x
>>> and fine offset hardware too, but those will probably need some adjustments
>>> to the pcap data parsing.
>>>
>>> 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.
SniffServer vs TCPServer
The driver can obtain packets by sniffing network traffic using pcap, or by
listening for TCP/IP requests. The pcap approach requires the python pcap
module - a separate installation on most platforms.
To run a listener, specify an address and port. This is the default mode.
For example:
[Interceptor]
mode = listen
address = localhost
port = 9999
To run a sniffer, specify an interface and filter. For example:
[Interceptor]
mode = sniff
iface = eth0
filter = src host 192.168.1.5 && dst port 80
"""
# FIXME: automatically detect the traffic type
# FIXME: default acurite mapping confuses multiple tower sensors
from __future__ import with_statement
import BaseHTTPServer
import SocketServer
import Queue
import binascii
import calendar
import fnmatch
import syslog
import threading
import time
import urlparse
import weewx.drivers
DRIVER_NAME = 'Interceptor'
DRIVER_VERSION = '0.15'
DEFAULT_ADDR = ''
DEFAULT_PORT = 80
DEFAULT_IFACE = 'eth0'
DEFAULT_FILTER = 'dst port 80'
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):
if '=' in s:
return dict([y.strip() for y in x.split('=')] for x in s.split('&'))
return dict()
class Consumer(object):
"""The Consumer contains two primary parts - a Server and a Parser. The
Server can be a sniff server or a TCP server. Either type of server
is a data sink. When it receives data, it places a string on a queue.
The driver then pops items of the queue and hands them over to the parser.
The Parser processes each string and spits out a dictionary that contains
the parsed data.
The handler is only used by the TCP server. It provides the response to
the client requests.
Sniffing is not available for every type of hardware.
"""
queue = Queue.Queue()
def __init__(self, parser, mode='listen',
address=DEFAULT_ADDR, port=DEFAULT_PORT, handler=None,
iface=DEFAULT_IFACE, pcap_filter=DEFAULT_FILTER):
self.parser = parser
loginf("mode is %s" % mode)
if mode == 'sniff':
self._server = Consumer.SniffServer(iface, pcap_filter)
elif mode == 'listen':
self._server = Consumer.TCPServer(address, port, handler)
else:
raise Exception("unrecognized mode '%s'" % mode)
def run_server(self):
self._server.run()
def stop_server(self):
self._server.stop()
self._server = None
def get_queue(self):
return Consumer.queue
class Server(object):
def run(self):
pass
def stop(self):
pass
class SniffServer(Server):
SNAPLEN = 1600
PROMISCUOUS = 0
TIMEOUT_MS = 100
def __init__(self, iface, pcap_filter):
import pcap
self.packet_sniffer = pcap.pcapObject()
loginf("sniff iface %s" % iface)
self.packet_sniffer.open_live(
iface, self.SNAPLEN, self.PROMISCUOUS,
self.TIMEOUT_MS)
loginf("sniff filter '%s'" % pcap_filter)
self.packet_sniffer.setfilter(pcap_filter, 0, 0)
self.running = False
self.reassembled_string = ''
self.sniff_active = False
def run(self):
self.running = True
while self.running:
self.packet_sniffer.dispatch(1, self.decode_ip_packet)
def stop(self):
self.running = False
self.packet_sniffer.close()
self.packet_sniffer = None
def decode_ip_packet(self, _pktlen, data, _timestamp):
if data:
# logdbg("sniff: pktlen=%s timestamp=%s data=%s" %
# (_pktlen, _timestamp, _obfuscate_passwords(data)))
if data[12:14] == '\x08\x00':
header_len = ord(data[14]) & 0x0f
_data = data[4 * header_len + 34:]
# logdbg("sniff: header_len=%s _data=%s" %
# (header_len, _obfuscate_passwords(_data)))
if 'GET' in _data:
self.reassembled_string = _data
self.sniff_active = True
elif 'HTTP' in data and self.sniff_active:
self.sniff_active = False
data = urlparse.urlparse(self.reassembled_string).query
logdbg("SNIFF: %s" % _obfuscate_passwords(data))
Consumer.queue.put(data)
elif self.sniff_active:
self.reassembled_string += _data
class TCPServer(Server, SocketServer.TCPServer):
daemon_threads = True
allow_reuse_address = True
def __init__(self, address, port, handler):
if handler is None:
handler = Consumer.Handler
loginf("listen on %s:%s" % (address, port))
SocketServer.TCPServer.__init__(self, (address, int(port)), handler)
def run(self):
self.serve_forever()
def stop(self):
self.shutdown()
self.server_close()
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):
# use glob matching for parts of the tuple
matches = fnmatch.filter([value], pattern)
return True if matches else 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 rain
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
# resulting raw packet format:
# <observation_name>.<sensor>.<id> : value
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, **stn_dict):
super(AcuriteBridge, self).__init__(
AcuriteBridge.Parser(), handler=AcuriteBridge.Handler, **stn_dict)
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. this map should work
# out-of-the-box for either wu format or chaney format, with a basic
# set of sensors. if there are more than one remote sensor then a
# custom sensor map is necessary to avoid confusion of outputs.
DEFAULT_SENSOR_MAP = {
# wu format uses barometer in every packet
'barometer': 'barometer.?*.*',
# chaney format uses barometer in bridge packets only
'pressure': 'pressure..*',
# both formats
'inTemp': 'temperature_in.*.*',
'inHumidity': 'humidity_in.*.*',
'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_in',
'indoortempf': 'temperature_in',
'ptempf': 'temperature_probe',
'baromin': 'barometer',
'windspeedmph': 'windspeed',
'winddir': 'winddir',
'dailyrainin': 'rainfall'
# WARNING: since rainfall is obtained from dailyrainin, there
# will be a counter wraparound at 00:00 each day.
}
IGNORED_LABELS = ['rainin', 'dewptf',
'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):
pkt = dict()
if '=' in s:
if s.find('action') >= 0:
pkt = self.parse_wu(s)
else:
pkt = self.parse_chaney(s)
return pkt
# parse packets that are in the weather underground -ish 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))
# convert rainfall to a delta
if 'rainfall' in pkt:
rain_total = pkt['rainfall']
pkt['rainfall'] = self._delta_rain(rain_total, self._last_rain)
self._last_rain = rain_total
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
if '=' not in x:
loginf("unexpected un-assigned variable '%s'" % 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_in'] = 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
# resulting raw packet format:
# <observation_name> : value
class Observer(Consumer):
def __init__(self, **stn_dict):
super(Observer, self).__init__(Observer.Parser(), **stn_dict)
class Parser(Consumer.Parser):
# map database fields to observation names
DEFAULT_SENSOR_MAP = {
'pressure': 'pressure',
'barometer': 'barometer',
'outHumidity': 'humidity_out',
'inHumidity': 'humidity_in',
'outTemp': 'temperature_out',
'inTemp': 'temperature_in',
'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': 'humidity_out',
'indoorhumidity': 'humidity_in',
'tempf': 'temperature_out',
'indoortempf': 'temperature_in',
'baromin': 'barometer',
'windspeedmph': 'wind_speed',
'windgustmph': 'wind_gust',
'solarradiation': 'radiation',
'dewptf': 'dewpoint',
'windchillf': 'windchill',
'yearlyrainin': 'rain_total',
# for firmware HP1001 2.2.2
'outhumi': 'humidity_out',
'inhumi': 'humidity_in',
'outtemp': 'temperature_out',
'intemp': 'temperature_in',
'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
# resulting raw packet format:
# <observation_name>.<ch><rid>.<mac> : value
class LW30x(Consumer):
def __init__(self, **stn_dict):
super(LW30x, self).__init__(LW30x.Parser(), **stn_dict)
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
"""
# resulting raw packet format:
# <observation_name>..<mac> : value
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, **stn_dict):
stn_dict['mode'] = 'listen' # sniffing not supported for this hardware
super(GW1000U, self).__init__(
GW1000U.Parser(), handler=GW1000U.Handler, **stn_dict)
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': 'temperature_in..*',
'outTemp': 'temperature_out..*',
'inHumidity': 'humidity_in..*',
'outHumidity': 'humidity_out..*',
'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['temperature_in'] = self.to_temperature(s, 39) # C
pkt['temperature_out'] = 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['humidity_in'] = self.to_hum(s, 140) # %
pkt['humidity_out'] = self.to_hum(s, 166) # %
pkt['rain_total'] = self.to_rainfall(s, 267) / 10.0 # cm
pkt['rain'] = self._delta_rain(pkt['rain_total'], self._last_rain)
self._last_rain = pkt['rain_total']
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.
# The driver to use:
driver = user.interceptor
# 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
# For acurite, fine offset, and oregon scientific hardware, the driver
# can sniff packets directly or run a socket server that listens for
# connections. Packet sniffing requires the installation of the pcap
# python module. The default mode is to listen using a socket server.
# Options are 'listen' and 'sniff'.
#mode = sniff
# When listening, specify at least a port on which to bind.
#address = 127.0.0.1
#port = 80
# When sniffing, specify a network interface and a pcap filter.
#iface = eth0
#filter = src 192.168.4.12 and dst port 80
# Specify a sensor map to associate sensor observations with fields in
# the database. This is most appropriate for hardware that supports
# a variable number of sensors. The values in the tuple on the right
# side are hardware-specific, but follow the pattern:
#
# <observation_name>.<hardware_id>.<bridge_id>
#
#[[sensor_map]]
# inTemp = temperature_in.*.*
# inHumidity = humidity_in.*.*
# outTemp = temperature.?*.*
# outHumidity = humidity.?*.*
"""
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)
stn_dict.pop('driver')
self._device_type = stn_dict.pop('device_type', 'acurite-bridge')
if not self._device_type in self.DEVICE_TYPES:
raise Exception("unsupported device type '%s'" % self._device_type)
loginf('device type: %s' % self._device_type)
self._obs_map = stn_dict.pop('sensor_map', None)
loginf('sensor map: %s' % self._obs_map)
self._device = self.DEVICE_TYPES.get(self._device_type)(**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_server()
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('--mode', dest='mode', metavar='MODE',
default='listen',
help='how to capture traffic: listen or sniff')
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('--iface', dest='iface', metavar='IFACE',
default=DEFAULT_IFACE,
help='network interface to sniff')
parser.add_option('--filter', dest='filter', metavar='FILTER',
default=DEFAULT_FILTER,
help='pcap filter for sniffing')
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)(
mode=options.mode,
iface=options.iface, pcap_filter=options.filter,
address=options.addr, port=options.port)
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")