john,
please try the attached wlink 0.12rc1.py
you will have to set the bucket size in the configuration like this:
[WeatherLink]
...
rain_bucket_size = 1 # 0=0.01inch; 1=0.2mm; 2=0.1mm
please let us know how it goes (you can thank tom for the solution - i just
pinched the code from the vantage driver :)
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/python
# $Id: wlink.py 1558 2016-09-27 16:00:46Z mwall $
# Copyright 2014 Matthew Wall
"""weewx driver for WeatherLink web sites
download data from a weatherlink site for use in weewx
To use this driver:
1) copy this file to the weewx user directory
cp wlink.py /home/weewx/bin/user
2) configure weewx.conf
[Station]
...
station_type = WeatherLink
[WeatherLink]
username = USERNAME
password = PASSWORD
driver = user.wlink
"""
# FIXME: there are unresolved timezone issues in this code. if you grab data
# from a weatherlink site that is in a time zone other than the one in which
# you are running weewx, you will have problems. the timestamps coming from
# weatherlink are local to the weather station. the 'loop' data have timestamp
# in the timezone of the machine on which weewx is running.
# FIXME: the archive records from weatherlink often have unusable timestamps.
# the last few records will often have datestamp of 0xff and timestamp of 0xff.
# for now we just skip these, since there is no time information we can use.
# FIXME: the web page parsing is really, really crude. it will break if there
# are any significant changes to the weatherlink web page structure.
# unfortunately the weatherlink web page is really primitive and gives us
# no structural hints as to the content.
from __future__ import with_statement
import httplib
import re
import socket
import struct
import syslog
import time
import urllib2
from HTMLParser import HTMLParser
import weewx
import weewx.drivers
DRIVER_NAME = "WeatherLink"
DRIVER_VERSION = "0.12rc1"
if weewx.__version__ < "3":
raise weewx.UnsupportedFeature("weewx 3 is required, found %s" %
weewx.__version__)
def logmsg(dst, msg):
syslog.syslog(dst, 'wlink: %s' % msg)
def logdbg(msg):
logmsg(syslog.LOG_DEBUG, msg)
def loginf(msg):
logmsg(syslog.LOG_INFO, msg)
def logcrt(msg):
logmsg(syslog.LOG_CRIT, msg)
def logerr(msg):
logmsg(syslog.LOG_ERR, msg)
def logdev(msg):
# print msg
pass
def loader(config_dict, engine):
return WeatherLink(**config_dict['WeatherLink'])
class WeatherLink(weewx.drivers.AbstractDevice):
"""weewx driver to download data from WeatherLink web sites
Parse the web page for 'loop' data. This happens every 60 seconds.
There are two formats:
A - pre-2015; uses div and span within td
B - end of 2015; elimited divs and inline styles
For archive data, make a request to the server.
"""
rain_bucket_dict = {0:'0.01 inches', 1:'0.2 mm', 2:'0.1 mm'}
def __init__(self, **stn_dict):
loginf("version is %s" % DRIVER_VERSION)
self.username = stn_dict['username']
self.password = stn_dict['password']
self.format = stn_dict.get('format', 'B')
loginf("expecting HTML format '%s'" % self.format)
self.poll_interval = float(stn_dict.get('poll_interval', 60))
loginf("polling interval is %s" % self.poll_interval)
rain_bucket_type = int(stn_dict.get('rain_bucket_type', 0))
loginf("rain bucket type is %d (%s)" %
(rain_bucket_type, self.rain_bucket_dict[rain_bucket_type]))
self.max_tries = int(stn_dict.get('max_tries', 5))
self.retry_wait = int(stn_dict.get('retry_wait', 30))
self.raw_data_cache = stn_dict.get('raw_data_cache', None)
self._archive_interval = stn_dict.get('archive_interval', None)
if self._archive_interval is not None:
self._archive_interval = int(self._archive_interval)
loginf("archive interval is %s seconds" % self._archive_interval)
if rain_bucket_type = 1:
_archive_map['rain'] = _archive_map['rainRate'] = _bucket_1
elif rain_bucket_type == 2:
_archive_map['rain'] = _archive_map['rainRate'] = _bucket_2
else:
_archive_map['rain'] = _archive_map['rainRate'] = _val100
@property
def hardware_name(self):
return "WeatherLink"
@property
def archive_interval(self):
# weewx wants the archive interval in seconds
if self._archive_interval is None:
self._archive_interval = self.download_archive_interval() * 60
return self._archive_interval
def genLoopPackets(self):
while True:
data = self.get_data("http://www.weatherlink.com/user/%s/index.php"
"?view=main&headers=0&type=2" % self.username)
packet = {
'dateTime': int(time.time() + 0.5),
'usUnits': weewx.US, # type 2 is US imperial
}
if self.raw_data_cache is not None:
with open(self.raw_data_cache, "w") as f:
f.write(data)
packet.update(self.parse_page(data))
yield packet
time.sleep(self.poll_interval)
def genArchiveRecords(self, since_ts):
if since_ts is None:
since_ts = 0
ts = _epoch_to_timestamp(since_ts)
data = self.get_data("http://weatherlink.com/webdl.php"
"?timestamp=%s&user=%s&pass=%s&action=data" %
(ts, self.username, self.password))
if data is None:
return
logdbg("found %s archive records since %s" %
((len(data) / 52), since_ts))
sidx = 0
while sidx < len(data):
record = self.unpack_archive_packet(data[sidx:sidx + 52])
if record['dateTime']:
yield record
sidx += 52
def get_data(self, url):
for count in range(self.max_tries):
try:
response = urllib2.urlopen(url)
data = response.read()
return data
except (urllib2.URLError, socket.error,
httplib.BadStatusLine, httplib.IncompleteRead), e:
logerr('download failed attempt %d of %d: %s' %
(count + 1, self.max_tries, e))
time.sleep(self.retry_wait)
else:
logerr('download failed after %d tries' % self.max_tries)
return None
def parse_page(self, text):
"""parse weather data from a weatherlink web page"""
packet = {}
if text is not None:
parser = WLParserB()
if self.format == 'A':
parser = WLParserA()
parser.feed(text)
packet = parser.get_data()
return packet
def download_archive_interval(self):
logdbg('downloading archive interval')
data = self.get_data("http://weatherlink.com/webdl.php"
"?timestamp=0&user=%s&pass=%s&action=headers" %
(self.username, self.password))
if data is None:
raise weewx.WeeWxIOError("download of archive interval failed")
for line in data.splitlines():
if line.find('ArchiveInt=') >= 0:
logdbg('found archive interval: %s' % line)
return int(line[11:])
raise weewx.WeeWxIOError("cannot determine archive interval from %s" %
data)
# adapted from the implementation in the vantage driver
def unpack_archive_packet(self, raw_record_string):
packet_type = ord(raw_record_string[42])
if packet_type == 0xff:
archive_format = rec_fmt_A
data_types = rec_types_A
elif packet_type == 0x00:
archive_format = rec_fmt_B
data_types = rec_types_B
else:
raise weewx.UnknownArchiveType("Unknown archive type: 0x%x" %
(packet_type,))
data_tuple = archive_format.unpack(raw_record_string)
raw_record = dict(zip(data_types, data_tuple))
packet = {
'dateTime': _archive_datetime(raw_record['date_stamp'],
raw_record['time_stamp']),
'usUnits': weewx.US,
}
for t in raw_record:
func = _archive_map.get(t)
if func:
packet[t] = func(raw_record[t])
if packet['windSpeed'] is None or packet['windSpeed'] == 0:
packet['windDir'] = None
packet['interval'] = self.archive_interval / 60
return packet
# =============================================================================
# Decoding routines
# =============================================================================
def _epoch_to_timestamp(epoch):
"""convert unix epoch to davis timestamp"""
tt = time.localtime(epoch)
ds = tt[2] + (tt[1] << 5) + ((tt[0] - 2000) << 9)
ts = tt[3] * 100 + tt[4]
x = (ds << 16) | ts
return x
def _archive_datetime(datestamp, timestamp):
"""Returns the epoch time of the archive packet."""
try:
time_tuple = ((0xfe00 & datestamp) >> 9, # year
(0x01e0 & datestamp) >> 5, # month
(0x001f & datestamp), # day
timestamp // 100, # hour
timestamp % 100, # minute
0, # second
0, 0, -1) # have OS guess DST
ts = int(time.mktime(time_tuple))
except (OverflowError, ValueError, TypeError):
logerr("cannot make timestamp: ds=%s ts=%s" % (datestamp, timestamp))
ts = None
return ts
def _stime(v):
h = v/100
m = v%100
# Return seconds since midnight
return 3600*h + 60*m
def _big_val(v):
return float(v) if v != 0x7fff else None
def _big_val10(v):
return float(v)/10.0 if v != 0x7fff else None
def _big_val100(v):
return float(v)/100.0 if v != 0xffff else None
def _val100(v):
return float(v)/100.0
def _val1000(v):
return float(v)/1000.0
def _val1000Zero(v):
return float(v)/1000.0 if v != 0 else None
def _little_val(v):
return float(v) if v != 0x00ff else None
def _little_val10(v):
return float(v)/10.0 if v != 0x00ff else None
def _little_temp(v):
return float(v-90) if v != 0x00ff else None
def _null(v):
return v
def _null_float(v):
return float(v)
def _null_int(v):
return int(v)
def _windDir(v):
return float(v) * 22.5 if v!= 0x00ff else None
# Rain bucket type "1", a 0.2 mm bucket
def _bucket_1(v):
return float(v)*0.00787401575
def _bucket_1_None(v):
return float(v)*0.00787401575 if v != 0xffff else None
# Rain bucket type "2", a 0.1 mm bucket
def _bucket_2(v):
return float(v)*0.00393700787
def _bucket_2_None(v):
return float(v)*0.00393700787 if v != 0xffff else None
_archive_map={'barometer' : _val1000Zero,
'inTemp' : _big_val10,
'outTemp' : _big_val10,
'highOutTemp' : lambda v : float(v/10.0) if v != -32768 else None,
'lowOutTemp' : _big_val10,
'inHumidity' : _little_val,
'outHumidity' : _little_val,
'windSpeed' : _little_val,
'windDir' : _windDir,
'windGust' : _null_float,
'windGustDir' : _windDir,
'rain' : _val100,
'rainRate' : _val100,
'ET' : _val1000,
'radiation' : _big_val,
'highRadiation' : _big_val,
'UV' : _little_val10,
'highUV' : _little_val10,
'extraTemp1' : _little_temp,
'extraTemp2' : _little_temp,
'extraTemp3' : _little_temp,
'soilTemp1' : _little_temp,
'soilTemp2' : _little_temp,
'soilTemp3' : _little_temp,
'soilTemp4' : _little_temp,
'leafTemp1' : _little_temp,
'leafTemp2' : _little_temp,
'extraHumid1' : _little_val,
'extraHumid2' : _little_val,
'soilMoist1' : _little_val,
'soilMoist2' : _little_val,
'soilMoist3' : _little_val,
'soilMoist4' : _little_val,
'leafWet1' : _little_val,
'leafWet2' : _little_val,
'leafWet3' : _little_val,
'leafWet4' : _little_val,
'forecastRule' : _null,
'readClosed' : _null,
'readOpened' : _null}
rec_format_A =[('date_stamp', 'H'), ('time_stamp', 'H'), ('outTemp', 'h'),
('highOutTemp', 'h'), ('lowOutTemp', 'h'), ('rain', 'H'),
('rainRate', 'H'), ('barometer', 'H'), ('radiation', 'H'),
('number_of_wind_samples', 'H'), ('inTemp', 'h'), ('inHumidity', 'B'),
('outHumidity', 'B'), ('windSpeed', 'B'), ('windGust', 'B'),
('windGustDir', 'B'), ('windDir', 'B'), ('UV', 'B'),
('ET', 'B'), ('invalid_data', 'B'), ('soilMoist1', 'B'),
('soilMoist2', 'B'), ('soilMoist3', 'B'), ('soilMoist4', 'B'),
('soilTemp1', 'B'), ('soilTemp2', 'B'), ('soilTemp3', 'B'),
('soilTemp4', 'B'), ('leafWet1', 'B'), ('leafWet2', 'B'),
('leafWet3', 'B'), ('leafWet4', 'B'), ('extraTemp1', 'B'),
('extraTemp2', 'B'), ('extraHumid1', 'B'), ('extraHumid2','B'),
('readClosed', 'H'), ('readOpened', 'H'), ('unused', 'B')]
rec_format_B = [('date_stamp', 'H'), ('time_stamp', 'H'), ('outTemp', 'h'),
('highOutTemp', 'h'), ('lowOutTemp', 'h'), ('rain', 'H'),
('rainRate', 'H'), ('barometer', 'H'), ('radiation', 'H'),
('number_of_wind_samples', 'H'), ('inTemp', 'h'), ('inHumidity', 'B'),
('outHumidity', 'B'), ('windSpeed', 'B'), ('windGust', 'B'),
('windGustDir', 'B'), ('windDir', 'B'), ('UV', 'B'),
('ET', 'B'), ('highRadiation', 'H'), ('highUV', 'B'),
('forecastRule', 'B'), ('leafTemp1', 'B'), ('leafTemp2', 'B'),
('leafWet1', 'B'), ('leafWet2', 'B'), ('soilTemp1', 'B'),
('soilTemp2', 'B'), ('soilTemp3', 'B'), ('soilTemp4', 'B'),
('download_record_type', 'B'), ('extraHumid1', 'B'), ('extraHumid2','B'),
('extraTemp1', 'B'), ('extraTemp2', 'B'), ('extraTemp3', 'B'),
('soilMoist1', 'B'), ('soilMoist2', 'B'), ('soilMoist3', 'B'),
('soilMoist4', 'B')]
rec_types_A, fmt_A = zip(*rec_format_A)
rec_types_B, fmt_B = zip(*rec_format_B)
rec_fmt_A = struct.Struct('<' + ''.join(fmt_A))
rec_fmt_B = struct.Struct('<' + ''.join(fmt_B))
compass_points = {'N': 0, 'NNE': 22.5, 'NE': 45, 'ENE': 67.5, 'E': 90,
'ESE': 112.5, 'SE': 135, 'SSE': 157.5, 'S': 180,
'SSW': 202.5, 'SW': 225, 'WSW': 247.5, 'W': 270,
'WNW': 292.5, 'NW': 315, 'NNW': 337.5}
# crude parser to get data from the weatherlink web page.
# works with html up to at least late 2015
class WLParserA(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.packet = {}
self.hierarchy = []
self.last_value = None
self.last_key = None
def handle_starttag(self, tag, attrs):
if tag not in ['meta', 'link', 'br']:
self.hierarchy.append(tag)
def handle_endtag(self, tag):
self.hierarchy.pop()
def handle_data(self, data):
data = data.strip()
if not 'span' in self.hierarchy:
if len(data):
self.last_key = data
return
if data in compass_points:
self.packet['windDir'] = compass_points[data]
elif self.last_value is not None:
if data == self.last_value:
try:
v = float(self.last_value)
self.packet['outTemperature'] = v
except ValueError:
pass
elif data == 'km/h' or data == 'Mph':
self.packet['windSpeed'] = float(self.last_value)
elif data == '%':
self.packet['outHumidity'] = float(self.last_value)
self.last_value = None
elif re.search('\d+mm', data):
m = re.search('(\d+)mm', data)
self.packet['rain'] = float(m.group(1)) / 10 # weewx wants cm
elif re.search('[\d.]+mb', data):
m = re.search('([\d.]+)mb', data)
self.packet['barometer'] = float(m.group(1))
elif re.search('[\d.]+hPa', data):
m = re.search('([\d.]+)hPa', data)
self.packet['barometer'] = float(m.group(1))
elif re.search('[\d.]+"', data):
m = re.search('([\d.]+)"', data)
if self.last_key == 'Rain':
self.packet['rain'] = float(m.group(1))
elif self.last_key == 'Barometer':
self.packet['barometer'] = float(m.group(1))
self.last_key = None
else:
self.last_value = data
def get_data(self):
return self.packet
# works with weatherlink html as of 2016
# pick off temperature, wind, humidity, rain, barometer
class WLParserB(HTMLParser):
def __init__(self):
HTMLParser.__init__(self)
self.packet = {}
self.hierarchy = []
self.last_key = 'Temperature'
def handle_starttag(self, tag, attrs):
if tag not in ['meta', 'link', 'br']:
self.hierarchy.append(tag)
def handle_endtag(self, tag):
self.hierarchy.pop()
def handle_data(self, data):
if not 'body' in self.hierarchy:
return
logdev('%s' % self.hierarchy)
data = data.strip()
logdev('%s' % data)
if len(data) == 0:
return
if data in ['Wind', 'Humidity', 'Rain', 'Barometer']:
self.last_key = data
logdev('found key %s' % self.last_key)
return
if self.last_key == 'Wind':
if data == 'Calm':
self.packet['windSpeed'] = 0
self.packet['windDir'] = None
elif re.search('[NSEW]+', data):
m = re.search('([NSEW]+)', data)
wdir = m.group(1)
if wdir in compass_points:
self.packet['windDir'] = compass_points[wdir]
logdev('windDir: %s' % self.packet.get('windDir'))
# do not reset last_key since we still need a wind speed
elif re.search('[\d.]+', data):
m = re.search('([\d.]+)', data)
self.packet['windSpeed'] = float(m.group(1))
logdev('windSpeed: %s' % self.packet.get('windSpeed'))
self.last_key = None
if self.last_key == 'Temperature':
try:
v = float(data)
self.packet['outTemperature'] = v
logdev('outTemperature: %s' % self.packet['outTemperature'])
self.last_key = None
except ValueError:
pass
if self.last_key == 'Humidity':
self.packet['outHumidity'] = float(data)
logdev('outHumidity: %s' % self.packet['outHumidity'])
self.last_key = None
if self.last_key == 'Rain':
if re.search('[\d.]+mm', data):
m = re.search('([\d.]+)mm', data)
self.packet['rain'] = float(m.group(1)) / 10 # weewx wants cm
logdev('rain: %s' % self.packet['rain'])
elif re.search('[\d.]+"', data):
m = re.search('([\d.]+)"', data)
self.packet['rain'] = float(m.group(1))
logdev('rain: %s' % self.packet['rain'])
self.last_key = None
if self.last_key == 'Barometer':
if re.search('[\d.]+mb', data):
m = re.search('([\d.]+)mb', data)
self.packet['barometer'] = float(m.group(1))
logdev('barometer: %s' % self.packet['barometer']) # FIXME
elif re.search('[\d.]+hPa', data):
m = re.search('([\d.]+)hPa', data)
self.packet['barometer'] = float(m.group(1))
logdev('barometer: %s' % self.packet['barometer']) # FIXME
elif re.search('[\d.]+"', data):
m = re.search('([\d.]+)"', data)
self.packet['barometer'] = float(m.group(1))
logdev('barometer: %s' % self.packet['barometer'])
self.last_key = None
def get_data(self):
return self.packet
# To test this driver, do the following:
# PYTHONPATH=/home/weewx/bin python /home/weewx/bin/user/wlink.py
if __name__ == "__main__":
usage = """%prog [options]"""
import optparse
parser = optparse.OptionParser(usage=usage)
parser.add_option('--username', dest='username', default='user',
help='weatherlink username')
parser.add_option('--password', dest='password', default='pass',
help='weatherlink password')
parser.add_option('--test-driver', dest='test_driver', action='store_true',
help='test the driver')
parser.add_option('--test-parser', dest='test_parser', action='store_true',
help='test the parser')
parser.add_option('--filename', dest='filename', default='testfile.xml',
help='name of file for test-parser')
parser.add_option('--format', dest='format', default='B',
help='html format, can be A or B')
(options, args) = parser.parse_args()
syslog.setlogmask(syslog.LOG_UPTO(syslog.LOG_DEBUG))
if options.test_parser:
data = []
with open(options.filename) as f:
for line in f:
data.append(line)
parser = WLParserB()
if options.format == 'A':
parser = WLParserA()
parser.feed(''.join(data))
print parser.get_data()
elif options.test_driver:
import weeutil.weeutil
station = WeatherLink(username=options.username,
password=options.password)
for p in station.genLoopPackets():
print weeutil.weeutil.timestamp_to_string(p['dateTime']), p